#13671·cesium

Implement key strategies for improving EXT_mesh_primitive_edge_visibility tile load time

Author: markschlosseratbentleyCreated Aug 4, 2026Updated Sep 15, 2026
Labelstype - bugcategory - 3d tiles

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

  1. Produce two variants of the same tileset: no-ext (no AEC extensions) and edges (EXT_mesh_primitive_edge_visibility only). Serve both over localhost to remove network variance.
  2. Drive Chromium from Playwright with vsync uncapped and real GPU rendering:
    --enable-gpu --disable-frame-rate-limit --disable-gpu-vsync
  3. In the page, create a Viewer with globe/sky/atmosphere/sun disabled and requestRenderMode: false, then load the tileset with default options (i.e. edgeDisplayMode left at SURFACES_ONLY (edges hidden)).
  4. Time from Cesium3DTileset.fromUrl to "all tiles loaded". Polling tileset.tilesLoaded until it is stable for ~10 consecutive frames is more reliable than the allTilesLoaded event, which doesn't always re-fire.
  5. 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.
  6. Sample per-frame deltas over a fixed window for FPS/percentiles, and read tileset.statistics for geometryByteLength and trianglesSelected.
  7. Use a fresh browser per run and discard the first run (cold OS file cache can double load time). 3+ runs per variant.
  8. 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.