@ifc-lite/renderer 1.35.1 → 1.36.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 +75 -0
- package/dist/contribution-cull.d.ts.map +1 -0
- package/dist/contribution-cull.js +63 -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 +190 -10
- package/dist/index.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 +85 -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 +186 -0
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +818 -31
- 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 +2 -2
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 } 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,21 @@ 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
|
+
// Residency ages are measured in RENDERED frames (idle never ages out).
|
|
937
|
+
this.scene.beginResidencyFrame();
|
|
938
|
+
// Capture renders restore evicted batches SYNCHRONOUSLY so isolation
|
|
939
|
+
// snapshots are complete in this very frame (see RenderOptions doc).
|
|
940
|
+
if (options.restoreEvictedForCapture && this.pipeline) {
|
|
941
|
+
this.scene.restoreAllEvicted(device, this.pipeline);
|
|
942
|
+
}
|
|
883
943
|
const visualEnhancement = this.resolveVisualEnhancement(options.visualEnhancement);
|
|
884
944
|
// Post effects during interaction (orbit/pan/zoom) are governed
|
|
885
945
|
// adaptively: they stay on while the interactive frame cadence holds
|
|
@@ -1446,6 +1506,35 @@ export class Renderer {
|
|
|
1446
1506
|
// Frustum culling for batched meshes - skip entire batches outside the camera view
|
|
1447
1507
|
// This is the primary performance optimization for large models (200K+ meshes)
|
|
1448
1508
|
const frustum = FrustumUtils.fromViewProjMatrix(viewProj);
|
|
1509
|
+
// Contribution culling (issue #1682): skip batches whose world
|
|
1510
|
+
// AABB projects below a pixel threshold. Disabled unless the
|
|
1511
|
+
// caller opts in via options.contributionCull; the threshold is
|
|
1512
|
+
// raised while interacting (quality matters least mid-gesture).
|
|
1513
|
+
const contribThresholdPx = resolveContributionThresholdPx(options.contributionCull, interacting);
|
|
1514
|
+
// LOD1 selection (issue #1682 phase 5): batches projecting below
|
|
1515
|
+
// this draw their simplified index range. Shares the projection
|
|
1516
|
+
// camera with contribution culling. Precedence is intentional:
|
|
1517
|
+
// a batch below the CULL threshold is skipped entirely, so a
|
|
1518
|
+
// lod threshold at or below the cull threshold never fires.
|
|
1519
|
+
const lodScreenPx = options.lod && options.lod.screenPx > 0 ? options.lod.screenPx : 0;
|
|
1520
|
+
const lodBatches = new Set();
|
|
1521
|
+
let cullCam = null;
|
|
1522
|
+
if (contribThresholdPx > 0 || lodScreenPx > 0) {
|
|
1523
|
+
const eye = this.camera.getPosition();
|
|
1524
|
+
const tgt = this.camera.getTarget();
|
|
1525
|
+
const dx = tgt.x - eye.x, dy = tgt.y - eye.y, dz = tgt.z - eye.z;
|
|
1526
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1527
|
+
cullCam = {
|
|
1528
|
+
eye,
|
|
1529
|
+
// Degenerate (eye == target) stays zero-length — the
|
|
1530
|
+
// projection helper fails open (never culls) on it.
|
|
1531
|
+
viewDir: len > 0 ? { x: dx / len, y: dy / len, z: dz / len } : { x: 0, y: 0, z: 0 },
|
|
1532
|
+
mode: this.camera.getProjectionMode(),
|
|
1533
|
+
fovYRadians: this.camera.getFOV(),
|
|
1534
|
+
orthoHalfHeight: this.camera.getOrthoSize(),
|
|
1535
|
+
viewportHeightPx: this.canvas.height,
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1449
1538
|
// Pre-compute visibility for each batch (only when filtering is active)
|
|
1450
1539
|
// A batch is visible if ANY of its elements are visible
|
|
1451
1540
|
// A batch is fully visible if ALL of its elements are visible
|
|
@@ -1524,8 +1613,25 @@ export class Renderer {
|
|
|
1524
1613
|
if (batch.bounds) {
|
|
1525
1614
|
const batchAABB = { min: batch.bounds.min, max: batch.bounds.max };
|
|
1526
1615
|
if (!FrustumUtils.isAABBVisible(frustum, batchAABB)) {
|
|
1616
|
+
frameBatchesFrustumCulled++;
|
|
1527
1617
|
continue; // Entire batch is off-screen
|
|
1528
1618
|
}
|
|
1619
|
+
if (cullCam) {
|
|
1620
|
+
const px = projectedAabbRadiusPx(batch.bounds.min, batch.bounds.max, cullCam);
|
|
1621
|
+
// Contribution cull: the whole batch projects below the
|
|
1622
|
+
// pixel threshold — drawing it could change at most a
|
|
1623
|
+
// (sub-)pixel. Selected entities still highlight: the
|
|
1624
|
+
// selection pass draws per-mesh, independent of batches.
|
|
1625
|
+
if (contribThresholdPx > 0 && px < contribThresholdPx) {
|
|
1626
|
+
frameBatchesContributionCulled++;
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
// LOD1: small-but-visible batches draw the simplified
|
|
1630
|
+
// index range over the same vertices.
|
|
1631
|
+
if (lodScreenPx > 0 && px < lodScreenPx && batch.lod1IndexBuffer) {
|
|
1632
|
+
lodBatches.add(batch.id);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1529
1635
|
}
|
|
1530
1636
|
const alpha = alphaForBatch(batch, batch.color[3]);
|
|
1531
1637
|
const nativelyTransparent = alpha < 0.99;
|
|
@@ -1546,10 +1652,28 @@ export class Renderer {
|
|
|
1546
1652
|
if (visibleIds.size > 0) {
|
|
1547
1653
|
pushVisibleAsPartial(batch, visibleIds, nativelyTransparent);
|
|
1548
1654
|
}
|
|
1655
|
+
// A COLD parent has no CPU meshData, so the partial
|
|
1656
|
+
// sub-batch above comes back empty — queue the
|
|
1657
|
+
// residency restore or the visible subset would
|
|
1658
|
+
// stay missing under hide/isolate forever.
|
|
1659
|
+
if (batch.gpuResident === false) {
|
|
1660
|
+
this.scene.requestBatchResidency(batch);
|
|
1661
|
+
}
|
|
1549
1662
|
continue; // Don't add batch to render list
|
|
1550
1663
|
}
|
|
1551
1664
|
}
|
|
1552
|
-
// Fully visible (or no filtering)
|
|
1665
|
+
// Fully visible (or no filtering) — this batch draws from its OWN
|
|
1666
|
+
// GPU buffers. An evicted batch (residency budget, #1682 phase 3a)
|
|
1667
|
+
// is skipped for a frame while its rebuild is queued; the partial
|
|
1668
|
+
// path above is unaffected (sub-batches own separate buffers built
|
|
1669
|
+
// from CPU meshData).
|
|
1670
|
+
if (batch.gpuResident === false) {
|
|
1671
|
+
this.scene.requestBatchResidency(batch);
|
|
1672
|
+
frameBatchesNotResident++;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
this.scene.recordBatchDrawn(batch);
|
|
1676
|
+
// Transparent batches with mixed
|
|
1553
1677
|
// override membership must be split so non-overridden batchmates
|
|
1554
1678
|
// stay transparent — see splitVisibleIdsByPromotion / issue #677.
|
|
1555
1679
|
if (nativelyTransparent) {
|
|
@@ -1643,12 +1767,44 @@ export class Renderer {
|
|
|
1643
1767
|
tpl[28] = o ? o[0] : 0;
|
|
1644
1768
|
tpl[29] = o ? o[1] : 0;
|
|
1645
1769
|
tpl[30] = o ? o[2] : 0;
|
|
1770
|
+
// Quantized dequantization params (issue #1682 phase 6);
|
|
1771
|
+
// zeroed for f32 batches (their pipelines ignore them).
|
|
1772
|
+
const qz = batch.quantized;
|
|
1773
|
+
tpl[56] = qz ? qz.min[0] : 0;
|
|
1774
|
+
tpl[57] = qz ? qz.min[1] : 0;
|
|
1775
|
+
tpl[58] = qz ? qz.min[2] : 0;
|
|
1776
|
+
tpl[59] = qz ? qz.step : 0;
|
|
1646
1777
|
device.queue.writeBuffer(batch.uniformBuffer, 0, tpl);
|
|
1647
|
-
// Single draw call for entire batch!
|
|
1778
|
+
// Single draw call for entire batch! LOD1-selected batches
|
|
1779
|
+
// bind their simplified index range over the same vertices.
|
|
1780
|
+
const useLod1 = batch.lod1IndexBuffer && batch.lod1IndexCount && lodBatches.has(batch.id);
|
|
1648
1781
|
pass.setBindGroup(0, batch.bindGroup);
|
|
1649
1782
|
pass.setVertexBuffer(0, batch.vertexBuffer);
|
|
1650
|
-
|
|
1651
|
-
|
|
1783
|
+
if (useLod1) {
|
|
1784
|
+
pass.setIndexBuffer(batch.lod1IndexBuffer, 'uint32');
|
|
1785
|
+
pass.drawIndexed(batch.lod1IndexCount);
|
|
1786
|
+
frameBatchesAtLod1++;
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
pass.setIndexBuffer(batch.indexBuffer, 'uint32');
|
|
1790
|
+
pass.drawIndexed(batch.indexCount);
|
|
1791
|
+
}
|
|
1792
|
+
frameDrawCalls++;
|
|
1793
|
+
frameBatchesDrawn++;
|
|
1794
|
+
};
|
|
1795
|
+
// Quantized batches (issue #1682 phase 6) draw through the
|
|
1796
|
+
// quantized pipeline variants; scene only quantizes after the
|
|
1797
|
+
// probe (enableQuantizedBatches) verified they exist, so the
|
|
1798
|
+
// base fallback here is type-safety, never taken.
|
|
1799
|
+
const pipeFor = (batch, kind) => {
|
|
1800
|
+
const base = kind === 'opaque'
|
|
1801
|
+
? this.pipeline.getPipeline()
|
|
1802
|
+
: kind === 'transparent'
|
|
1803
|
+
? this.pipeline.getTransparentPipeline()
|
|
1804
|
+
: this.pipeline.getOverlayPipeline();
|
|
1805
|
+
if (!batch.quantized)
|
|
1806
|
+
return base;
|
|
1807
|
+
return this.pipeline.getQuantizedPipelineVariant(kind) ?? base;
|
|
1652
1808
|
};
|
|
1653
1809
|
// Render opaque batches with the opaque (double-sided) pipeline.
|
|
1654
1810
|
// Material-layer slices render double-sided like all other IFC
|
|
@@ -1661,8 +1817,10 @@ export class Renderer {
|
|
|
1661
1817
|
// Double-siding draws every face of the watertight skin ⇒ solid.
|
|
1662
1818
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
1663
1819
|
for (const batch of opaqueBatches) {
|
|
1820
|
+
pass.setPipeline(pipeFor(batch, 'opaque'));
|
|
1664
1821
|
renderBatch(batch);
|
|
1665
1822
|
}
|
|
1823
|
+
pass.setPipeline(this.pipeline.getPipeline());
|
|
1666
1824
|
// GPU-instancing pass — repeated geometry collated by the producer
|
|
1667
1825
|
// into one template + a per-occurrence instance buffer (mat4 +
|
|
1668
1826
|
// entityId + rgba), drawn with the instanced pipeline as
|
|
@@ -1686,6 +1844,7 @@ export class Renderer {
|
|
|
1686
1844
|
pass.setVertexBuffer(1, it.instanceBuffer);
|
|
1687
1845
|
pass.setIndexBuffer(it.indexBuffer, 'uint32');
|
|
1688
1846
|
pass.drawIndexed(it.indexCount, it.instanceCount);
|
|
1847
|
+
frameDrawCalls++;
|
|
1689
1848
|
}
|
|
1690
1849
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
1691
1850
|
// The TRANSPARENT instanced sub-pass is drawn later, alongside the
|
|
@@ -1738,6 +1897,7 @@ export class Renderer {
|
|
|
1738
1897
|
pass.setVertexBuffer(0, tm.vertexBuffer);
|
|
1739
1898
|
pass.setIndexBuffer(tm.indexBuffer, 'uint32');
|
|
1740
1899
|
pass.drawIndexed(tm.indexCount);
|
|
1900
|
+
frameDrawCalls++;
|
|
1741
1901
|
}
|
|
1742
1902
|
// Restore the opaque pipeline for the passes that follow.
|
|
1743
1903
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
@@ -1760,7 +1920,7 @@ export class Renderer {
|
|
|
1760
1920
|
// lens/Pset colour override, so the overlay paint pass finds depth.
|
|
1761
1921
|
const isTransparent = shouldRouteBatchTransparent(alphaForBatch(subBatch, color[3]), subBatch.expressIds, colorOverrides);
|
|
1762
1922
|
if (isTransparent) {
|
|
1763
|
-
pass.setPipeline(
|
|
1923
|
+
pass.setPipeline(pipeFor(subBatch, 'transparent'));
|
|
1764
1924
|
}
|
|
1765
1925
|
else {
|
|
1766
1926
|
// Opaque (incl. material-layer slices): double-sided.
|
|
@@ -1768,7 +1928,7 @@ export class Renderer {
|
|
|
1768
1928
|
// open watertight-skin bands with unreliable winding,
|
|
1769
1929
|
// so culling punched holes (wall read hollow). See the
|
|
1770
1930
|
// full-batch path above.
|
|
1771
|
-
pass.setPipeline(
|
|
1931
|
+
pass.setPipeline(pipeFor(subBatch, 'opaque'));
|
|
1772
1932
|
opaqueSubBatches.push(subBatch);
|
|
1773
1933
|
}
|
|
1774
1934
|
// Render the sub-batch as a single draw call
|
|
@@ -1794,6 +1954,7 @@ export class Renderer {
|
|
|
1794
1954
|
// bit 1 = overlay; bit 5 (32) = emphasize (pop) — see shader.
|
|
1795
1955
|
tplFlags[0] = options.emphasizeOverrides ? (2 | 32) : 2;
|
|
1796
1956
|
for (const batch of overrideBatches) {
|
|
1957
|
+
pass.setPipeline(pipeFor(batch, 'overlay'));
|
|
1797
1958
|
renderBatch(batch);
|
|
1798
1959
|
}
|
|
1799
1960
|
tplFlags[0] = 0; // restore for any downstream use of the template
|
|
@@ -1872,6 +2033,7 @@ export class Renderer {
|
|
|
1872
2033
|
pass.setVertexBuffer(1, it.instanceBuffer);
|
|
1873
2034
|
pass.setIndexBuffer(it.indexBuffer, 'uint32');
|
|
1874
2035
|
pass.drawIndexed(it.indexCount, it.instanceCount);
|
|
2036
|
+
frameDrawCalls++;
|
|
1875
2037
|
}
|
|
1876
2038
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
1877
2039
|
}
|
|
@@ -1879,6 +2041,7 @@ export class Renderer {
|
|
|
1879
2041
|
if (transparentBatches.length > 0) {
|
|
1880
2042
|
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
1881
2043
|
for (const batch of transparentBatches) {
|
|
2044
|
+
pass.setPipeline(pipeFor(batch, 'transparent'));
|
|
1882
2045
|
renderBatch(batch);
|
|
1883
2046
|
}
|
|
1884
2047
|
}
|
|
@@ -1923,6 +2086,7 @@ export class Renderer {
|
|
|
1923
2086
|
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
1924
2087
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
1925
2088
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
2089
|
+
frameDrawCalls++;
|
|
1926
2090
|
}
|
|
1927
2091
|
}
|
|
1928
2092
|
// Ensure selected meshes have uniform buffers and bind groups
|
|
@@ -1983,6 +2147,7 @@ export class Renderer {
|
|
|
1983
2147
|
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
1984
2148
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
1985
2149
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
2150
|
+
frameDrawCalls++;
|
|
1986
2151
|
}
|
|
1987
2152
|
}
|
|
1988
2153
|
else {
|
|
@@ -1998,6 +2163,7 @@ export class Renderer {
|
|
|
1998
2163
|
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
1999
2164
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
2000
2165
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
2166
|
+
frameDrawCalls++;
|
|
2001
2167
|
}
|
|
2002
2168
|
// Render transparent meshes with transparent pipeline (alpha blending)
|
|
2003
2169
|
if (transparentMeshes.length > 0) {
|
|
@@ -2012,6 +2178,7 @@ export class Renderer {
|
|
|
2012
2178
|
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
2013
2179
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
2014
2180
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
2181
|
+
frameDrawCalls++;
|
|
2015
2182
|
}
|
|
2016
2183
|
}
|
|
2017
2184
|
}
|
|
@@ -2184,6 +2351,19 @@ export class Renderer {
|
|
|
2184
2351
|
});
|
|
2185
2352
|
}
|
|
2186
2353
|
device.queue.submit([encoder.finish()]);
|
|
2354
|
+
this._lastFrameStats = {
|
|
2355
|
+
drawCalls: frameDrawCalls,
|
|
2356
|
+
batchesDrawn: frameBatchesDrawn,
|
|
2357
|
+
batchesFrustumCulled: frameBatchesFrustumCulled,
|
|
2358
|
+
batchesContributionCulled: frameBatchesContributionCulled,
|
|
2359
|
+
batchesNotResident: frameBatchesNotResident,
|
|
2360
|
+
batchesAtLod1: frameBatchesAtLod1,
|
|
2361
|
+
timestamp: performance.now(),
|
|
2362
|
+
};
|
|
2363
|
+
// GPU residency budget (issue #1682 phase 3a): evict least-recently
|
|
2364
|
+
// drawn bucket batches after submit — destruction of just-submitted
|
|
2365
|
+
// buffers is deferred past in-flight work by WebGPU.
|
|
2366
|
+
this.scene.enforceGpuBudget();
|
|
2187
2367
|
// Pop validation error scope and capture the exact error
|
|
2188
2368
|
if (captureGpuError) {
|
|
2189
2369
|
device.popErrorScope().then((error) => {
|