@ifc-lite/renderer 1.43.0 → 1.44.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/camera-animation.d.ts +20 -32
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +71 -358
- package/dist/camera-animation.js.map +1 -1
- package/dist/camera-controls.d.ts +11 -51
- package/dist/camera-controls.d.ts.map +1 -1
- package/dist/camera-controls.js +140 -12
- package/dist/camera-controls.js.map +1 -1
- package/dist/camera-first-person.d.ts +52 -0
- package/dist/camera-first-person.d.ts.map +1 -0
- package/dist/camera-first-person.js +119 -0
- package/dist/camera-first-person.js.map +1 -0
- package/dist/camera-framing.d.ts +86 -0
- package/dist/camera-framing.d.ts.map +1 -0
- package/dist/camera-framing.js +230 -0
- package/dist/camera-framing.js.map +1 -0
- package/dist/camera-guards.d.ts +148 -0
- package/dist/camera-guards.d.ts.map +1 -0
- package/dist/camera-guards.js +150 -0
- package/dist/camera-guards.js.map +1 -0
- package/dist/camera-matrices.d.ts +14 -0
- package/dist/camera-matrices.d.ts.map +1 -0
- package/dist/camera-matrices.js +146 -0
- package/dist/camera-matrices.js.map +1 -0
- package/dist/camera-preset-view.d.ts +56 -0
- package/dist/camera-preset-view.d.ts.map +1 -0
- package/dist/camera-preset-view.js +180 -0
- package/dist/camera-preset-view.js.map +1 -0
- package/dist/camera-projection.d.ts +1 -1
- package/dist/camera-projection.d.ts.map +1 -1
- package/dist/camera-projection.js +83 -30
- package/dist/camera-projection.js.map +1 -1
- package/dist/camera-state.d.ts +62 -0
- package/dist/camera-state.d.ts.map +1 -0
- package/dist/camera-state.js +5 -0
- package/dist/camera-state.js.map +1 -0
- package/dist/camera.d.ts +22 -11
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +75 -120
- package/dist/camera.js.map +1 -1
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js +23 -1
- package/dist/environment.js.map +1 -1
- package/dist/index.d.ts +154 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +294 -45
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +49 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +122 -67
- package/dist/math.js.map +1 -1
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
- package/dist/renderer-symbolic-overlays.js +20 -25
- package/dist/renderer-symbolic-overlays.js.map +1 -1
- package/dist/scene-geometry.d.ts +32 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +66 -0
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +22 -97
- package/dist/scene.js.map +1 -1
- package/dist/section-2d-lift.d.ts +105 -0
- package/dist/section-2d-lift.d.ts.map +1 -0
- package/dist/section-2d-lift.js +162 -0
- package/dist/section-2d-lift.js.map +1 -0
- package/dist/section-2d-line-buffer.d.ts +58 -0
- package/dist/section-2d-line-buffer.d.ts.map +1 -0
- package/dist/section-2d-line-buffer.js +109 -0
- package/dist/section-2d-line-buffer.js.map +1 -0
- package/dist/section-2d-overlay.d.ts +28 -77
- package/dist/section-2d-overlay.d.ts.map +1 -1
- package/dist/section-2d-overlay.js +161 -593
- package/dist/section-2d-overlay.js.map +1 -1
- package/dist/section-plane-basis.d.ts +22 -3
- package/dist/section-plane-basis.d.ts.map +1 -1
- package/dist/section-plane-basis.js +51 -10
- package/dist/section-plane-basis.js.map +1 -1
- package/dist/section-plane.d.ts.map +1 -1
- package/dist/section-plane.js +10 -1
- package/dist/section-plane.js.map +1 -1
- package/dist/shaders/section-2d-overlay.wgsl.d.ts +102 -0
- package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -0
- package/dist/shaders/section-2d-overlay.wgsl.js +281 -0
- package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -11,6 +11,13 @@ export { pickFitPolicy } from './camera-fit-policy.js';
|
|
|
11
11
|
export { Scene } from './scene.js';
|
|
12
12
|
export { Picker } from './picker.js';
|
|
13
13
|
export { MathUtils } from './math.js';
|
|
14
|
+
// The orthonormal camera basis `MathUtils.lookAt` renders through, exposed so
|
|
15
|
+
// that a consumer which has to reconstruct the on-screen frame outside the
|
|
16
|
+
// renderer derives it from the same substitution the picture used, instead of
|
|
17
|
+
// recomputing `cross(forward, up)` and inventing its own answer for a
|
|
18
|
+
// degenerate `up` (#2467 made this call inside the package; the Cesium overlay
|
|
19
|
+
// is the same situation from outside it).
|
|
20
|
+
export { viewBasis } from './math.js';
|
|
14
21
|
export { SectionPlaneRenderer } from './section-plane.js';
|
|
15
22
|
export { Section2DOverlayRenderer } from './section-2d-overlay.js';
|
|
16
23
|
// IfcAnnotation overlay pipelines (3D world-space). Self-contained — caller
|
|
@@ -47,7 +54,7 @@ import { RenderPipeline } from './pipeline.js';
|
|
|
47
54
|
import { Camera } from './camera.js';
|
|
48
55
|
import { Scene } from './scene.js';
|
|
49
56
|
import { Picker } from './picker.js';
|
|
50
|
-
import { MathUtils } from './math.js';
|
|
57
|
+
import { MathUtils, viewBasis } from './math.js';
|
|
51
58
|
import { FrustumUtils } from '@ifc-lite/spatial';
|
|
52
59
|
import { VisualEnhancementResolver } from './visual-enhancement.js';
|
|
53
60
|
import { packClipBox } from './clip-box.js';
|
|
@@ -127,6 +134,33 @@ function computeBvhFingerprint(meshes) {
|
|
|
127
134
|
function isDeviceLossThrow(error) {
|
|
128
135
|
return typeof DOMException !== 'undefined' && error instanceof DOMException;
|
|
129
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The reason `whenReady()` rejects when the renderer is destroyed.
|
|
139
|
+
*
|
|
140
|
+
* A plain `Error` carrying a stable `name` rather than an exported subclass:
|
|
141
|
+
* consumers can discriminate it with `err.name === 'RendererDestroyedError'`
|
|
142
|
+
* without this package growing a new export (and without `instanceof` breaking
|
|
143
|
+
* across duplicated copies of the package).
|
|
144
|
+
*/
|
|
145
|
+
function rendererDestroyedError() {
|
|
146
|
+
const error = new Error('Renderer was destroyed before it became ready');
|
|
147
|
+
error.name = 'RendererDestroyedError';
|
|
148
|
+
return error;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The reason `whenReady()` rejects once the GPU device has been lost.
|
|
152
|
+
*
|
|
153
|
+
* Deliberately NOT `RendererDestroyedError`: a destroyed renderer is finished,
|
|
154
|
+
* while a lost one is dead only until the host re-initialises it (`init()`
|
|
155
|
+
* clears the latch and readiness is published again). A caller that wants to
|
|
156
|
+
* retry needs to tell those apart, so the loss carries its own `name` — same
|
|
157
|
+
* plain-`Error` shape, for the same reasons.
|
|
158
|
+
*/
|
|
159
|
+
function rendererDeviceLostError() {
|
|
160
|
+
const error = new Error('GPU device was lost before the renderer became ready');
|
|
161
|
+
error.name = 'RendererDeviceLostError';
|
|
162
|
+
return error;
|
|
163
|
+
}
|
|
130
164
|
/**
|
|
131
165
|
* Main renderer class
|
|
132
166
|
*/
|
|
@@ -165,9 +199,28 @@ export class Renderer {
|
|
|
165
199
|
highQuality: true,
|
|
166
200
|
};
|
|
167
201
|
pointCloudRenderer = null;
|
|
168
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Set true at the end of the LATEST `init()`; gates `whenReady()` and
|
|
204
|
+
* `isReady()`. Revoked synchronously by `init()` and by `destroy()`, and
|
|
205
|
+
* overridden (not cleared) by a device loss — see `deviceLost`, which the
|
|
206
|
+
* two readiness methods consult alongside this flag because the loss can
|
|
207
|
+
* land in the middle of the init that is about to set it.
|
|
208
|
+
*/
|
|
169
209
|
ready = false;
|
|
170
210
|
readyWaiters = [];
|
|
211
|
+
/**
|
|
212
|
+
* Set by the public `destroy()`, cleared synchronously by `init()`. It is
|
|
213
|
+
* the difference between "not ready YET" and "never going to be ready":
|
|
214
|
+
* `whenReady()` parks for the first and rejects for the second.
|
|
215
|
+
*
|
|
216
|
+
* Without it a caller parked across the teardown waits forever, because
|
|
217
|
+
* nothing after `destroy()` will ever reach `markReady()` — the host's
|
|
218
|
+
* remount builds a NEW Renderer rather than re-initialising this one. The
|
|
219
|
+
* private `teardown()` deliberately does NOT set it: the teardown
|
|
220
|
+
* `initOnce()` runs on the previous init's objects is part of an init that
|
|
221
|
+
* IS going to publish readiness, and its waiters must survive to be flushed.
|
|
222
|
+
*/
|
|
223
|
+
destroyed = false;
|
|
171
224
|
/**
|
|
172
225
|
* The tail of the `init()` queue. `init()` chains onto this rather than
|
|
173
226
|
* running immediately, so two overlapping calls cannot both walk past the
|
|
@@ -177,12 +230,33 @@ export class Renderer {
|
|
|
177
230
|
* caller) so one failure does not deadlock every later call.
|
|
178
231
|
*/
|
|
179
232
|
initChain = Promise.resolve();
|
|
233
|
+
/**
|
|
234
|
+
* Incremented synchronously by every `init()` call AND by every public
|
|
235
|
+
* `destroy()`. It stamps "the lifecycle event an in-flight init belongs to":
|
|
236
|
+
* an init that no longer carries the current stamp has been superseded and
|
|
237
|
+
* must neither allocate nor publish readiness.
|
|
238
|
+
*
|
|
239
|
+
* Both bumps are load-bearing, for the same reason. Because the queue above
|
|
240
|
+
* defers the body, an init can finish while a later one is still waiting its
|
|
241
|
+
* turn; that later call is about to tear down everything the earlier one
|
|
242
|
+
* built, so the earlier one must not publish readiness. And a host that calls
|
|
243
|
+
* `destroy()` while an init is parked on `await device.init(...)` gets the
|
|
244
|
+
* same hazard from the other direction: without the bump that init resumes,
|
|
245
|
+
* allocates a full replacement GPU stack nothing references, and re-publishes
|
|
246
|
+
* `ready` against a renderer that has already been torn down (#2465).
|
|
247
|
+
*
|
|
248
|
+
* The teardown `initOnce()` runs as part of its OWN re-init deliberately does
|
|
249
|
+
* NOT bump it — see `destroy()` vs `teardown()`. Bumping there would make
|
|
250
|
+
* every re-init invalidate itself, and nothing would ever become ready again.
|
|
251
|
+
*/
|
|
252
|
+
initGeneration = 0;
|
|
180
253
|
/**
|
|
181
254
|
* Set once the GPU device is lost for a non-intentional reason (driver
|
|
182
255
|
* reset / VRAM exhaustion — see `WebGPUDevice`). Every GPU resource is then
|
|
183
256
|
* dead, so `render()` becomes a no-op (it would only spew validation errors)
|
|
184
|
-
*
|
|
185
|
-
* `
|
|
257
|
+
* and the renderer stops reporting itself ready (`isReady()` goes false,
|
|
258
|
+
* `whenReady()` rejects) until the host re-initialises it. Consumers learn
|
|
259
|
+
* of this via `onDeviceLost` and typically respond by reloading the model.
|
|
186
260
|
*
|
|
187
261
|
* Two signals set it: the async `device.lost` promise (Chromium), and a
|
|
188
262
|
* frame throwing a `DOMException` out of `render()` (Safari 26.5, which
|
|
@@ -192,6 +266,29 @@ export class Renderer {
|
|
|
192
266
|
* device, and it must cost one frame, not the session.
|
|
193
267
|
*/
|
|
194
268
|
deviceLost = false;
|
|
269
|
+
/**
|
|
270
|
+
* The lifecycle generation the latched loss belongs to (see
|
|
271
|
+
* `initGeneration`); null until the first loss, and never cleared
|
|
272
|
+
* afterwards. It is only ever read next to `deviceLost`, which is what
|
|
273
|
+
* makes a stale stamp harmless — and that pairing is required, not
|
|
274
|
+
* cosmetic: a loss that latched between `init()` bumping the generation and
|
|
275
|
+
* its queued body running is stamped with the CURRENT generation, and only
|
|
276
|
+
* the flag that body clears says the renderer has moved on.
|
|
277
|
+
*
|
|
278
|
+
* `whenReady()` rejects only while this still equals the CURRENT generation,
|
|
279
|
+
* which is what re-arms the wait the instant a host calls `init()` — before
|
|
280
|
+
* the queued body has had a chance to clear `deviceLost` itself. Without
|
|
281
|
+
* that, `renderer.init(); await renderer.whenReady();` — the recovery shape
|
|
282
|
+
* `init()` already revokes readiness synchronously for — would reject inside
|
|
283
|
+
* the microtask window on a renderer that is being brought back up.
|
|
284
|
+
*
|
|
285
|
+
* Scoping it here rather than clearing `deviceLost` in `init()` keeps the
|
|
286
|
+
* flag meaning exactly one thing everywhere else: `render()`, the pick path
|
|
287
|
+
* and `getGPUDevice()` must stay shut for the OLD device across that same
|
|
288
|
+
* window, and clearing early would let frames run against dead GPU objects
|
|
289
|
+
* (and, on Safari, re-latch and re-notify the loss they already reported).
|
|
290
|
+
*/
|
|
291
|
+
deviceLostGeneration = null;
|
|
195
292
|
/** Retained so a listener registered AFTER the loss still learns of it. */
|
|
196
293
|
deviceLostInfo = null;
|
|
197
294
|
deviceLostListeners = new Set();
|
|
@@ -353,31 +450,62 @@ export class Renderer {
|
|
|
353
450
|
* rather than adding a second, differently-shaped rule.
|
|
354
451
|
*/
|
|
355
452
|
async init() {
|
|
453
|
+
// Revoke readiness SYNCHRONOUSLY, before the body is queued. Everything
|
|
454
|
+
// below runs in a later microtask (or, for a queued call, only after the
|
|
455
|
+
// one ahead of it settles), so leaving `ready` set would let
|
|
456
|
+
// `renderer.init(); await renderer.whenReady();` resolve immediately
|
|
457
|
+
// against the GPU objects this init is about to destroy — the very
|
|
458
|
+
// hazard the re-arm inside `initOnce()` exists to prevent. On a first
|
|
459
|
+
// init there is nothing to invalidate and this is a no-op.
|
|
460
|
+
const generation = ++this.initGeneration;
|
|
461
|
+
this.ready = false;
|
|
462
|
+
// Re-arm `whenReady()`: this instance is being brought back up, so a
|
|
463
|
+
// wait requested from here on is "not ready yet" again, not "destroyed".
|
|
464
|
+
this.destroyed = false;
|
|
356
465
|
// A previous init that REJECTED must not block the next one, so the
|
|
357
466
|
// stored link swallows the outcome. The caller still receives `run`, so
|
|
358
467
|
// rejections continue to surface exactly as before.
|
|
359
|
-
const run = this.initChain.then(() => this.initOnce(), () => this.initOnce());
|
|
468
|
+
const run = this.initChain.then(() => this.initOnce(generation), () => this.initOnce(generation));
|
|
360
469
|
this.initChain = run.then(() => undefined, () => undefined);
|
|
361
470
|
return run;
|
|
362
471
|
}
|
|
363
|
-
async initOnce() {
|
|
472
|
+
async initOnce(generation) {
|
|
364
473
|
// `pipeline` is the marker for "a previous init() completed": it is
|
|
365
474
|
// assigned unconditionally there and nulled by destroy().
|
|
366
475
|
if (this.pipeline !== null) {
|
|
367
|
-
|
|
368
|
-
//
|
|
369
|
-
// `
|
|
370
|
-
//
|
|
371
|
-
|
|
476
|
+
// Release the device the previous init() resolved on, and re-arm
|
|
477
|
+
// `whenReady()` so it cannot resolve against GPU objects that no
|
|
478
|
+
// longer exist. `teardown()`, not the public `destroy()`: this
|
|
479
|
+
// teardown is part of THIS init, so it must not invalidate this
|
|
480
|
+
// init's own generation.
|
|
481
|
+
this.teardown();
|
|
372
482
|
}
|
|
373
483
|
// Clear the lost flag so a re-init (destroy()+init() on the same instance)
|
|
374
484
|
// resumes rendering instead of staying a permanent no-op from an earlier loss.
|
|
485
|
+
// This also releases `whenReady()`'s rejection for the one case the
|
|
486
|
+
// generation stamp cannot: a loss that latched between `init()` bumping
|
|
487
|
+
// the generation and this body running is stamped with the generation
|
|
488
|
+
// that is clearing it. `deviceLostGeneration` deliberately keeps its
|
|
489
|
+
// stale value — it is only ever read alongside this flag.
|
|
375
490
|
this.deviceLost = false;
|
|
376
491
|
// Subscribe before the device exists so a loss during the first frames
|
|
377
492
|
// is never missed — the handler is only invoked when `device.lost`
|
|
378
493
|
// actually resolves (a real fault), long after init in practice.
|
|
379
494
|
this.device.onDeviceLost((info) => this.handleDeviceLost(info));
|
|
380
495
|
await this.device.init(this.canvas);
|
|
496
|
+
// A `destroy()` (or a newer `init()`) landed while we were parked on the
|
|
497
|
+
// device. Everything below allocates a full GPU stack — two pipelines,
|
|
498
|
+
// the picker, the post-processor, the point-cloud and deviation
|
|
499
|
+
// pipelines, the EDL pass, the overlay glyph atlas — and this aborted
|
|
500
|
+
// path runs no second teardown, so all of it would be orphaned outright
|
|
501
|
+
// (#2465). `markReady()`'s generation check is not enough on its own: it
|
|
502
|
+
// withholds the readiness PUBLICATION, not the allocation. Release the
|
|
503
|
+
// device we just brought up and stop here; a queued init will bring up
|
|
504
|
+
// its own.
|
|
505
|
+
if (generation !== this.initGeneration) {
|
|
506
|
+
this.device.destroy();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
381
509
|
// Get canvas dimensions (use pixel dimensions if set, otherwise use CSS dimensions)
|
|
382
510
|
// and clamp to the GPU's max 2D texture dimension so the initial pipeline allocations
|
|
383
511
|
// can't overflow on tall/wide layouts (see render() for the per-frame clamp).
|
|
@@ -441,7 +569,7 @@ export class Renderer {
|
|
|
441
569
|
// from the GPU click-pick provider above, since raycastScene* is
|
|
442
570
|
// synchronous CPU code while pick() is an async GPU readback.
|
|
443
571
|
this.raycastEngine.setPointCloudProvider(() => this.pointCloudRenderer?.getRayQuerySources() ?? []);
|
|
444
|
-
this.markReady();
|
|
572
|
+
this.markReady(generation);
|
|
445
573
|
}
|
|
446
574
|
/**
|
|
447
575
|
* Resolves once `init()` has finished and the GPU device + point-cloud
|
|
@@ -450,25 +578,80 @@ export class Renderer {
|
|
|
450
578
|
* the async WebGPU init resolves — should `await renderer.whenReady()`
|
|
451
579
|
* before `beginPointCloudStream`, which otherwise throws
|
|
452
580
|
* "Renderer not initialized".
|
|
581
|
+
*
|
|
582
|
+
* REJECTS (with an `Error` whose `name` is `RendererDestroyedError`) if
|
|
583
|
+
* `destroy()` runs while the caller is waiting, or if it already ran and no
|
|
584
|
+
* `init()` has been started since. It never resolves against a destroyed
|
|
585
|
+
* renderer — that is what this method exists to prevent — so the only
|
|
586
|
+
* alternative would be a promise that never settles, which suspends the
|
|
587
|
+
* caller's async frame permanently and takes everything the frame captured
|
|
588
|
+
* with it. The viewer reaches that state on an ordinary path: `Viewport`
|
|
589
|
+
* builds a NEW `Renderer` per mount and destroys the old one in its effect
|
|
590
|
+
* cleanup, so a `destroy()` there is FINAL for the instance a consumer
|
|
591
|
+
* captured — a point-cloud drop that straddles a layout swap or a
|
|
592
|
+
* StrictMode remount would otherwise hang mid-load, with no error, forever.
|
|
593
|
+
* Callers should handle the rejection as "the target went away", not as a
|
|
594
|
+
* load failure.
|
|
595
|
+
*
|
|
596
|
+
* It also REJECTS (`RendererDeviceLostError`) while the GPU device is lost.
|
|
597
|
+
* The device is what this method promises, and a lost one cannot serve the
|
|
598
|
+
* call the caller is waiting to make — `getGPUDevice()` returns null, so
|
|
599
|
+
* `beginPointCloudStream` throws "Renderer not initialized" the moment the
|
|
600
|
+
* wait resolves. The third outcome is the one `destroy()` already ruled out:
|
|
601
|
+
* parking a waiter that only a host-initiated `init()` could ever settle,
|
|
602
|
+
* and that the viewer's usual response to a loss (drop this renderer, build
|
|
603
|
+
* a new one) guarantees will never come. Unlike the destroyed case this is
|
|
604
|
+
* NOT final — a later `init()` on the same instance re-arms the wait
|
|
605
|
+
* synchronously, so "retry after re-init" is a contract callers can act on,
|
|
606
|
+
* which is why the two rejections carry different names.
|
|
453
607
|
*/
|
|
454
608
|
whenReady() {
|
|
609
|
+
// Checked before `ready`, which an init that completed after the loss
|
|
610
|
+
// latched may well have published (`init()` subscribes to the device's
|
|
611
|
+
// loss signal before awaiting it, so a loss DURING init leaves both
|
|
612
|
+
// flags set). Readiness is about the device, and the device is gone.
|
|
613
|
+
if (this.deviceLost && this.deviceLostGeneration === this.initGeneration) {
|
|
614
|
+
return Promise.reject(rendererDeviceLostError());
|
|
615
|
+
}
|
|
455
616
|
if (this.ready)
|
|
456
617
|
return Promise.resolve();
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
618
|
+
if (this.destroyed)
|
|
619
|
+
return Promise.reject(rendererDestroyedError());
|
|
620
|
+
return new Promise((resolve, reject) => { this.readyWaiters.push({ resolve, reject }); });
|
|
621
|
+
}
|
|
622
|
+
markReady(generation) {
|
|
623
|
+
// A newer init() is already queued: it will tear all of this down before
|
|
624
|
+
// building its own, so publishing readiness here would hand callers a
|
|
625
|
+
// device with a demolition order on it.
|
|
626
|
+
if (generation !== this.initGeneration)
|
|
627
|
+
return;
|
|
460
628
|
this.ready = true;
|
|
461
629
|
const waiters = this.readyWaiters;
|
|
462
630
|
this.readyWaiters = [];
|
|
463
631
|
for (const w of waiters)
|
|
464
|
-
w();
|
|
632
|
+
w.resolve();
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Fail every parked `whenReady()` waiter with `error`. Called by `destroy()`
|
|
636
|
+
* and by `handleDeviceLost()` — the two events after which nothing this
|
|
637
|
+
* instance does on its own can make the wait true. NOT by `teardown()`,
|
|
638
|
+
* whose waiters belong to the re-init running it and must survive to be
|
|
639
|
+
* flushed by it.
|
|
640
|
+
*/
|
|
641
|
+
rejectReadyWaiters(error) {
|
|
642
|
+
const waiters = this.readyWaiters;
|
|
643
|
+
this.readyWaiters = [];
|
|
644
|
+
for (const w of waiters)
|
|
645
|
+
w.reject(error);
|
|
465
646
|
}
|
|
466
647
|
/**
|
|
467
648
|
* Subscribe to non-intentional GPU device loss (driver reset / VRAM
|
|
468
649
|
* exhaustion — NOT an intentional `destroy()`). Fired at most once per
|
|
469
|
-
* device. After it fires, `render()` is a no-op
|
|
470
|
-
*
|
|
471
|
-
*
|
|
650
|
+
* device. After it fires, `render()` is a no-op and the renderer reports
|
|
651
|
+
* itself un-ready (`isReady()` false, `whenReady()` rejecting with
|
|
652
|
+
* `RendererDeviceLostError`) until it is re-initialised, so the typical
|
|
653
|
+
* response is to dispose this renderer and reload the model. Returns an
|
|
654
|
+
* unsubscribe function.
|
|
472
655
|
*
|
|
473
656
|
* Camera and model state live on the CPU (JS) and survive device loss, so a
|
|
474
657
|
* reload restores the model at its current orientation — the loss is a GPU
|
|
@@ -522,8 +705,17 @@ export class Renderer {
|
|
|
522
705
|
if (this.deviceLost)
|
|
523
706
|
return;
|
|
524
707
|
this.deviceLost = true;
|
|
708
|
+
this.deviceLostGeneration = this.initGeneration;
|
|
525
709
|
this.deviceLostInfo = info;
|
|
526
710
|
console.warn('[Renderer] GPU device lost — halting rendering until re-init:', info.message);
|
|
711
|
+
// Readiness describes the GPU objects, and every one of them just died:
|
|
712
|
+
// `isReady()` reports it from here on, and anyone parked in
|
|
713
|
+
// `whenReady()` is failed rather than left to be resolved by the init
|
|
714
|
+
// this loss may have landed in the middle of. Done BEFORE the listeners
|
|
715
|
+
// run, so a listener that recovers by calling `init()` synchronously
|
|
716
|
+
// finds the waiters already settled and re-arms the wait for the next
|
|
717
|
+
// caller rather than racing the flush.
|
|
718
|
+
this.rejectReadyWaiters(rendererDeviceLostError());
|
|
527
719
|
for (const listener of this.deviceLostListeners) {
|
|
528
720
|
try {
|
|
529
721
|
listener(info);
|
|
@@ -1746,31 +1938,22 @@ export class Renderer {
|
|
|
1746
1938
|
sampleCount: this.pipeline.getSampleCount(),
|
|
1747
1939
|
}, skyShaderSource);
|
|
1748
1940
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
//
|
|
1760
|
-
|
|
1761
|
-
let ry = fz * camUp.x - fx * camUp.z;
|
|
1762
|
-
let rz = fx * camUp.y - fy * camUp.x;
|
|
1763
|
-
const rlen = Math.hypot(rx, ry, rz) || 1;
|
|
1764
|
-
rx /= rlen;
|
|
1765
|
-
ry /= rlen;
|
|
1766
|
-
rz /= rlen;
|
|
1767
|
-
const ux = ry * fz - rz * fy;
|
|
1768
|
-
const uy = rz * fx - rx * fz;
|
|
1769
|
-
const uz = rx * fy - ry * fx;
|
|
1941
|
+
// The sky shader rebuilds a per-pixel view ray from this
|
|
1942
|
+
// basis, so it must be the basis the frame's view matrix was
|
|
1943
|
+
// built from — `viewBasis`, not a local re-derivation
|
|
1944
|
+
// (#2489). The copy that used to live here guarded its two
|
|
1945
|
+
// divisors with `|| 1` and neither numerator, so a non-finite
|
|
1946
|
+
// camera coordinate made every axis NaN and the sky drew as a
|
|
1947
|
+
// flat undefined colour over the whole viewport; and for a
|
|
1948
|
+
// plan pose (`up` parallel to the view direction) it returned
|
|
1949
|
+
// zero-length axes, which is the same picture. Reading the
|
|
1950
|
+
// shared basis also keeps the horizon in the sky aligned with
|
|
1951
|
+
// the horizon in the geometry for free.
|
|
1952
|
+
const camBasis = viewBasis(this.camera.getPosition(), this.camera.getTarget(), this.camera.getUp());
|
|
1770
1953
|
this.skyPass.draw(pass, {
|
|
1771
|
-
forward: [
|
|
1772
|
-
right: [
|
|
1773
|
-
up: [
|
|
1954
|
+
forward: [camBasis.forward.x, camBasis.forward.y, camBasis.forward.z],
|
|
1955
|
+
right: [camBasis.right.x, camBasis.right.y, camBasis.right.z],
|
|
1956
|
+
up: [camBasis.up.x, camBasis.up.y, camBasis.up.z],
|
|
1774
1957
|
fovY: this.camera.getFOV(),
|
|
1775
1958
|
aspect: this.canvas.height > 0 ? this.canvas.width / this.canvas.height : 1,
|
|
1776
1959
|
}, environment);
|
|
@@ -2772,6 +2955,17 @@ export class Renderer {
|
|
|
2772
2955
|
* Resize canvas
|
|
2773
2956
|
*/
|
|
2774
2957
|
resize(width, height) {
|
|
2958
|
+
// `canvas.width` is an IDL `unsigned long`, so it silently coerces a
|
|
2959
|
+
// non-finite or negative argument to **0** — a zero drawing buffer
|
|
2960
|
+
// that every pick guard in this package misses, because they all
|
|
2961
|
+
// check the bounding rect rather than the buffer. `unprojectToRay`
|
|
2962
|
+
// then divides by it. This is documented public API of a published
|
|
2963
|
+
// package (`docs/api/typescript.md`), so an external caller wiring a
|
|
2964
|
+
// ResizeObserver to it is the reachable route; both in-repo callers
|
|
2965
|
+
// already floor their own values. Keep the last usable size, the same
|
|
2966
|
+
// policy `setAspect` uses for the ratio it derives (#2473).
|
|
2967
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
|
|
2968
|
+
return;
|
|
2775
2969
|
this.canvas.width = width;
|
|
2776
2970
|
this.canvas.height = height;
|
|
2777
2971
|
this.camera.setAspect(width / height);
|
|
@@ -2926,10 +3120,27 @@ export class Renderer {
|
|
|
2926
3120
|
return this.pipeline;
|
|
2927
3121
|
}
|
|
2928
3122
|
/**
|
|
2929
|
-
* Check if renderer is fully initialized and ready to use
|
|
3123
|
+
* Check if renderer is fully initialized and ready to use.
|
|
3124
|
+
*
|
|
3125
|
+
* `ready` is part of the test, not decoration: between `init()` being called
|
|
3126
|
+
* and its queued body running, the device and pipeline still belong to the
|
|
3127
|
+
* PREVIOUS init and are about to be destroyed, so the other two conditions
|
|
3128
|
+
* alone would report a renderer that is on its way out as usable.
|
|
3129
|
+
*
|
|
3130
|
+
* So is the device-loss check. A lost device is never torn down —
|
|
3131
|
+
* `WebGPUDevice.destroy()` is the only thing that nulls the handle and an
|
|
3132
|
+
* involuntary loss (driver reset / VRAM exhaustion / GPU-process crash)
|
|
3133
|
+
* never calls it — so `isInitialized()` stays true, the pipeline stays
|
|
3134
|
+
* non-null, and `ready` stays set from the init that completed before the
|
|
3135
|
+
* loss. All three conditions therefore still hold while `render()` is a
|
|
3136
|
+
* no-op and `getGPUDevice()` returns null: the renderer would report itself
|
|
3137
|
+
* usable through this third door alone. Unlike the two revocations above
|
|
3138
|
+
* this one needs no generation scoping — an `init()` clears `ready`
|
|
3139
|
+
* synchronously, so a latch left standing until the queued body clears it
|
|
3140
|
+
* cannot make this method spuriously false in the meantime.
|
|
2930
3141
|
*/
|
|
2931
3142
|
isReady() {
|
|
2932
|
-
return this.device.isInitialized() && this.pipeline !== null;
|
|
3143
|
+
return this.ready && !this.deviceLost && this.device.isInitialized() && this.pipeline !== null;
|
|
2933
3144
|
}
|
|
2934
3145
|
/**
|
|
2935
3146
|
* Get the GPU device (returns null if not initialized, or if the device
|
|
@@ -2985,8 +3196,46 @@ export class Renderer {
|
|
|
2985
3196
|
* post-processing buffers, section-plane renderers, and snap caches.
|
|
2986
3197
|
* After calling this method the renderer is no longer usable.
|
|
2987
3198
|
* Safe to call multiple times (idempotent).
|
|
3199
|
+
*
|
|
3200
|
+
* An `init()` still in flight is invalidated too. It is parked on
|
|
3201
|
+
* `await device.init(...)`, and without the generation bump below it resumes
|
|
3202
|
+
* after this returns, allocates a complete replacement GPU stack that nothing
|
|
3203
|
+
* references, and re-publishes `ready` — resolving `whenReady()` waiters
|
|
3204
|
+
* against a renderer the host has already torn down (#2465). The bump is what
|
|
3205
|
+
* `initOnce()` re-checks after its await, and what makes `markReady()` refuse
|
|
3206
|
+
* the stale completion.
|
|
3207
|
+
*
|
|
3208
|
+
* This is why the teardown itself lives in `teardown()`: `initOnce()` runs it
|
|
3209
|
+
* on the PREVIOUS init's objects as part of its own re-init, and routing that
|
|
3210
|
+
* through here would have every init invalidate its own generation, leaving
|
|
3211
|
+
* `whenReady()` pending forever.
|
|
3212
|
+
*
|
|
3213
|
+
* Anyone parked in `whenReady()` is FAILED rather than left pending. Nothing
|
|
3214
|
+
* after this call can make the wait true — the invalidation above is exactly
|
|
3215
|
+
* what stops the in-flight init from publishing readiness, and a host that
|
|
3216
|
+
* remounts builds a new `Renderer` rather than re-initialising this one — so
|
|
3217
|
+
* leaving the promise unsettled suspends the caller's async frame for the
|
|
3218
|
+
* lifetime of the page. `apps/viewer`'s point-cloud drop is one of those
|
|
3219
|
+
* frames: it captured this instance before the teardown, and would stop
|
|
3220
|
+
* mid-load with the spinner still up and no error to report. See
|
|
3221
|
+
* `whenReady()` for the rejection contract.
|
|
2988
3222
|
*/
|
|
2989
3223
|
destroy() {
|
|
3224
|
+
this.initGeneration++;
|
|
3225
|
+
this.destroyed = true;
|
|
3226
|
+
this.teardown();
|
|
3227
|
+
this.rejectReadyWaiters(rendererDestroyedError());
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Release every GPU object this renderer owns, WITHOUT invalidating an
|
|
3231
|
+
* in-flight init. Callers: the public `destroy()` (which invalidates first)
|
|
3232
|
+
* and `initOnce()`, tearing down the previous init before building its own.
|
|
3233
|
+
*/
|
|
3234
|
+
teardown() {
|
|
3235
|
+
// Nothing below survives this call, so `whenReady()` / `isReady()` must
|
|
3236
|
+
// go back to waiting. Set first: every release below is synchronous, but
|
|
3237
|
+
// the flag is what a caller holding a live reference actually reads.
|
|
3238
|
+
this.ready = false;
|
|
2990
3239
|
// Scene mesh GPU buffers
|
|
2991
3240
|
this.scene.clear();
|
|
2992
3241
|
// Re-arm the section-bounds diagnostic log for the next model.
|