Implement key strategies for improving EXT_mesh_primitive_edge_visibility tile load time
What happened?
Problem
EdgeVisibilityPipelineStage runs whenever primitive.edgeVisibility is defined; edgeDisplayMode only gates the final draw-command push. So in the default SURFACES_ONLY mode (edges never rendered), every tile still pays for edge extraction/dedup, quad-expanded edge geometry build + GPU upload, and the scene edge MRT framebuffer.
Measured (CesiumJS main, Chromium/ANGLE Metal, vsync off, localhost, default options): Dataset A loads in 1082 ms without the extension vs 2802 ms with it (+159%), plus ~15% lower uncapped FPS. Across an 18-dataset AEC suite the load penalty is +7% to +159% (median ~+35%); gzipped payload delta is only ~2–10%, so the cost is processing, not transfer.
Possible fixes
Fix 1: build edge geometry lazily
Skip EdgeVisibilityPipelineStage/EdgeDetectionPipelineStage in ModelRuntimePrimitive.configurePipeline when edgeDisplayMode === SURFACES_ONLY, and call resetDrawCommands() from the edgeDisplayMode setter so edges build on demand when first enabled.
Prototype (3 runs, run 1 dropped): load 2802 → ~1420 ms (vs no-ext ~1169 ms; residual mostly noise); FPS and JS heap at parity with no-ext; sanity anchors identical. Enabling edge display later costs a one-time ~1.2 s rebuild for this dataset.
Fix 2: cheaper edge dedup when edges are built
With edges displayed, extractVisibleEdges dominates the load profile, mostly from allocating a "${small},${big}" string per edge for the dedup Set. Numeric keys (small * 2**32 + big, exact for ≤2^21 vertices, string fallback beyond) cut its self time 800 → 300 ms and displayed-mode load 3.19 → 2.69 s. Further headroom exists (per-edge objects → typed arrays, buffer consolidation), but with fix 1 this path is opt-in only.
Reproduction steps
- Produce two variants of the same tileset:
no-ext(no AEC extensions) andedges(EXT_mesh_primitive_edge_visibilityonly). Serve both over localhost to remove network variance. - Drive Chromium from Playwright with vsync uncapped and real GPU rendering:
--enable-gpu --disable-frame-rate-limit --disable-gpu-vsync - In the page, create a
Viewerwith globe/sky/atmosphere/sun disabled andrequestRenderMode: false, then load the tileset with default options (i.e.edgeDisplayModeleft atSURFACES_ONLY(edges hidden)). - Time from
Cesium3DTileset.fromUrlto "all tiles loaded". Pollingtileset.tilesLoadeduntil it is stable for ~10 consecutive frames is more reliable than theallTilesLoadedevent, which doesn't always re-fire. - Fly to a fixed, deterministic camera view derived from the tileset bounding sphere (e.g. center, heading 45°, pitch -15°, range
max(radius * 0.08, 1)) so both variants see identical content. - Sample per-frame deltas over a fixed window for FPS/percentiles, and read
tileset.statisticsforgeometryByteLengthandtrianglesSelected. - Use a fresh browser per run and discard the first run (cold OS file cache can double load time). 3+ runs per variant.
- Sanity check:
trianglesSelected,commands, and selected-tile counts should match between the two variants; that confirms an apples-to-apples comparison.
Expected: the edges variant loads in roughly the same time as no-ext, since edges are not being displayed.
Actual: the edges variant takes substantially longer to load (up to ~2.6x here) and renders ~10–15% slower, despite no edges being drawn.
Sandcastle example
No response
Environment
Browser: Chromium via Playwright, ANGLE/Metal backend, macOS (Apple silicon), vsync disabled
CesiumJS Version: CesiumJS main
Operating System: macOS
AI acknowledgment
- I used AI to generate this issue report.
- (If the above is checked) I have reviewed the AI-generated content before submitting.
Source: CesiumGS/cesium