@ifc-lite/renderer 1.37.0 → 1.38.1
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/device.d.ts +32 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +62 -2
- package/dist/device.js.map +1 -1
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +262 -57
- package/dist/index.js.map +1 -1
- package/dist/scene.d.ts +29 -3
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +120 -35
- package/dist/scene.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/visibility-epoch.d.ts +30 -0
- package/dist/visibility-epoch.d.ts.map +1 -0
- package/dist/visibility-epoch.js +64 -0
- package/dist/visibility-epoch.js.map +1 -0
- package/package.json +4 -4
package/dist/scene.js
CHANGED
|
@@ -8,6 +8,7 @@ import { sumResidentGpuBytes } from './render-stats.js';
|
|
|
8
8
|
import { simplifyIndicesByClustering, lodCellSizeForBounds, LOD_MIN_TRIANGLES } from './lod-simplify.js';
|
|
9
9
|
import { quantizeInterleaved } from './quantize.js';
|
|
10
10
|
import { bucketBaseKeyFor } from './chunk-grid.js';
|
|
11
|
+
import { VisibilityEpochTracker } from './visibility-epoch.js';
|
|
11
12
|
import { selectEvictions } from './residency.js';
|
|
12
13
|
import { OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
|
|
13
14
|
import { prepareInstancedRender, foldOccurrenceWorldBox, INSTANCE_STRIDE_BYTES, INSTANCE_COLOR_OFFSET, INSTANCE_FLAGS_OFFSET, INSTANCE_FLAG_SELECTED, INSTANCE_FLAG_HIDDEN, } from './instanced-render.js';
|
|
@@ -39,8 +40,11 @@ export class Scene {
|
|
|
39
40
|
instancedHidden = new Set(); // currently hidden instanced express_ids (hide/isolate)
|
|
40
41
|
instancedOverridden = new Set(); // currently colour-overridden instanced express_ids
|
|
41
42
|
instancedHasTransparent = false; // an override made some instanced occurrence translucent
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
// Content-based change guard for setInstancedVisibility — same contract as
|
|
44
|
+
// RenderOptions.hiddenIds (in-place mutation and fresh identical Sets both
|
|
45
|
+
// behave), keeping the instanced path in lockstep with the batched path.
|
|
46
|
+
instancedVisibilityEpochs = new VisibilityEpochTracker();
|
|
47
|
+
lastInstancedVisibilityVersion = -1;
|
|
44
48
|
instancedVisibilityDirty = false; // set when a new shard adds occurrences → re-apply visibility
|
|
45
49
|
// Buffer-size-aware bucket splitting: when a single color group's geometry
|
|
46
50
|
// would exceed the GPU maxBufferSize, overflow is directed to a new
|
|
@@ -96,6 +100,11 @@ export class Scene {
|
|
|
96
100
|
// This allows rendering partially visible batches as single draw calls instead of 10,000+ individual draws
|
|
97
101
|
partialBatchCache = new Map();
|
|
98
102
|
partialBatchCacheKeys = new Map(); // sourceBatchKey -> current cache key (for invalidation)
|
|
103
|
+
// sourceBatchKey -> visibility/override epoch its cached partial batch was
|
|
104
|
+
// built for. Lets getOrCreatePartialBatch return the cached clone WITHOUT
|
|
105
|
+
// re-sorting + re-hashing every visible id each frame while the epoch holds
|
|
106
|
+
// (issue: O(elements) per-frame work under hide/isolate). See render loop.
|
|
107
|
+
partialBatchCacheVersions = new Map();
|
|
99
108
|
// Color overlay system for lens coloring — NEVER modifies original batches.
|
|
100
109
|
// Overlay batches render on top using depthCompare 'equal', so they only
|
|
101
110
|
// paint where original geometry already wrote depth. Clearing is instant.
|
|
@@ -103,6 +112,11 @@ export class Scene {
|
|
|
103
112
|
// Defensively-typed: the renderer is the sole writer (via setColorOverrides),
|
|
104
113
|
// external readers go through getColorOverrides() and get a ReadonlyMap.
|
|
105
114
|
colorOverrides = null;
|
|
115
|
+
// Bumped whenever the colour-override set changes. The partial sub-batch's
|
|
116
|
+
// visible subset depends on override promotion (splitVisibleIdsByPromotion),
|
|
117
|
+
// so the render loop folds this into the partial-batch cache epoch to keep the
|
|
118
|
+
// per-frame fast path correct when overrides change with no visibility change.
|
|
119
|
+
colorOverrideGeneration = 0;
|
|
106
120
|
// Streaming optimization: track pending batch rebuilds
|
|
107
121
|
pendingBatchKeys = new Set();
|
|
108
122
|
// Temporary fragment batches created during streaming for immediate rendering.
|
|
@@ -571,7 +585,58 @@ export class Scene {
|
|
|
571
585
|
this.partialBatchCache.delete(cacheKey);
|
|
572
586
|
}
|
|
573
587
|
this.partialBatchCacheKeys.delete(sourceBatchKey);
|
|
588
|
+
this.partialBatchCacheVersions.delete(sourceBatchKey);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/** Destroy + drop EVERY cached partial sub-batch. The clones built during
|
|
592
|
+
* hide/isolate are deliberately excluded from the GPU residency budget and
|
|
593
|
+
* are otherwise only freed on clear()/finalize/evict — never when filtering
|
|
594
|
+
* ends. The render loop calls this on the transition back to fully-visible so
|
|
595
|
+
* the ~model-sized clone VRAM is not pinned until the next model reload. Uses
|
|
596
|
+
* the same destroy-then-clear idiom as clear(); safe to call between frames
|
|
597
|
+
* because the previous frame is already submitted (WebGPU defers the free
|
|
598
|
+
* past in-flight work). */
|
|
599
|
+
dropAllPartialCaches() {
|
|
600
|
+
if (this.partialBatchCache.size === 0
|
|
601
|
+
&& this.partialBatchCacheKeys.size === 0
|
|
602
|
+
&& this.partialBatchCacheVersions.size === 0) {
|
|
603
|
+
return;
|
|
574
604
|
}
|
|
605
|
+
for (const batch of this.partialBatchCache.values())
|
|
606
|
+
destroyGpuResources(batch);
|
|
607
|
+
this.partialBatchCache.clear();
|
|
608
|
+
this.partialBatchCacheKeys.clear();
|
|
609
|
+
this.partialBatchCacheVersions.clear();
|
|
610
|
+
}
|
|
611
|
+
/** Free the hydrated (pick / selection-highlight) individual meshes that are
|
|
612
|
+
* no longer selected, destroying their GPU buffers and dropping them from
|
|
613
|
+
* `this.meshes`. A mesh is kept iff its expressId is in `keep` AND it
|
|
614
|
+
* matches `keepModelIndex` (undefined = any model) — the same predicate the
|
|
615
|
+
* render loop uses to draw selection highlights, so disposal is its exact
|
|
616
|
+
* complement. The model scoping matters for federation: models can share
|
|
617
|
+
* express ids, and an id-only check would strand the OTHER model's hydrated
|
|
618
|
+
* mesh resident and drawing when selection moves across models. Only meshes
|
|
619
|
+
* flagged `hydrated` are touched — authored geometry added via addMesh()
|
|
620
|
+
* and batch geometry are left untouched. Returns how many were freed. */
|
|
621
|
+
disposeHydratedMeshesExcept(keep, keepModelIndex) {
|
|
622
|
+
if (this.meshes.length === 0)
|
|
623
|
+
return 0;
|
|
624
|
+
const kept = [];
|
|
625
|
+
let disposed = 0;
|
|
626
|
+
for (const mesh of this.meshes) {
|
|
627
|
+
const keepMesh = keep.has(mesh.expressId)
|
|
628
|
+
&& (keepModelIndex === undefined || mesh.modelIndex === keepModelIndex);
|
|
629
|
+
if (mesh.hydrated && !keepMesh) {
|
|
630
|
+
destroyGpuResources(mesh);
|
|
631
|
+
disposed++;
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
kept.push(mesh);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (disposed > 0)
|
|
638
|
+
this.meshes = kept;
|
|
639
|
+
return disposed;
|
|
575
640
|
}
|
|
576
641
|
/**
|
|
577
642
|
* Bucket BASE key for a mesh: colour key, prefixed with the mesh's grid
|
|
@@ -1718,10 +1783,7 @@ export class Scene {
|
|
|
1718
1783
|
this.residencyRestoreQueue.clear();
|
|
1719
1784
|
this.pendingBatchKeys.clear();
|
|
1720
1785
|
// Destroy cached partial batches — their colorKeys are now stale
|
|
1721
|
-
|
|
1722
|
-
destroyGpuResources(batch);
|
|
1723
|
-
this.partialBatchCache.clear();
|
|
1724
|
-
this.partialBatchCacheKeys.clear();
|
|
1786
|
+
this.dropAllPartialCaches();
|
|
1725
1787
|
// Re-seat the carried cold shells in the fresh bucket map (their GPU
|
|
1726
1788
|
// shells re-enter the flat array via rebuildPendingBatches below).
|
|
1727
1789
|
for (const [key, bucket] of carriedCold)
|
|
@@ -1799,10 +1861,7 @@ export class Scene {
|
|
|
1799
1861
|
this.lastDrawnFrame.clear();
|
|
1800
1862
|
this.residencyRestoreQueue.clear();
|
|
1801
1863
|
this.pendingBatchKeys.clear();
|
|
1802
|
-
|
|
1803
|
-
destroyGpuResources(batch);
|
|
1804
|
-
this.partialBatchCache.clear();
|
|
1805
|
-
this.partialBatchCacheKeys.clear();
|
|
1864
|
+
this.dropAllPartialCaches();
|
|
1806
1865
|
// Re-seat the carried cold shells in the fresh bucket map.
|
|
1807
1866
|
for (const [key, bucket] of carriedCold)
|
|
1808
1867
|
this.buckets.set(key, bucket);
|
|
@@ -1924,10 +1983,7 @@ export class Scene {
|
|
|
1924
1983
|
this.coldBuckets.clear();
|
|
1925
1984
|
this.dirtyBuckets.clear();
|
|
1926
1985
|
this.pendingBatchKeys.clear();
|
|
1927
|
-
|
|
1928
|
-
destroyGpuResources(batch);
|
|
1929
|
-
this.partialBatchCache.clear();
|
|
1930
|
-
this.partialBatchCacheKeys.clear();
|
|
1986
|
+
this.dropAllPartialCaches();
|
|
1931
1987
|
this.geometryReleased = true;
|
|
1932
1988
|
this.ephemeralStreamingMode = false;
|
|
1933
1989
|
}
|
|
@@ -2012,10 +2068,7 @@ export class Scene {
|
|
|
2012
2068
|
this.coldBuckets.clear();
|
|
2013
2069
|
this.dirtyBuckets.clear();
|
|
2014
2070
|
// 3. Clear partial batch cache (would need mesh data to rebuild)
|
|
2015
|
-
|
|
2016
|
-
destroyGpuResources(batch);
|
|
2017
|
-
this.partialBatchCache.clear();
|
|
2018
|
-
this.partialBatchCacheKeys.clear();
|
|
2071
|
+
this.dropAllPartialCaches();
|
|
2019
2072
|
this.geometryReleased = true;
|
|
2020
2073
|
console.log(`[Scene] Released JS geometry data. ${this.boundingBoxes.size} bounding boxes cached. ` +
|
|
2021
2074
|
`${this.batchedMeshes.length} GPU batches retained.`);
|
|
@@ -2321,10 +2374,25 @@ export class Scene {
|
|
|
2321
2374
|
* @param pipeline - Rendering pipeline
|
|
2322
2375
|
* @returns BatchedMesh containing only visible elements, or undefined if no visible elements
|
|
2323
2376
|
*/
|
|
2324
|
-
getOrCreatePartialBatch(sourceBatchKey, colorKey, visibleIds, device, pipeline) {
|
|
2377
|
+
getOrCreatePartialBatch(sourceBatchKey, colorKey, visibleIds, device, pipeline, visibilityEpoch) {
|
|
2325
2378
|
// Cannot create partial batches after geometry data has been released
|
|
2326
2379
|
if (this.geometryReleased)
|
|
2327
2380
|
return undefined;
|
|
2381
|
+
// Fast path (PERF): while the visibility + colour-override epoch is
|
|
2382
|
+
// unchanged, the visible subset for this sourceBatch is provably identical
|
|
2383
|
+
// to what we cached (the source batch is immutable per id and both hide/
|
|
2384
|
+
// isolate and override promotion are folded into the epoch). Return the
|
|
2385
|
+
// cached clone WITHOUT the O(n) sort + FNV hash below. A rebuilt/evicted
|
|
2386
|
+
// source batch gets a new id → new sourceBatchKey → cache miss here.
|
|
2387
|
+
if (visibilityEpoch !== undefined &&
|
|
2388
|
+
this.partialBatchCacheVersions.get(sourceBatchKey) === visibilityEpoch) {
|
|
2389
|
+
const key = this.partialBatchCacheKeys.get(sourceBatchKey);
|
|
2390
|
+
if (key !== undefined) {
|
|
2391
|
+
const cached = this.partialBatchCache.get(key);
|
|
2392
|
+
if (cached)
|
|
2393
|
+
return cached;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2328
2396
|
// Create cache key from colorKey + deterministic hash of all visible IDs
|
|
2329
2397
|
// Using a proper hash over all IDs to avoid collisions when middle IDs differ
|
|
2330
2398
|
const sortedIds = Array.from(visibleIds).sort((a, b) => a - b);
|
|
@@ -2341,8 +2409,13 @@ export class Scene {
|
|
|
2341
2409
|
const currentCacheKey = this.partialBatchCacheKeys.get(sourceBatchKey);
|
|
2342
2410
|
if (currentCacheKey === cacheKey) {
|
|
2343
2411
|
const cached = this.partialBatchCache.get(cacheKey);
|
|
2344
|
-
if (cached)
|
|
2412
|
+
if (cached) {
|
|
2413
|
+
// Record the epoch so subsequent frames take the sort-free fast path.
|
|
2414
|
+
if (visibilityEpoch !== undefined) {
|
|
2415
|
+
this.partialBatchCacheVersions.set(sourceBatchKey, visibilityEpoch);
|
|
2416
|
+
}
|
|
2345
2417
|
return cached;
|
|
2418
|
+
}
|
|
2346
2419
|
}
|
|
2347
2420
|
// Invalidate old cache for this colorKey if visibility changed
|
|
2348
2421
|
if (currentCacheKey && currentCacheKey !== cacheKey) {
|
|
@@ -2381,6 +2454,9 @@ export class Scene {
|
|
|
2381
2454
|
// Cache it
|
|
2382
2455
|
this.partialBatchCache.set(cacheKey, partialBatch);
|
|
2383
2456
|
this.partialBatchCacheKeys.set(sourceBatchKey, cacheKey);
|
|
2457
|
+
if (visibilityEpoch !== undefined) {
|
|
2458
|
+
this.partialBatchCacheVersions.set(sourceBatchKey, visibilityEpoch);
|
|
2459
|
+
}
|
|
2384
2460
|
return partialBatch;
|
|
2385
2461
|
}
|
|
2386
2462
|
// ─── Color overlay system ────────────────────────────────────────────
|
|
@@ -2399,6 +2475,9 @@ export class Scene {
|
|
|
2399
2475
|
setColorOverrides(overrides, device, pipeline) {
|
|
2400
2476
|
// Destroy previous overlay batches
|
|
2401
2477
|
this.destroyOverrideBatches();
|
|
2478
|
+
// The override set is changing — invalidate the partial-batch cache epoch so
|
|
2479
|
+
// the render loop rebuilds any promotion-split sub-batches (see render loop).
|
|
2480
|
+
this.colorOverrideGeneration++;
|
|
2402
2481
|
if (this.geometryReleased) {
|
|
2403
2482
|
console.warn('[Scene] setColorOverrides called after geometry data was released — skipping.');
|
|
2404
2483
|
this.colorOverrides = null;
|
|
@@ -2452,9 +2531,16 @@ export class Scene {
|
|
|
2452
2531
|
*/
|
|
2453
2532
|
clearColorOverrides() {
|
|
2454
2533
|
this.destroyOverrideBatches();
|
|
2534
|
+
this.colorOverrideGeneration++;
|
|
2455
2535
|
this.colorOverrides = null;
|
|
2456
2536
|
this.setInstancedColorOverrides(null);
|
|
2457
2537
|
}
|
|
2538
|
+
/** Monotonic counter that changes whenever the colour-override set changes.
|
|
2539
|
+
* The render loop folds it into the partial sub-batch cache epoch so the
|
|
2540
|
+
* per-frame fast path stays correct across override changes. */
|
|
2541
|
+
getColorOverrideGeneration() {
|
|
2542
|
+
return this.colorOverrideGeneration;
|
|
2543
|
+
}
|
|
2458
2544
|
/** Get overlay batches for rendering */
|
|
2459
2545
|
getOverrideBatches() {
|
|
2460
2546
|
return this.overrideBatches;
|
|
@@ -2891,19 +2977,20 @@ export class Scene {
|
|
|
2891
2977
|
const device = this.instancedDevice;
|
|
2892
2978
|
if (!device || this.instancedTemplates.length === 0)
|
|
2893
2979
|
return;
|
|
2894
|
-
// Called every render frame.
|
|
2895
|
-
//
|
|
2896
|
-
//
|
|
2897
|
-
//
|
|
2980
|
+
// Called every render frame. Change detection is by CONTENT (the tracker
|
|
2981
|
+
// snapshot-compares), matching the RenderOptions.hiddenIds contract: an
|
|
2982
|
+
// in-place mutation of the caller's Set is seen, a fresh identical Set is
|
|
2983
|
+
// not treated as a change, and the O(occurrences) rebuild below still only
|
|
2984
|
+
// runs on a real visibility change (orbit stays cheap). The dirty flag
|
|
2985
|
+
// forces a recompute after a new shard adds occurrences mid-stream, so an
|
|
2898
2986
|
// active isolate/hide also applies to geometry that streams in afterwards.
|
|
2987
|
+
const visibilityVersion = this.instancedVisibilityEpochs.update(hiddenIds, isolatedIds);
|
|
2899
2988
|
if (!this.instancedVisibilityDirty &&
|
|
2900
|
-
|
|
2901
|
-
isolatedIds === this.lastInstancedIsolatedIds) {
|
|
2989
|
+
visibilityVersion === this.lastInstancedVisibilityVersion) {
|
|
2902
2990
|
return;
|
|
2903
2991
|
}
|
|
2904
2992
|
this.instancedVisibilityDirty = false;
|
|
2905
|
-
this.
|
|
2906
|
-
this.lastInstancedIsolatedIds = isolatedIds ?? null;
|
|
2993
|
+
this.lastInstancedVisibilityVersion = visibilityVersion;
|
|
2907
2994
|
const isHidden = (eid) => (hiddenIds != null && hiddenIds.has(eid)) ||
|
|
2908
2995
|
(isolatedIds != null && !isolatedIds.has(eid));
|
|
2909
2996
|
// Recompute the effective hidden set over all instanced occurrences and diff vs
|
|
@@ -3121,13 +3208,13 @@ export class Scene {
|
|
|
3121
3208
|
this.instancedHidden.clear();
|
|
3122
3209
|
this.instancedOverridden.clear();
|
|
3123
3210
|
this.instancedHasTransparent = false;
|
|
3124
|
-
|
|
3125
|
-
this.
|
|
3211
|
+
// Force the next setInstancedVisibility to recompute against fresh state.
|
|
3212
|
+
this.lastInstancedVisibilityVersion = -1;
|
|
3126
3213
|
this.instancedVisibilityDirty = false;
|
|
3127
3214
|
this.instancedDevice = undefined;
|
|
3128
|
-
// Clear partial batch cache
|
|
3129
|
-
|
|
3130
|
-
|
|
3215
|
+
// Clear partial batch cache (destroys buffers + drops all cache maps)
|
|
3216
|
+
this.dropAllPartialCaches();
|
|
3217
|
+
this.colorOverrideGeneration++;
|
|
3131
3218
|
// Destroy streaming fragments (already included in batchedMeshes, but tracked separately)
|
|
3132
3219
|
this.streamingFragments = [];
|
|
3133
3220
|
this.destroyOverrideBatches();
|
|
@@ -3147,8 +3234,6 @@ export class Scene {
|
|
|
3147
3234
|
this.dirtyBuckets.clear();
|
|
3148
3235
|
this.cachedMaxBufferSize = 0;
|
|
3149
3236
|
this.pendingBatchKeys.clear();
|
|
3150
|
-
this.partialBatchCache.clear();
|
|
3151
|
-
this.partialBatchCacheKeys.clear();
|
|
3152
3237
|
this.meshQueue = [];
|
|
3153
3238
|
this.meshQueueReadIndex = 0;
|
|
3154
3239
|
this.geometryReleased = false;
|