@ifc-lite/renderer 1.38.0 → 1.39.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/device.d.ts +10 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +27 -0
- package/dist/device.js.map +1 -1
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +207 -57
- package/dist/index.js.map +1 -1
- package/dist/pointcloud/point-cloud-renderer.d.ts +15 -6
- package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-renderer.js +7 -4
- package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
- package/dist/pointcloud/point-cloud-uniforms.d.ts +14 -2
- package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-uniforms.js +35 -2
- package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
- package/dist/pointcloud/point-pipeline.d.ts +3 -2
- package/dist/pointcloud/point-pipeline.d.ts.map +1 -1
- package/dist/pointcloud/point-pipeline.js +8 -5
- package/dist/pointcloud/point-pipeline.js.map +1 -1
- package/dist/pointcloud/point-shader.wgsl.d.ts +1 -1
- package/dist/pointcloud/point-shader.wgsl.d.ts.map +1 -1
- package/dist/pointcloud/point-shader.wgsl.js +20 -17
- package/dist/pointcloud/point-shader.wgsl.js.map +1 -1
- package/dist/scene.d.ts +47 -3
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +206 -52
- 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 +2 -2
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';
|
|
@@ -29,6 +30,10 @@ export class Scene {
|
|
|
29
30
|
meshDataMap = new Map(); // Map expressId -> MeshData[] (for lazy buffer creation, accumulates multiple pieces)
|
|
30
31
|
boundingBoxes = new Map(); // Map expressId -> bounding box (computed lazily)
|
|
31
32
|
texturedMeshes = []; // #961: IFC surface-textured meshes (own buffers/texture/bindGroup)
|
|
33
|
+
/** #1781: GPU textures shared across meshes, keyed by `MeshTextureRef.textureId`
|
|
34
|
+
* (one `IfcImageTexture` → one upload, sampled by every face set mapping it).
|
|
35
|
+
* Refcounted: entries die when the last referencing mesh is removed / on clear(). */
|
|
36
|
+
sharedTextures = new Map();
|
|
32
37
|
texturedDevice; // #961: cached for textured-mesh re-upload on translate
|
|
33
38
|
instancedTemplates = []; // GPU-instancing: unique templates + per-occurrence buffers (fed by addInstancedShard)
|
|
34
39
|
instancedVisible = true; // GPU-instancing: hidden in Types view mode (instanced geometry is class-0 occurrences)
|
|
@@ -39,8 +44,11 @@ export class Scene {
|
|
|
39
44
|
instancedHidden = new Set(); // currently hidden instanced express_ids (hide/isolate)
|
|
40
45
|
instancedOverridden = new Set(); // currently colour-overridden instanced express_ids
|
|
41
46
|
instancedHasTransparent = false; // an override made some instanced occurrence translucent
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
// Content-based change guard for setInstancedVisibility — same contract as
|
|
48
|
+
// RenderOptions.hiddenIds (in-place mutation and fresh identical Sets both
|
|
49
|
+
// behave), keeping the instanced path in lockstep with the batched path.
|
|
50
|
+
instancedVisibilityEpochs = new VisibilityEpochTracker();
|
|
51
|
+
lastInstancedVisibilityVersion = -1;
|
|
44
52
|
instancedVisibilityDirty = false; // set when a new shard adds occurrences → re-apply visibility
|
|
45
53
|
// Buffer-size-aware bucket splitting: when a single color group's geometry
|
|
46
54
|
// would exceed the GPU maxBufferSize, overflow is directed to a new
|
|
@@ -96,6 +104,11 @@ export class Scene {
|
|
|
96
104
|
// This allows rendering partially visible batches as single draw calls instead of 10,000+ individual draws
|
|
97
105
|
partialBatchCache = new Map();
|
|
98
106
|
partialBatchCacheKeys = new Map(); // sourceBatchKey -> current cache key (for invalidation)
|
|
107
|
+
// sourceBatchKey -> visibility/override epoch its cached partial batch was
|
|
108
|
+
// built for. Lets getOrCreatePartialBatch return the cached clone WITHOUT
|
|
109
|
+
// re-sorting + re-hashing every visible id each frame while the epoch holds
|
|
110
|
+
// (issue: O(elements) per-frame work under hide/isolate). See render loop.
|
|
111
|
+
partialBatchCacheVersions = new Map();
|
|
99
112
|
// Color overlay system for lens coloring — NEVER modifies original batches.
|
|
100
113
|
// Overlay batches render on top using depthCompare 'equal', so they only
|
|
101
114
|
// paint where original geometry already wrote depth. Clearing is instant.
|
|
@@ -103,6 +116,11 @@ export class Scene {
|
|
|
103
116
|
// Defensively-typed: the renderer is the sole writer (via setColorOverrides),
|
|
104
117
|
// external readers go through getColorOverrides() and get a ReadonlyMap.
|
|
105
118
|
colorOverrides = null;
|
|
119
|
+
// Bumped whenever the colour-override set changes. The partial sub-batch's
|
|
120
|
+
// visible subset depends on override promotion (splitVisibleIdsByPromotion),
|
|
121
|
+
// so the render loop folds this into the partial-batch cache epoch to keep the
|
|
122
|
+
// per-frame fast path correct when overrides change with no visibility change.
|
|
123
|
+
colorOverrideGeneration = 0;
|
|
106
124
|
// Streaming optimization: track pending batch rebuilds
|
|
107
125
|
pendingBatchKeys = new Set();
|
|
108
126
|
// Temporary fragment batches created during streaming for immediate rendering.
|
|
@@ -571,7 +589,58 @@ export class Scene {
|
|
|
571
589
|
this.partialBatchCache.delete(cacheKey);
|
|
572
590
|
}
|
|
573
591
|
this.partialBatchCacheKeys.delete(sourceBatchKey);
|
|
592
|
+
this.partialBatchCacheVersions.delete(sourceBatchKey);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/** Destroy + drop EVERY cached partial sub-batch. The clones built during
|
|
596
|
+
* hide/isolate are deliberately excluded from the GPU residency budget and
|
|
597
|
+
* are otherwise only freed on clear()/finalize/evict — never when filtering
|
|
598
|
+
* ends. The render loop calls this on the transition back to fully-visible so
|
|
599
|
+
* the ~model-sized clone VRAM is not pinned until the next model reload. Uses
|
|
600
|
+
* the same destroy-then-clear idiom as clear(); safe to call between frames
|
|
601
|
+
* because the previous frame is already submitted (WebGPU defers the free
|
|
602
|
+
* past in-flight work). */
|
|
603
|
+
dropAllPartialCaches() {
|
|
604
|
+
if (this.partialBatchCache.size === 0
|
|
605
|
+
&& this.partialBatchCacheKeys.size === 0
|
|
606
|
+
&& this.partialBatchCacheVersions.size === 0) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
for (const batch of this.partialBatchCache.values())
|
|
610
|
+
destroyGpuResources(batch);
|
|
611
|
+
this.partialBatchCache.clear();
|
|
612
|
+
this.partialBatchCacheKeys.clear();
|
|
613
|
+
this.partialBatchCacheVersions.clear();
|
|
614
|
+
}
|
|
615
|
+
/** Free the hydrated (pick / selection-highlight) individual meshes that are
|
|
616
|
+
* no longer selected, destroying their GPU buffers and dropping them from
|
|
617
|
+
* `this.meshes`. A mesh is kept iff its expressId is in `keep` AND it
|
|
618
|
+
* matches `keepModelIndex` (undefined = any model) — the same predicate the
|
|
619
|
+
* render loop uses to draw selection highlights, so disposal is its exact
|
|
620
|
+
* complement. The model scoping matters for federation: models can share
|
|
621
|
+
* express ids, and an id-only check would strand the OTHER model's hydrated
|
|
622
|
+
* mesh resident and drawing when selection moves across models. Only meshes
|
|
623
|
+
* flagged `hydrated` are touched — authored geometry added via addMesh()
|
|
624
|
+
* and batch geometry are left untouched. Returns how many were freed. */
|
|
625
|
+
disposeHydratedMeshesExcept(keep, keepModelIndex) {
|
|
626
|
+
if (this.meshes.length === 0)
|
|
627
|
+
return 0;
|
|
628
|
+
const kept = [];
|
|
629
|
+
let disposed = 0;
|
|
630
|
+
for (const mesh of this.meshes) {
|
|
631
|
+
const keepMesh = keep.has(mesh.expressId)
|
|
632
|
+
&& (keepModelIndex === undefined || mesh.modelIndex === keepModelIndex);
|
|
633
|
+
if (mesh.hydrated && !keepMesh) {
|
|
634
|
+
destroyGpuResources(mesh);
|
|
635
|
+
disposed++;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
kept.push(mesh);
|
|
639
|
+
}
|
|
574
640
|
}
|
|
641
|
+
if (disposed > 0)
|
|
642
|
+
this.meshes = kept;
|
|
643
|
+
return disposed;
|
|
575
644
|
}
|
|
576
645
|
/**
|
|
577
646
|
* Bucket BASE key for a mesh: colour key, prefixed with the mesh's grid
|
|
@@ -899,10 +968,10 @@ export class Scene {
|
|
|
899
968
|
// otherwise a flat-colour copy would be drawn over the texture. Still
|
|
900
969
|
// register them in meshDataMap (addMeshData) so CPU picking/bbox/frame work.
|
|
901
970
|
let renderable = meshDataArray;
|
|
902
|
-
if (meshDataArray.some((m) =>
|
|
971
|
+
if (meshDataArray.some((m) => Scene.hasRenderableTexture(m))) {
|
|
903
972
|
renderable = [];
|
|
904
973
|
for (const meshData of meshDataArray) {
|
|
905
|
-
if (
|
|
974
|
+
if (Scene.hasRenderableTexture(meshData)) {
|
|
906
975
|
this.createTexturedMesh(meshData, device, pipeline);
|
|
907
976
|
this.addMeshData(meshData);
|
|
908
977
|
}
|
|
@@ -1070,7 +1139,7 @@ export class Scene {
|
|
|
1070
1139
|
tm.vertexBuffer.destroy();
|
|
1071
1140
|
tm.indexBuffer.destroy();
|
|
1072
1141
|
tm.uniformBuffer.destroy();
|
|
1073
|
-
|
|
1142
|
+
this.releaseTexturedMeshTexture(tm);
|
|
1074
1143
|
this.texturedMeshes.splice(i, 1);
|
|
1075
1144
|
removedDedicated = true;
|
|
1076
1145
|
}
|
|
@@ -1256,7 +1325,7 @@ export class Scene {
|
|
|
1256
1325
|
// re-interleave + re-upload the moved textured parts (paired by expressId,
|
|
1257
1326
|
// in creation order). Without this a moved textured entity renders stale.
|
|
1258
1327
|
if (this.texturedDevice && this.texturedMeshes.length > 0) {
|
|
1259
|
-
const texturedData = meshDataList.filter((md) =>
|
|
1328
|
+
const texturedData = meshDataList.filter((md) => Scene.hasRenderableTexture(md));
|
|
1260
1329
|
if (texturedData.length > 0) {
|
|
1261
1330
|
const entries = this.texturedMeshes.filter((tm) => tm.expressId === expressId);
|
|
1262
1331
|
for (let i = 0; i < entries.length && i < texturedData.length; i++) {
|
|
@@ -1454,7 +1523,7 @@ export class Scene {
|
|
|
1454
1523
|
// #961: textured meshes render from their own GPU vertex buffer — re-upload
|
|
1455
1524
|
// the rotated parts so they don't render stale (mirrors the translate path).
|
|
1456
1525
|
if (this.texturedDevice && this.texturedMeshes.length > 0) {
|
|
1457
|
-
const texturedData = meshDataList.filter((md) =>
|
|
1526
|
+
const texturedData = meshDataList.filter((md) => Scene.hasRenderableTexture(md));
|
|
1458
1527
|
if (texturedData.length > 0) {
|
|
1459
1528
|
const entries = this.texturedMeshes.filter((tm) => tm.expressId === expressId);
|
|
1460
1529
|
for (let i = 0; i < entries.length && i < texturedData.length; i++) {
|
|
@@ -1718,10 +1787,7 @@ export class Scene {
|
|
|
1718
1787
|
this.residencyRestoreQueue.clear();
|
|
1719
1788
|
this.pendingBatchKeys.clear();
|
|
1720
1789
|
// Destroy cached partial batches — their colorKeys are now stale
|
|
1721
|
-
|
|
1722
|
-
destroyGpuResources(batch);
|
|
1723
|
-
this.partialBatchCache.clear();
|
|
1724
|
-
this.partialBatchCacheKeys.clear();
|
|
1790
|
+
this.dropAllPartialCaches();
|
|
1725
1791
|
// Re-seat the carried cold shells in the fresh bucket map (their GPU
|
|
1726
1792
|
// shells re-enter the flat array via rebuildPendingBatches below).
|
|
1727
1793
|
for (const [key, bucket] of carriedCold)
|
|
@@ -1799,10 +1865,7 @@ export class Scene {
|
|
|
1799
1865
|
this.lastDrawnFrame.clear();
|
|
1800
1866
|
this.residencyRestoreQueue.clear();
|
|
1801
1867
|
this.pendingBatchKeys.clear();
|
|
1802
|
-
|
|
1803
|
-
destroyGpuResources(batch);
|
|
1804
|
-
this.partialBatchCache.clear();
|
|
1805
|
-
this.partialBatchCacheKeys.clear();
|
|
1868
|
+
this.dropAllPartialCaches();
|
|
1806
1869
|
// Re-seat the carried cold shells in the fresh bucket map.
|
|
1807
1870
|
for (const [key, bucket] of carriedCold)
|
|
1808
1871
|
this.buckets.set(key, bucket);
|
|
@@ -1924,10 +1987,7 @@ export class Scene {
|
|
|
1924
1987
|
this.coldBuckets.clear();
|
|
1925
1988
|
this.dirtyBuckets.clear();
|
|
1926
1989
|
this.pendingBatchKeys.clear();
|
|
1927
|
-
|
|
1928
|
-
destroyGpuResources(batch);
|
|
1929
|
-
this.partialBatchCache.clear();
|
|
1930
|
-
this.partialBatchCacheKeys.clear();
|
|
1990
|
+
this.dropAllPartialCaches();
|
|
1931
1991
|
this.geometryReleased = true;
|
|
1932
1992
|
this.ephemeralStreamingMode = false;
|
|
1933
1993
|
}
|
|
@@ -2012,10 +2072,7 @@ export class Scene {
|
|
|
2012
2072
|
this.coldBuckets.clear();
|
|
2013
2073
|
this.dirtyBuckets.clear();
|
|
2014
2074
|
// 3. Clear partial batch cache (would need mesh data to rebuild)
|
|
2015
|
-
|
|
2016
|
-
destroyGpuResources(batch);
|
|
2017
|
-
this.partialBatchCache.clear();
|
|
2018
|
-
this.partialBatchCacheKeys.clear();
|
|
2075
|
+
this.dropAllPartialCaches();
|
|
2019
2076
|
this.geometryReleased = true;
|
|
2020
2077
|
console.log(`[Scene] Released JS geometry data. ${this.boundingBoxes.size} bounding boxes cached. ` +
|
|
2021
2078
|
`${this.batchedMeshes.length} GPU batches retained.`);
|
|
@@ -2321,10 +2378,25 @@ export class Scene {
|
|
|
2321
2378
|
* @param pipeline - Rendering pipeline
|
|
2322
2379
|
* @returns BatchedMesh containing only visible elements, or undefined if no visible elements
|
|
2323
2380
|
*/
|
|
2324
|
-
getOrCreatePartialBatch(sourceBatchKey, colorKey, visibleIds, device, pipeline) {
|
|
2381
|
+
getOrCreatePartialBatch(sourceBatchKey, colorKey, visibleIds, device, pipeline, visibilityEpoch) {
|
|
2325
2382
|
// Cannot create partial batches after geometry data has been released
|
|
2326
2383
|
if (this.geometryReleased)
|
|
2327
2384
|
return undefined;
|
|
2385
|
+
// Fast path (PERF): while the visibility + colour-override epoch is
|
|
2386
|
+
// unchanged, the visible subset for this sourceBatch is provably identical
|
|
2387
|
+
// to what we cached (the source batch is immutable per id and both hide/
|
|
2388
|
+
// isolate and override promotion are folded into the epoch). Return the
|
|
2389
|
+
// cached clone WITHOUT the O(n) sort + FNV hash below. A rebuilt/evicted
|
|
2390
|
+
// source batch gets a new id → new sourceBatchKey → cache miss here.
|
|
2391
|
+
if (visibilityEpoch !== undefined &&
|
|
2392
|
+
this.partialBatchCacheVersions.get(sourceBatchKey) === visibilityEpoch) {
|
|
2393
|
+
const key = this.partialBatchCacheKeys.get(sourceBatchKey);
|
|
2394
|
+
if (key !== undefined) {
|
|
2395
|
+
const cached = this.partialBatchCache.get(key);
|
|
2396
|
+
if (cached)
|
|
2397
|
+
return cached;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2328
2400
|
// Create cache key from colorKey + deterministic hash of all visible IDs
|
|
2329
2401
|
// Using a proper hash over all IDs to avoid collisions when middle IDs differ
|
|
2330
2402
|
const sortedIds = Array.from(visibleIds).sort((a, b) => a - b);
|
|
@@ -2341,8 +2413,13 @@ export class Scene {
|
|
|
2341
2413
|
const currentCacheKey = this.partialBatchCacheKeys.get(sourceBatchKey);
|
|
2342
2414
|
if (currentCacheKey === cacheKey) {
|
|
2343
2415
|
const cached = this.partialBatchCache.get(cacheKey);
|
|
2344
|
-
if (cached)
|
|
2416
|
+
if (cached) {
|
|
2417
|
+
// Record the epoch so subsequent frames take the sort-free fast path.
|
|
2418
|
+
if (visibilityEpoch !== undefined) {
|
|
2419
|
+
this.partialBatchCacheVersions.set(sourceBatchKey, visibilityEpoch);
|
|
2420
|
+
}
|
|
2345
2421
|
return cached;
|
|
2422
|
+
}
|
|
2346
2423
|
}
|
|
2347
2424
|
// Invalidate old cache for this colorKey if visibility changed
|
|
2348
2425
|
if (currentCacheKey && currentCacheKey !== cacheKey) {
|
|
@@ -2381,6 +2458,9 @@ export class Scene {
|
|
|
2381
2458
|
// Cache it
|
|
2382
2459
|
this.partialBatchCache.set(cacheKey, partialBatch);
|
|
2383
2460
|
this.partialBatchCacheKeys.set(sourceBatchKey, cacheKey);
|
|
2461
|
+
if (visibilityEpoch !== undefined) {
|
|
2462
|
+
this.partialBatchCacheVersions.set(sourceBatchKey, visibilityEpoch);
|
|
2463
|
+
}
|
|
2384
2464
|
return partialBatch;
|
|
2385
2465
|
}
|
|
2386
2466
|
// ─── Color overlay system ────────────────────────────────────────────
|
|
@@ -2399,6 +2479,9 @@ export class Scene {
|
|
|
2399
2479
|
setColorOverrides(overrides, device, pipeline) {
|
|
2400
2480
|
// Destroy previous overlay batches
|
|
2401
2481
|
this.destroyOverrideBatches();
|
|
2482
|
+
// The override set is changing — invalidate the partial-batch cache epoch so
|
|
2483
|
+
// the render loop rebuilds any promotion-split sub-batches (see render loop).
|
|
2484
|
+
this.colorOverrideGeneration++;
|
|
2402
2485
|
if (this.geometryReleased) {
|
|
2403
2486
|
console.warn('[Scene] setColorOverrides called after geometry data was released — skipping.');
|
|
2404
2487
|
this.colorOverrides = null;
|
|
@@ -2452,9 +2535,16 @@ export class Scene {
|
|
|
2452
2535
|
*/
|
|
2453
2536
|
clearColorOverrides() {
|
|
2454
2537
|
this.destroyOverrideBatches();
|
|
2538
|
+
this.colorOverrideGeneration++;
|
|
2455
2539
|
this.colorOverrides = null;
|
|
2456
2540
|
this.setInstancedColorOverrides(null);
|
|
2457
2541
|
}
|
|
2542
|
+
/** Monotonic counter that changes whenever the colour-override set changes.
|
|
2543
|
+
* The render loop folds it into the partial sub-batch cache epoch so the
|
|
2544
|
+
* per-frame fast path stays correct across override changes. */
|
|
2545
|
+
getColorOverrideGeneration() {
|
|
2546
|
+
return this.colorOverrideGeneration;
|
|
2547
|
+
}
|
|
2458
2548
|
/** Get overlay batches for rendering */
|
|
2459
2549
|
getOverrideBatches() {
|
|
2460
2550
|
return this.overrideBatches;
|
|
@@ -2891,19 +2981,20 @@ export class Scene {
|
|
|
2891
2981
|
const device = this.instancedDevice;
|
|
2892
2982
|
if (!device || this.instancedTemplates.length === 0)
|
|
2893
2983
|
return;
|
|
2894
|
-
// Called every render frame.
|
|
2895
|
-
//
|
|
2896
|
-
//
|
|
2897
|
-
//
|
|
2984
|
+
// Called every render frame. Change detection is by CONTENT (the tracker
|
|
2985
|
+
// snapshot-compares), matching the RenderOptions.hiddenIds contract: an
|
|
2986
|
+
// in-place mutation of the caller's Set is seen, a fresh identical Set is
|
|
2987
|
+
// not treated as a change, and the O(occurrences) rebuild below still only
|
|
2988
|
+
// runs on a real visibility change (orbit stays cheap). The dirty flag
|
|
2989
|
+
// forces a recompute after a new shard adds occurrences mid-stream, so an
|
|
2898
2990
|
// active isolate/hide also applies to geometry that streams in afterwards.
|
|
2991
|
+
const visibilityVersion = this.instancedVisibilityEpochs.update(hiddenIds, isolatedIds);
|
|
2899
2992
|
if (!this.instancedVisibilityDirty &&
|
|
2900
|
-
|
|
2901
|
-
isolatedIds === this.lastInstancedIsolatedIds) {
|
|
2993
|
+
visibilityVersion === this.lastInstancedVisibilityVersion) {
|
|
2902
2994
|
return;
|
|
2903
2995
|
}
|
|
2904
2996
|
this.instancedVisibilityDirty = false;
|
|
2905
|
-
this.
|
|
2906
|
-
this.lastInstancedIsolatedIds = isolatedIds ?? null;
|
|
2997
|
+
this.lastInstancedVisibilityVersion = visibilityVersion;
|
|
2907
2998
|
const isHidden = (eid) => (hiddenIds != null && hiddenIds.has(eid)) ||
|
|
2908
2999
|
(isolatedIds != null && !isolatedIds.has(eid));
|
|
2909
3000
|
// Recompute the effective hidden set over all instanced occurrences and diff vs
|
|
@@ -3011,6 +3102,15 @@ export class Scene {
|
|
|
3011
3102
|
* The per-frame uniform (viewProj/section/flags + colour tint) is written by
|
|
3012
3103
|
* the renderer each frame, mirroring how colour batches are driven.
|
|
3013
3104
|
*/
|
|
3105
|
+
/** True when the mesh can render through the textured pipeline: UVs plus
|
|
3106
|
+
* either a Rust-decoded image (#961) or a viewer-resolved ImageBitmap for
|
|
3107
|
+
* an external `IfcImageTexture` reference (#1781). A `textureRef` whose
|
|
3108
|
+
* image was NOT resolved (missing zip sibling) renders as ordinary
|
|
3109
|
+
* flat-colour geometry instead. */
|
|
3110
|
+
static hasRenderableTexture(meshData) {
|
|
3111
|
+
return Boolean(meshData.uvs) &&
|
|
3112
|
+
Boolean(meshData.texture || (meshData.textureRef && meshData.textureBitmap));
|
|
3113
|
+
}
|
|
3014
3114
|
/**
|
|
3015
3115
|
* Interleave a textured mesh's vertices into the stride-36 layout
|
|
3016
3116
|
* `[px,py,pz, nx,ny,nz, entityId(u32), u,v]`. Shared by initial upload and
|
|
@@ -3019,7 +3119,7 @@ export class Scene {
|
|
|
3019
3119
|
*/
|
|
3020
3120
|
interleaveTexturedVertices(meshData) {
|
|
3021
3121
|
const uvs = meshData.uvs;
|
|
3022
|
-
if (!meshData
|
|
3122
|
+
if (!Scene.hasRenderableTexture(meshData) || !uvs)
|
|
3023
3123
|
return null;
|
|
3024
3124
|
const positions = meshData.positions;
|
|
3025
3125
|
const normals = meshData.normals;
|
|
@@ -3050,8 +3150,10 @@ export class Scene {
|
|
|
3050
3150
|
}
|
|
3051
3151
|
createTexturedMesh(meshData, device, pipeline) {
|
|
3052
3152
|
const tex = meshData.texture;
|
|
3153
|
+
const ref = meshData.textureRef;
|
|
3154
|
+
const bitmap = meshData.textureBitmap;
|
|
3053
3155
|
const interleaved = this.interleaveTexturedVertices(meshData);
|
|
3054
|
-
if (!tex ||
|
|
3156
|
+
if (!interleaved || !(tex || (ref && bitmap)))
|
|
3055
3157
|
return;
|
|
3056
3158
|
this.texturedDevice = device; // reused by translateMeshesForEntity re-upload
|
|
3057
3159
|
const vertexBuffer = device.createBuffer({
|
|
@@ -3064,17 +3166,48 @@ export class Scene {
|
|
|
3064
3166
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
3065
3167
|
});
|
|
3066
3168
|
device.queue.writeBuffer(indexBuffer, 0, meshData.indices);
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3169
|
+
let texture;
|
|
3170
|
+
let sharedTextureKey;
|
|
3171
|
+
if (tex) {
|
|
3172
|
+
// #961: upload the Rust-decoded RGBA8 verbatim — no image decoding in JS.
|
|
3173
|
+
texture = device.createTexture({
|
|
3174
|
+
size: { width: tex.width, height: tex.height },
|
|
3175
|
+
format: 'rgba8unorm',
|
|
3176
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
3177
|
+
});
|
|
3178
|
+
device.queue.writeTexture({ texture }, tex.rgba, { bytesPerRow: tex.width * 4, rowsPerImage: tex.height }, { width: tex.width, height: tex.height });
|
|
3179
|
+
}
|
|
3180
|
+
else {
|
|
3181
|
+
// #1781: external image texture — the viewer decoded the `.ifcZIP`
|
|
3182
|
+
// sibling to an ImageBitmap once per textureId; upload it ONCE and share
|
|
3183
|
+
// the GPU texture across every mesh sampling it (real files map one
|
|
3184
|
+
// 4096² image from dozens of face sets — per-mesh copies would be GBs).
|
|
3185
|
+
const refKey = ref.textureId;
|
|
3186
|
+
const bmp = bitmap;
|
|
3187
|
+
let entry = this.sharedTextures.get(refKey);
|
|
3188
|
+
if (!entry) {
|
|
3189
|
+
const gpuTex = device.createTexture({
|
|
3190
|
+
size: { width: bmp.width, height: bmp.height },
|
|
3191
|
+
format: 'rgba8unorm',
|
|
3192
|
+
// RENDER_ATTACHMENT is required by copyExternalImageToTexture.
|
|
3193
|
+
usage: GPUTextureUsage.TEXTURE_BINDING |
|
|
3194
|
+
GPUTextureUsage.COPY_DST |
|
|
3195
|
+
GPUTextureUsage.RENDER_ATTACHMENT,
|
|
3196
|
+
});
|
|
3197
|
+
device.queue.copyExternalImageToTexture({ source: bmp }, { texture: gpuTex }, { width: bmp.width, height: bmp.height });
|
|
3198
|
+
entry = { texture: gpuTex, refs: 0 };
|
|
3199
|
+
this.sharedTextures.set(refKey, entry);
|
|
3200
|
+
}
|
|
3201
|
+
entry.refs++;
|
|
3202
|
+
texture = entry.texture;
|
|
3203
|
+
sharedTextureKey = refKey;
|
|
3204
|
+
}
|
|
3205
|
+
const repeatS = tex ? tex.repeatS : ref.repeatS;
|
|
3206
|
+
const repeatT = tex ? tex.repeatT : ref.repeatT;
|
|
3074
3207
|
const wrap = (repeat) => (repeat ? 'repeat' : 'clamp-to-edge');
|
|
3075
3208
|
const sampler = device.createSampler({
|
|
3076
|
-
addressModeU: wrap(
|
|
3077
|
-
addressModeV: wrap(
|
|
3209
|
+
addressModeU: wrap(repeatS),
|
|
3210
|
+
addressModeV: wrap(repeatT),
|
|
3078
3211
|
magFilter: 'linear',
|
|
3079
3212
|
minFilter: 'linear',
|
|
3080
3213
|
mipmapFilter: 'linear',
|
|
@@ -3094,8 +3227,26 @@ export class Scene {
|
|
|
3094
3227
|
sampler,
|
|
3095
3228
|
bindGroup,
|
|
3096
3229
|
color: meshData.color,
|
|
3230
|
+
...(sharedTextureKey !== undefined ? { sharedTextureKey } : {}),
|
|
3097
3231
|
});
|
|
3098
3232
|
}
|
|
3233
|
+
/** Release a textured mesh's GPU texture: shared (#1781) entries decrement
|
|
3234
|
+
* the registry refcount and die with their LAST reference; per-mesh (#961)
|
|
3235
|
+
* uploads are destroyed outright. */
|
|
3236
|
+
releaseTexturedMeshTexture(tm) {
|
|
3237
|
+
if (tm.sharedTextureKey === undefined) {
|
|
3238
|
+
tm.texture.destroy();
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
const entry = this.sharedTextures.get(tm.sharedTextureKey);
|
|
3242
|
+
if (!entry)
|
|
3243
|
+
return;
|
|
3244
|
+
entry.refs--;
|
|
3245
|
+
if (entry.refs <= 0) {
|
|
3246
|
+
entry.texture.destroy();
|
|
3247
|
+
this.sharedTextures.delete(tm.sharedTextureKey);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3099
3250
|
clear() {
|
|
3100
3251
|
for (const mesh of this.meshes)
|
|
3101
3252
|
destroyGpuResources(mesh);
|
|
@@ -3105,9 +3256,14 @@ export class Scene {
|
|
|
3105
3256
|
tm.vertexBuffer.destroy();
|
|
3106
3257
|
tm.indexBuffer.destroy();
|
|
3107
3258
|
tm.uniformBuffer.destroy();
|
|
3108
|
-
|
|
3259
|
+
this.releaseTexturedMeshTexture(tm);
|
|
3109
3260
|
}
|
|
3110
3261
|
this.texturedMeshes = [];
|
|
3262
|
+
// Belt-and-braces: refcounting above should have emptied the registry;
|
|
3263
|
+
// destroy any straggler so clear() can never leak a shared GPU texture.
|
|
3264
|
+
for (const entry of this.sharedTextures.values())
|
|
3265
|
+
entry.texture.destroy();
|
|
3266
|
+
this.sharedTextures.clear();
|
|
3111
3267
|
// GPU-instancing templates own their vertex/index/instance buffers.
|
|
3112
3268
|
for (const it of this.instancedTemplates) {
|
|
3113
3269
|
it.vertexBuffer.destroy();
|
|
@@ -3121,13 +3277,13 @@ export class Scene {
|
|
|
3121
3277
|
this.instancedHidden.clear();
|
|
3122
3278
|
this.instancedOverridden.clear();
|
|
3123
3279
|
this.instancedHasTransparent = false;
|
|
3124
|
-
|
|
3125
|
-
this.
|
|
3280
|
+
// Force the next setInstancedVisibility to recompute against fresh state.
|
|
3281
|
+
this.lastInstancedVisibilityVersion = -1;
|
|
3126
3282
|
this.instancedVisibilityDirty = false;
|
|
3127
3283
|
this.instancedDevice = undefined;
|
|
3128
|
-
// Clear partial batch cache
|
|
3129
|
-
|
|
3130
|
-
|
|
3284
|
+
// Clear partial batch cache (destroys buffers + drops all cache maps)
|
|
3285
|
+
this.dropAllPartialCaches();
|
|
3286
|
+
this.colorOverrideGeneration++;
|
|
3131
3287
|
// Destroy streaming fragments (already included in batchedMeshes, but tracked separately)
|
|
3132
3288
|
this.streamingFragments = [];
|
|
3133
3289
|
this.destroyOverrideBatches();
|
|
@@ -3147,8 +3303,6 @@ export class Scene {
|
|
|
3147
3303
|
this.dirtyBuckets.clear();
|
|
3148
3304
|
this.cachedMaxBufferSize = 0;
|
|
3149
3305
|
this.pendingBatchKeys.clear();
|
|
3150
|
-
this.partialBatchCache.clear();
|
|
3151
|
-
this.partialBatchCacheKeys.clear();
|
|
3152
3306
|
this.meshQueue = [];
|
|
3153
3307
|
this.meshQueueReadIndex = 0;
|
|
3154
3308
|
this.geometryReleased = false;
|