@ifc-lite/renderer 1.35.2 → 1.37.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/chunk-grid.d.ts +60 -0
- package/dist/chunk-grid.d.ts.map +1 -0
- package/dist/chunk-grid.js +47 -0
- package/dist/chunk-grid.js.map +1 -0
- package/dist/contribution-cull.d.ts +94 -0
- package/dist/contribution-cull.d.ts.map +1 -0
- package/dist/contribution-cull.js +111 -0
- package/dist/contribution-cull.js.map +1 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +24 -1
- package/dist/device.js.map +1 -1
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +233 -14
- package/dist/index.js.map +1 -1
- package/dist/instanced-render.d.ts +31 -0
- package/dist/instanced-render.d.ts.map +1 -1
- package/dist/instanced-render.js +45 -0
- package/dist/instanced-render.js.map +1 -1
- package/dist/lod-simplify.d.ts +36 -0
- package/dist/lod-simplify.d.ts.map +1 -0
- package/dist/lod-simplify.js +101 -0
- package/dist/lod-simplify.js.map +1 -0
- package/dist/pipeline.d.ts +13 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +63 -2
- package/dist/pipeline.js.map +1 -1
- package/dist/quantize.d.ts +49 -0
- package/dist/quantize.d.ts.map +1 -0
- package/dist/quantize.js +137 -0
- package/dist/quantize.js.map +1 -0
- package/dist/render-stats.d.ts +95 -0
- package/dist/render-stats.d.ts.map +1 -0
- package/dist/render-stats.js +31 -0
- package/dist/render-stats.js.map +1 -0
- package/dist/residency.d.ts +52 -0
- package/dist/residency.d.ts.map +1 -0
- package/dist/residency.js +28 -0
- package/dist/residency.js.map +1 -0
- package/dist/scene.d.ts +190 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +799 -42
- package/dist/scene.js.map +1 -1
- package/dist/shaders/main.wgsl.d.ts +1 -1
- package/dist/shaders/main.wgsl.d.ts.map +1 -1
- package/dist/shaders/main.wgsl.js +53 -0
- package/dist/shaders/main.wgsl.js.map +1 -1
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -32,6 +32,12 @@ export * from './types.js';
|
|
|
32
32
|
export { resolveEnvironment, deriveSkyGradient, packEnvironmentUniforms, ENVIRONMENT_UNIFORM_SIZE, } from './environment.js';
|
|
33
33
|
// Extracted manager classes
|
|
34
34
|
export { PickingManager } from './picking-manager.js';
|
|
35
|
+
export { resolveContributionThresholdPx, projectedAabbRadiusPx } from './contribution-cull.js';
|
|
36
|
+
export { chunkCellKey, bucketBaseKeyFor, DEFAULT_CHUNK_CELL_SIZE } from './chunk-grid.js';
|
|
37
|
+
export { selectEvictions, MIN_EVICTION_AGE_FRAMES } from './residency.js';
|
|
38
|
+
export { simplifyIndicesByClustering, lodCellSizeForBounds, LOD_MIN_TRIANGLES, LOD_CELL_FRACTION } from './lod-simplify.js';
|
|
39
|
+
export { quantizeInterleaved, octEncode, octDecode, QUANT_STEP, MAX_QUANT_EXTENT, QUANT_BYTES_PER_VERTEX } from './quantize.js';
|
|
40
|
+
export { sumResidentGpuBytes } from './render-stats.js';
|
|
35
41
|
export { RaycastEngine } from './raycast-engine.js';
|
|
36
42
|
export { PointPicker, decodePickSample } from './point-picker.js';
|
|
37
43
|
// Point cloud rendering (Phase 0: IFCx inline; Phase 1+: streaming LAS/LAZ)
|
|
@@ -53,6 +59,7 @@ import { PickingManager } from './picking-manager.js';
|
|
|
53
59
|
import { RaycastEngine } from './raycast-engine.js';
|
|
54
60
|
import { PostProcessor } from './post-processor.js';
|
|
55
61
|
import { InteractionEffectsGovernor } from './interaction-effects-governor.js';
|
|
62
|
+
import { resolveContributionThresholdPx, projectedAabbRadiusPx, projectedInstancedRadiusPx } from './contribution-cull.js';
|
|
56
63
|
import { EdlPass } from './edl-pass.js';
|
|
57
64
|
import { SkyPass } from './sky-pass.js';
|
|
58
65
|
import { skyShaderSource } from './shaders/sky.wgsl.js';
|
|
@@ -144,6 +151,8 @@ export class Renderer {
|
|
|
144
151
|
// Diagnostic counters for mobile debugging
|
|
145
152
|
_renderCallCount = 0;
|
|
146
153
|
_renderSkipCount = 0;
|
|
154
|
+
/** Snapshot of the last completed render() — see getFrameStats(). */
|
|
155
|
+
_lastFrameStats = null;
|
|
147
156
|
_renderErrorCount = 0;
|
|
148
157
|
_lastRenderError = '';
|
|
149
158
|
// Dirty flag: set by requestRender(), consumed by the animation loop.
|
|
@@ -155,7 +164,9 @@ export class Renderer {
|
|
|
155
164
|
// Pooled per-frame buffers to avoid GC pressure from per-batch Float32Array allocations
|
|
156
165
|
// A single 224-byte uniform buffer (56 floats) is reused for all batches/meshes within a frame
|
|
157
166
|
// (48 floats viewProj…flags + 8 floats clipBoxMin/clipBoxMax)
|
|
158
|
-
|
|
167
|
+
// 60 floats = the WGSL Uniforms struct incl. quantParams (see
|
|
168
|
+
// pipeline.getUniformBufferSize).
|
|
169
|
+
uniformScratch = new Float32Array(60);
|
|
159
170
|
uniformScratchU32 = new Uint32Array(this.uniformScratch.buffer, 176, 4);
|
|
160
171
|
// What the last render() actually clipped, so the GPU picker can mirror it and
|
|
161
172
|
// section/crop-clipped geometry stays unpickable, not just invisible. Updated
|
|
@@ -739,13 +750,24 @@ export class Renderer {
|
|
|
739
750
|
const fr = Math.fround;
|
|
740
751
|
const sox = so ? fr(so[0]) : null, soy = so ? fr(so[1]) : 0, soz = so ? fr(so[2]) : 0;
|
|
741
752
|
const dx = so ? (ox - so[0]) : ox, dy = so ? (oy - so[1]) : oy, dz = so ? (oz - so[2]) : oz;
|
|
753
|
+
// Quantized batches (issue #1682 phase 6) render lattice-snapped
|
|
754
|
+
// positions: the shader's quantMin + q*step is exactly the lattice
|
|
755
|
+
// node nearest the batch's stored f32 rel coordinate. Reproduce it by
|
|
756
|
+
// snapping the SAME rel coordinate here (round(s*1024)/1024 in f64
|
|
757
|
+
// yields the identical exact-f32 lattice value — see quantize.ts), so
|
|
758
|
+
// the highlight/picker mesh stays BIT-coincident with its quantized
|
|
759
|
+
// source surface, exactly as the two-step fold above achieves for the
|
|
760
|
+
// f32 path. Meshes whose batch fell back to f32 must not snap.
|
|
761
|
+
const snap = this.scene.isMeshQuantized(meshData)
|
|
762
|
+
? (v) => Math.round(v * 1024) / 1024
|
|
763
|
+
: (v) => v;
|
|
742
764
|
const p = meshData.positions;
|
|
743
765
|
for (let i = 0; i < vertexCount; i++) {
|
|
744
766
|
const base = i * 7;
|
|
745
767
|
const posBase = i * 3;
|
|
746
|
-
interleaved[base] = so ? fr(sox + fr(p[posBase] + dx)) : (p[posBase] + dx);
|
|
747
|
-
interleaved[base + 1] = so ? fr(soy + fr(p[posBase + 1] + dy)) : (p[posBase + 1] + dy);
|
|
748
|
-
interleaved[base + 2] = so ? fr(soz + fr(p[posBase + 2] + dz)) : (p[posBase + 2] + dz);
|
|
768
|
+
interleaved[base] = so ? fr(sox + snap(fr(p[posBase] + dx))) : snap(p[posBase] + dx);
|
|
769
|
+
interleaved[base + 1] = so ? fr(soy + snap(fr(p[posBase + 1] + dy))) : snap(p[posBase + 1] + dy);
|
|
770
|
+
interleaved[base + 2] = so ? fr(soz + snap(fr(p[posBase + 2] + dz))) : snap(p[posBase + 2] + dz);
|
|
749
771
|
const hasNormals = meshData.normals.length > 0;
|
|
750
772
|
interleaved[base + 3] = hasNormals ? meshData.normals[posBase] : 0;
|
|
751
773
|
interleaved[base + 4] = hasNormals ? meshData.normals[posBase + 1] : 0;
|
|
@@ -834,6 +856,29 @@ export class Renderer {
|
|
|
834
856
|
bounds: b ? `${b.min.x.toFixed(0)}..${b.max.x.toFixed(0)} ${b.min.y.toFixed(0)}..${b.max.y.toFixed(0)} ${b.min.z.toFixed(0)}..${b.max.z.toFixed(0)}` : 'none',
|
|
835
857
|
};
|
|
836
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Statistics of the last COMPLETED render() call (draw calls issued,
|
|
861
|
+
* batches drawn / frustum-culled / contribution-culled), or null before
|
|
862
|
+
* the first frame. Pair with `getScene().getResidentGpuBytes()` for the
|
|
863
|
+
* load-complete telemetry snapshot (issue #1682 observability).
|
|
864
|
+
*/
|
|
865
|
+
getFrameStats() {
|
|
866
|
+
return this._lastFrameStats;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Probe the quantized pipeline variants and, when all exist, enable
|
|
870
|
+
* 12-byte quantized batch vertices (issue #1682 phase 6). Returns
|
|
871
|
+
* whether quantization is active — on failure (e.g. a fragile backend
|
|
872
|
+
* rejecting pipeline creation) batches stay on the f32 path.
|
|
873
|
+
*/
|
|
874
|
+
async enableQuantizedBatches() {
|
|
875
|
+
if (!this.pipeline)
|
|
876
|
+
return false;
|
|
877
|
+
const ok = await this.pipeline.ensureQuantizedPipelines();
|
|
878
|
+
if (ok)
|
|
879
|
+
this.scene.setQuantizedBatches(true);
|
|
880
|
+
return ok;
|
|
881
|
+
}
|
|
837
882
|
render(options = {}) {
|
|
838
883
|
this._renderCallCount++;
|
|
839
884
|
if (!this.device.isInitialized() || !this.pipeline) {
|
|
@@ -880,6 +925,24 @@ export class Renderer {
|
|
|
880
925
|
}
|
|
881
926
|
const device = this.device.getDevice();
|
|
882
927
|
const viewProj = this.camera.getViewProjMatrix().m;
|
|
928
|
+
// Frame stats (issue #1682): geometry draw calls + per-frame cull
|
|
929
|
+
// outcomes, snapshotted into _lastFrameStats before queue.submit.
|
|
930
|
+
let frameDrawCalls = 0;
|
|
931
|
+
let frameBatchesDrawn = 0;
|
|
932
|
+
let frameBatchesFrustumCulled = 0;
|
|
933
|
+
let frameBatchesContributionCulled = 0;
|
|
934
|
+
let frameBatchesNotResident = 0;
|
|
935
|
+
let frameBatchesAtLod1 = 0;
|
|
936
|
+
let frameInstancedDrawn = 0;
|
|
937
|
+
let frameInstancedFrustumCulled = 0;
|
|
938
|
+
let frameInstancedContributionCulled = 0;
|
|
939
|
+
// Residency ages are measured in RENDERED frames (idle never ages out).
|
|
940
|
+
this.scene.beginResidencyFrame();
|
|
941
|
+
// Capture renders restore evicted batches SYNCHRONOUSLY so isolation
|
|
942
|
+
// snapshots are complete in this very frame (see RenderOptions doc).
|
|
943
|
+
if (options.restoreEvictedForCapture && this.pipeline) {
|
|
944
|
+
this.scene.restoreAllEvicted(device, this.pipeline);
|
|
945
|
+
}
|
|
883
946
|
const visualEnhancement = this.resolveVisualEnhancement(options.visualEnhancement);
|
|
884
947
|
// Post effects during interaction (orbit/pan/zoom) are governed
|
|
885
948
|
// adaptively: they stay on while the interactive frame cadence holds
|
|
@@ -1446,6 +1509,35 @@ export class Renderer {
|
|
|
1446
1509
|
// Frustum culling for batched meshes - skip entire batches outside the camera view
|
|
1447
1510
|
// This is the primary performance optimization for large models (200K+ meshes)
|
|
1448
1511
|
const frustum = FrustumUtils.fromViewProjMatrix(viewProj);
|
|
1512
|
+
// Contribution culling (issue #1682): skip batches whose world
|
|
1513
|
+
// AABB projects below a pixel threshold. Disabled unless the
|
|
1514
|
+
// caller opts in via options.contributionCull; the threshold is
|
|
1515
|
+
// raised while interacting (quality matters least mid-gesture).
|
|
1516
|
+
const contribThresholdPx = resolveContributionThresholdPx(options.contributionCull, interacting);
|
|
1517
|
+
// LOD1 selection (issue #1682 phase 5): batches projecting below
|
|
1518
|
+
// this draw their simplified index range. Shares the projection
|
|
1519
|
+
// camera with contribution culling. Precedence is intentional:
|
|
1520
|
+
// a batch below the CULL threshold is skipped entirely, so a
|
|
1521
|
+
// lod threshold at or below the cull threshold never fires.
|
|
1522
|
+
const lodScreenPx = options.lod && options.lod.screenPx > 0 ? options.lod.screenPx : 0;
|
|
1523
|
+
const lodBatches = new Set();
|
|
1524
|
+
let cullCam = null;
|
|
1525
|
+
if (contribThresholdPx > 0 || lodScreenPx > 0) {
|
|
1526
|
+
const eye = this.camera.getPosition();
|
|
1527
|
+
const tgt = this.camera.getTarget();
|
|
1528
|
+
const dx = tgt.x - eye.x, dy = tgt.y - eye.y, dz = tgt.z - eye.z;
|
|
1529
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1530
|
+
cullCam = {
|
|
1531
|
+
eye,
|
|
1532
|
+
// Degenerate (eye == target) stays zero-length — the
|
|
1533
|
+
// projection helper fails open (never culls) on it.
|
|
1534
|
+
viewDir: len > 0 ? { x: dx / len, y: dy / len, z: dz / len } : { x: 0, y: 0, z: 0 },
|
|
1535
|
+
mode: this.camera.getProjectionMode(),
|
|
1536
|
+
fovYRadians: this.camera.getFOV(),
|
|
1537
|
+
orthoHalfHeight: this.camera.getOrthoSize(),
|
|
1538
|
+
viewportHeightPx: this.canvas.height,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1449
1541
|
// Pre-compute visibility for each batch (only when filtering is active)
|
|
1450
1542
|
// A batch is visible if ANY of its elements are visible
|
|
1451
1543
|
// A batch is fully visible if ALL of its elements are visible
|
|
@@ -1524,8 +1616,25 @@ export class Renderer {
|
|
|
1524
1616
|
if (batch.bounds) {
|
|
1525
1617
|
const batchAABB = { min: batch.bounds.min, max: batch.bounds.max };
|
|
1526
1618
|
if (!FrustumUtils.isAABBVisible(frustum, batchAABB)) {
|
|
1619
|
+
frameBatchesFrustumCulled++;
|
|
1527
1620
|
continue; // Entire batch is off-screen
|
|
1528
1621
|
}
|
|
1622
|
+
if (cullCam) {
|
|
1623
|
+
const px = projectedAabbRadiusPx(batch.bounds.min, batch.bounds.max, cullCam);
|
|
1624
|
+
// Contribution cull: the whole batch projects below the
|
|
1625
|
+
// pixel threshold — drawing it could change at most a
|
|
1626
|
+
// (sub-)pixel. Selected entities still highlight: the
|
|
1627
|
+
// selection pass draws per-mesh, independent of batches.
|
|
1628
|
+
if (contribThresholdPx > 0 && px < contribThresholdPx) {
|
|
1629
|
+
frameBatchesContributionCulled++;
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
// LOD1: small-but-visible batches draw the simplified
|
|
1633
|
+
// index range over the same vertices.
|
|
1634
|
+
if (lodScreenPx > 0 && px < lodScreenPx && batch.lod1IndexBuffer) {
|
|
1635
|
+
lodBatches.add(batch.id);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1529
1638
|
}
|
|
1530
1639
|
const alpha = alphaForBatch(batch, batch.color[3]);
|
|
1531
1640
|
const nativelyTransparent = alpha < 0.99;
|
|
@@ -1546,10 +1655,28 @@ export class Renderer {
|
|
|
1546
1655
|
if (visibleIds.size > 0) {
|
|
1547
1656
|
pushVisibleAsPartial(batch, visibleIds, nativelyTransparent);
|
|
1548
1657
|
}
|
|
1658
|
+
// A COLD parent has no CPU meshData, so the partial
|
|
1659
|
+
// sub-batch above comes back empty — queue the
|
|
1660
|
+
// residency restore or the visible subset would
|
|
1661
|
+
// stay missing under hide/isolate forever.
|
|
1662
|
+
if (batch.gpuResident === false) {
|
|
1663
|
+
this.scene.requestBatchResidency(batch);
|
|
1664
|
+
}
|
|
1549
1665
|
continue; // Don't add batch to render list
|
|
1550
1666
|
}
|
|
1551
1667
|
}
|
|
1552
|
-
// Fully visible (or no filtering)
|
|
1668
|
+
// Fully visible (or no filtering) — this batch draws from its OWN
|
|
1669
|
+
// GPU buffers. An evicted batch (residency budget, #1682 phase 3a)
|
|
1670
|
+
// is skipped for a frame while its rebuild is queued; the partial
|
|
1671
|
+
// path above is unaffected (sub-batches own separate buffers built
|
|
1672
|
+
// from CPU meshData).
|
|
1673
|
+
if (batch.gpuResident === false) {
|
|
1674
|
+
this.scene.requestBatchResidency(batch);
|
|
1675
|
+
frameBatchesNotResident++;
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
this.scene.recordBatchDrawn(batch);
|
|
1679
|
+
// Transparent batches with mixed
|
|
1553
1680
|
// override membership must be split so non-overridden batchmates
|
|
1554
1681
|
// stay transparent — see splitVisibleIdsByPromotion / issue #677.
|
|
1555
1682
|
if (nativelyTransparent) {
|
|
@@ -1643,12 +1770,44 @@ export class Renderer {
|
|
|
1643
1770
|
tpl[28] = o ? o[0] : 0;
|
|
1644
1771
|
tpl[29] = o ? o[1] : 0;
|
|
1645
1772
|
tpl[30] = o ? o[2] : 0;
|
|
1773
|
+
// Quantized dequantization params (issue #1682 phase 6);
|
|
1774
|
+
// zeroed for f32 batches (their pipelines ignore them).
|
|
1775
|
+
const qz = batch.quantized;
|
|
1776
|
+
tpl[56] = qz ? qz.min[0] : 0;
|
|
1777
|
+
tpl[57] = qz ? qz.min[1] : 0;
|
|
1778
|
+
tpl[58] = qz ? qz.min[2] : 0;
|
|
1779
|
+
tpl[59] = qz ? qz.step : 0;
|
|
1646
1780
|
device.queue.writeBuffer(batch.uniformBuffer, 0, tpl);
|
|
1647
|
-
// Single draw call for entire batch!
|
|
1781
|
+
// Single draw call for entire batch! LOD1-selected batches
|
|
1782
|
+
// bind their simplified index range over the same vertices.
|
|
1783
|
+
const useLod1 = batch.lod1IndexBuffer && batch.lod1IndexCount && lodBatches.has(batch.id);
|
|
1648
1784
|
pass.setBindGroup(0, batch.bindGroup);
|
|
1649
1785
|
pass.setVertexBuffer(0, batch.vertexBuffer);
|
|
1650
|
-
|
|
1651
|
-
|
|
1786
|
+
if (useLod1) {
|
|
1787
|
+
pass.setIndexBuffer(batch.lod1IndexBuffer, 'uint32');
|
|
1788
|
+
pass.drawIndexed(batch.lod1IndexCount);
|
|
1789
|
+
frameBatchesAtLod1++;
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
pass.setIndexBuffer(batch.indexBuffer, 'uint32');
|
|
1793
|
+
pass.drawIndexed(batch.indexCount);
|
|
1794
|
+
}
|
|
1795
|
+
frameDrawCalls++;
|
|
1796
|
+
frameBatchesDrawn++;
|
|
1797
|
+
};
|
|
1798
|
+
// Quantized batches (issue #1682 phase 6) draw through the
|
|
1799
|
+
// quantized pipeline variants; scene only quantizes after the
|
|
1800
|
+
// probe (enableQuantizedBatches) verified they exist, so the
|
|
1801
|
+
// base fallback here is type-safety, never taken.
|
|
1802
|
+
const pipeFor = (batch, kind) => {
|
|
1803
|
+
const base = kind === 'opaque'
|
|
1804
|
+
? this.pipeline.getPipeline()
|
|
1805
|
+
: kind === 'transparent'
|
|
1806
|
+
? this.pipeline.getTransparentPipeline()
|
|
1807
|
+
: this.pipeline.getOverlayPipeline();
|
|
1808
|
+
if (!batch.quantized)
|
|
1809
|
+
return base;
|
|
1810
|
+
return this.pipeline.getQuantizedPipelineVariant(kind) ?? base;
|
|
1652
1811
|
};
|
|
1653
1812
|
// Render opaque batches with the opaque (double-sided) pipeline.
|
|
1654
1813
|
// Material-layer slices render double-sided like all other IFC
|
|
@@ -1661,8 +1820,10 @@ export class Renderer {
|
|
|
1661
1820
|
// Double-siding draws every face of the watertight skin ⇒ solid.
|
|
1662
1821
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
1663
1822
|
for (const batch of opaqueBatches) {
|
|
1823
|
+
pass.setPipeline(pipeFor(batch, 'opaque'));
|
|
1664
1824
|
renderBatch(batch);
|
|
1665
1825
|
}
|
|
1826
|
+
pass.setPipeline(this.pipeline.getPipeline());
|
|
1666
1827
|
// GPU-instancing pass — repeated geometry collated by the producer
|
|
1667
1828
|
// into one template + a per-occurrence instance buffer (mat4 +
|
|
1668
1829
|
// entityId + rgba), drawn with the instanced pipeline as
|
|
@@ -1672,7 +1833,40 @@ export class Renderer {
|
|
|
1672
1833
|
// IFC Z-up→WebGL Y-up swap, so the uniform's model is unused here;
|
|
1673
1834
|
// we reuse the frame's viewProj + section + flags from `tpl`.
|
|
1674
1835
|
const instancedTemplates = this.scene.getInstancedTemplates();
|
|
1836
|
+
// Cull templates ONCE per frame; the transparent instanced
|
|
1837
|
+
// sub-pass below reuses this list. Frustum: the union of the
|
|
1838
|
+
// occurrences' world AABBs off-screen ⇒ every occurrence is.
|
|
1839
|
+
// Contribution: the LARGEST occurrence projected at the union
|
|
1840
|
+
// box's nearest view depth is an upper bound for any single
|
|
1841
|
+
// occurrence — bolts-scattered-everywhere templates cull as
|
|
1842
|
+
// soon as no bolt can exceed the pixel threshold, even though
|
|
1843
|
+
// the union box itself is model-sized. Templates with a
|
|
1844
|
+
// selected occurrence are exempt so the highlight can't vanish.
|
|
1845
|
+
// CATIA-class models put ~97% of draw calls in this pass, so
|
|
1846
|
+
// this is where interactive frame cost lives (issue #1682).
|
|
1847
|
+
let visibleInstanced = instancedTemplates;
|
|
1675
1848
|
if (instancedTemplates.length > 0) {
|
|
1849
|
+
const kept = [];
|
|
1850
|
+
for (const it of instancedTemplates) {
|
|
1851
|
+
if (it.bounds) {
|
|
1852
|
+
if (!FrustumUtils.isAABBVisible(frustum, it.bounds)) {
|
|
1853
|
+
frameInstancedFrustumCulled++;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (contribThresholdPx > 0 &&
|
|
1857
|
+
cullCam &&
|
|
1858
|
+
it.selectedCount === 0 &&
|
|
1859
|
+
projectedInstancedRadiusPx(it.bounds.min, it.bounds.max, it.maxOccRadius, cullCam) <
|
|
1860
|
+
contribThresholdPx) {
|
|
1861
|
+
frameInstancedContributionCulled++;
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
kept.push(it);
|
|
1866
|
+
}
|
|
1867
|
+
visibleInstanced = kept;
|
|
1868
|
+
}
|
|
1869
|
+
if (visibleInstanced.length > 0) {
|
|
1676
1870
|
// Opaque instanced pass. flags.x bit 2 marks "instanced pass" so the
|
|
1677
1871
|
// shader routes per-instance opacity: opaque (or selected) occurrences
|
|
1678
1872
|
// draw here; translucent ones (lens/x-ray/compare overrides) are
|
|
@@ -1681,11 +1875,13 @@ export class Renderer {
|
|
|
1681
1875
|
pass.setPipeline(this.pipeline.getInstancedPipeline());
|
|
1682
1876
|
pass.setBindGroup(0, this.pipeline.getBindGroup());
|
|
1683
1877
|
pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
|
|
1684
|
-
for (const it of
|
|
1878
|
+
for (const it of visibleInstanced) {
|
|
1685
1879
|
pass.setVertexBuffer(0, it.vertexBuffer);
|
|
1686
1880
|
pass.setVertexBuffer(1, it.instanceBuffer);
|
|
1687
1881
|
pass.setIndexBuffer(it.indexBuffer, 'uint32');
|
|
1688
1882
|
pass.drawIndexed(it.indexCount, it.instanceCount);
|
|
1883
|
+
frameDrawCalls++;
|
|
1884
|
+
frameInstancedDrawn++;
|
|
1689
1885
|
}
|
|
1690
1886
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
1691
1887
|
// The TRANSPARENT instanced sub-pass is drawn later, alongside the
|
|
@@ -1738,6 +1934,7 @@ export class Renderer {
|
|
|
1738
1934
|
pass.setVertexBuffer(0, tm.vertexBuffer);
|
|
1739
1935
|
pass.setIndexBuffer(tm.indexBuffer, 'uint32');
|
|
1740
1936
|
pass.drawIndexed(tm.indexCount);
|
|
1937
|
+
frameDrawCalls++;
|
|
1741
1938
|
}
|
|
1742
1939
|
// Restore the opaque pipeline for the passes that follow.
|
|
1743
1940
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
@@ -1760,7 +1957,7 @@ export class Renderer {
|
|
|
1760
1957
|
// lens/Pset colour override, so the overlay paint pass finds depth.
|
|
1761
1958
|
const isTransparent = shouldRouteBatchTransparent(alphaForBatch(subBatch, color[3]), subBatch.expressIds, colorOverrides);
|
|
1762
1959
|
if (isTransparent) {
|
|
1763
|
-
pass.setPipeline(
|
|
1960
|
+
pass.setPipeline(pipeFor(subBatch, 'transparent'));
|
|
1764
1961
|
}
|
|
1765
1962
|
else {
|
|
1766
1963
|
// Opaque (incl. material-layer slices): double-sided.
|
|
@@ -1768,7 +1965,7 @@ export class Renderer {
|
|
|
1768
1965
|
// open watertight-skin bands with unreliable winding,
|
|
1769
1966
|
// so culling punched holes (wall read hollow). See the
|
|
1770
1967
|
// full-batch path above.
|
|
1771
|
-
pass.setPipeline(
|
|
1968
|
+
pass.setPipeline(pipeFor(subBatch, 'opaque'));
|
|
1772
1969
|
opaqueSubBatches.push(subBatch);
|
|
1773
1970
|
}
|
|
1774
1971
|
// Render the sub-batch as a single draw call
|
|
@@ -1794,6 +1991,7 @@ export class Renderer {
|
|
|
1794
1991
|
// bit 1 = overlay; bit 5 (32) = emphasize (pop) — see shader.
|
|
1795
1992
|
tplFlags[0] = options.emphasizeOverrides ? (2 | 32) : 2;
|
|
1796
1993
|
for (const batch of overrideBatches) {
|
|
1994
|
+
pass.setPipeline(pipeFor(batch, 'overlay'));
|
|
1797
1995
|
renderBatch(batch);
|
|
1798
1996
|
}
|
|
1799
1997
|
tplFlags[0] = 0; // restore for any downstream use of the template
|
|
@@ -1858,20 +2056,20 @@ export class Renderer {
|
|
|
1858
2056
|
// a complete depth buffer. Only runs when an override actually made some
|
|
1859
2057
|
// occurrence translucent (otherwise zero cost). flags.x bit 3 flips the
|
|
1860
2058
|
// shader's opacity routing so only translucent occurrences draw here.
|
|
1861
|
-
const instancedTransparent = this.scene.getInstancedTemplates();
|
|
1862
2059
|
const instancedTransparentPipeline = this.pipeline.getInstancedTransparentPipeline();
|
|
1863
|
-
if (
|
|
2060
|
+
if (visibleInstanced.length > 0 &&
|
|
1864
2061
|
this.scene.hasTransparentInstances() &&
|
|
1865
2062
|
instancedTransparentPipeline !== null) {
|
|
1866
2063
|
this.pipeline.writeRawUniforms(tpl, 0x4 | 0x8);
|
|
1867
2064
|
pass.setPipeline(instancedTransparentPipeline);
|
|
1868
2065
|
pass.setBindGroup(0, this.pipeline.getBindGroup());
|
|
1869
2066
|
pass.setBindGroup(1, this.pipeline.getEnvironmentBindGroup());
|
|
1870
|
-
for (const it of
|
|
2067
|
+
for (const it of visibleInstanced) {
|
|
1871
2068
|
pass.setVertexBuffer(0, it.vertexBuffer);
|
|
1872
2069
|
pass.setVertexBuffer(1, it.instanceBuffer);
|
|
1873
2070
|
pass.setIndexBuffer(it.indexBuffer, 'uint32');
|
|
1874
2071
|
pass.drawIndexed(it.indexCount, it.instanceCount);
|
|
2072
|
+
frameDrawCalls++;
|
|
1875
2073
|
}
|
|
1876
2074
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
1877
2075
|
}
|
|
@@ -1879,6 +2077,7 @@ export class Renderer {
|
|
|
1879
2077
|
if (transparentBatches.length > 0) {
|
|
1880
2078
|
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
1881
2079
|
for (const batch of transparentBatches) {
|
|
2080
|
+
pass.setPipeline(pipeFor(batch, 'transparent'));
|
|
1882
2081
|
renderBatch(batch);
|
|
1883
2082
|
}
|
|
1884
2083
|
}
|
|
@@ -1923,6 +2122,7 @@ export class Renderer {
|
|
|
1923
2122
|
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
1924
2123
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
1925
2124
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
2125
|
+
frameDrawCalls++;
|
|
1926
2126
|
}
|
|
1927
2127
|
}
|
|
1928
2128
|
// Ensure selected meshes have uniform buffers and bind groups
|
|
@@ -1983,6 +2183,7 @@ export class Renderer {
|
|
|
1983
2183
|
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
1984
2184
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
1985
2185
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
2186
|
+
frameDrawCalls++;
|
|
1986
2187
|
}
|
|
1987
2188
|
}
|
|
1988
2189
|
else {
|
|
@@ -1998,6 +2199,7 @@ export class Renderer {
|
|
|
1998
2199
|
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
1999
2200
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
2000
2201
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
2202
|
+
frameDrawCalls++;
|
|
2001
2203
|
}
|
|
2002
2204
|
// Render transparent meshes with transparent pipeline (alpha blending)
|
|
2003
2205
|
if (transparentMeshes.length > 0) {
|
|
@@ -2012,6 +2214,7 @@ export class Renderer {
|
|
|
2012
2214
|
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
2013
2215
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
2014
2216
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
2217
|
+
frameDrawCalls++;
|
|
2015
2218
|
}
|
|
2016
2219
|
}
|
|
2017
2220
|
}
|
|
@@ -2184,6 +2387,22 @@ export class Renderer {
|
|
|
2184
2387
|
});
|
|
2185
2388
|
}
|
|
2186
2389
|
device.queue.submit([encoder.finish()]);
|
|
2390
|
+
this._lastFrameStats = {
|
|
2391
|
+
drawCalls: frameDrawCalls,
|
|
2392
|
+
batchesDrawn: frameBatchesDrawn,
|
|
2393
|
+
batchesFrustumCulled: frameBatchesFrustumCulled,
|
|
2394
|
+
batchesContributionCulled: frameBatchesContributionCulled,
|
|
2395
|
+
batchesNotResident: frameBatchesNotResident,
|
|
2396
|
+
batchesAtLod1: frameBatchesAtLod1,
|
|
2397
|
+
instancedDrawn: frameInstancedDrawn,
|
|
2398
|
+
instancedFrustumCulled: frameInstancedFrustumCulled,
|
|
2399
|
+
instancedContributionCulled: frameInstancedContributionCulled,
|
|
2400
|
+
timestamp: performance.now(),
|
|
2401
|
+
};
|
|
2402
|
+
// GPU residency budget (issue #1682 phase 3a): evict least-recently
|
|
2403
|
+
// drawn bucket batches after submit — destruction of just-submitted
|
|
2404
|
+
// buffers is deferred past in-flight work by WebGPU.
|
|
2405
|
+
this.scene.enforceGpuBudget();
|
|
2187
2406
|
// Pop validation error scope and capture the exact error
|
|
2188
2407
|
if (captureGpuError) {
|
|
2189
2408
|
device.popErrorScope().then((error) => {
|