perf(render): batch solid meshes, fix per-pick freezes on heavy 3D models

Opening / navigating a heavy 3D DWG (e.g. a BIM model with ~10k ACIS solids)
was effectively frozen. Several independent costs, all addressed here:

- Face3D edge/fill buffers were re-uploaded on every camera move (keyed on
  camera_generation). They are world-space and selection-independent, so gate
  them on (geometry_epoch, fill_mode) instead — a pan no longer re-walks every
  wire.
- Solid meshes were drawn one buffer + one draw call per solid (~10k draws/
  frame), which strangled the GPU front end. Concatenate every solid's LOD0
  into a few large buffers (split only to stay under the 256 MB buffer cap) and
  draw the whole set in a handful of calls. Built once per geometry epoch, so
  selection / hover never re-pack it.
- Selection / hover highlight is now a small tinted overlay of just the picked
  solids, drawn last with an Always-depth pipeline so it shows on top even when
  the solid is occluded — instead of re-tinting and re-uploading all meshes.
- Hover / pick hit-testing scanned every triangle of every solid each move
  (seconds on a heavy model). Add a 3D-AABB screen-rect broad phase
  (MeshLodSet now carries z_aabb) and reuse the renderer's cached expanded mesh
  set, so picking is O(solids) cheap projections plus the few solids actually
  under the cursor.
- Block-internal hatch hit-testing exploded every INSERT on every hover.
  Cache the result per geometry epoch and skip blocks that contain no hatch.
- Curved-surface (cone/sphere/torus) tessellation tolerance is now radius-
  relative (CURVE_REL_TOL) so a cylinder's facet count is size-independent and
  matches the circle/arc wire tessellation, instead of exploding on large radii.

Also adds docs/tessellation.md mapping every EntityType to its tessellation
path (truck B-rep vs truck curve topology vs direct).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-30 22:46:29 +03:00
commit a994a148fe
10 changed files with 572 additions and 146 deletions

View file

@ -263,6 +263,16 @@ impl shader::Primitive for Primitive {
if geo_changed {
inner.upload_images(device, queue, &vp.images[..]);
}
inner.cached_epoch = cur_key;
}
// Face3D edge/fill buffers are world-space and selection-independent
// (upload_face3d takes no selection input), so they only change with
// the geometry or the 3D-fill toggle — never on a pan/orbit. Gating
// here on `(geometry_epoch, face3d_fill_active)` instead of inside the
// `cur_key` block (which carries `camera_generation`) stops a camera
// move from re-walking every wire to rebuild the Face3D fill buffer.
let face3d_key = (vp.geometry_epoch, face3d_fill_active);
if face3d_key != inner.cached_face3d_key {
inner.upload_face3d(
device,
&vp.face3d_wires[..],
@ -270,7 +280,7 @@ impl shader::Primitive for Primitive {
!face3d_fill_active,
&vp.draw_depths,
);
inner.cached_epoch = cur_key;
inner.cached_face3d_key = face3d_key;
}
// Wire buffers are world-space, so a camera move alone doesn't
// change them — only the view_proj uniform (uploaded every frame).
@ -298,18 +308,25 @@ impl shader::Primitive for Primitive {
);
inner.cached_selection = sel_key;
}
// Solid meshes carry their selection/hover tint baked into the
// vertex colours, so re-upload them when the geometry OR the
// highlight set changes.
let mesh_key = (vp.geometry_epoch, vp.selection_generation);
if mesh_key != inner.cached_mesh_key {
inner.upload_meshes(
// Batched solid meshes — geometry-only, so they ride the geometry
// epoch alone and stay resident across camera moves and selection /
// hover changes (no per-pick rebuild of the whole solid set).
if vp.geometry_epoch != inner.cached_mesh_batch_epoch {
inner.upload_mesh_batch(device, &vp.meshes[..]);
inner.cached_mesh_batch_epoch = vp.geometry_epoch;
}
// Selection / hover highlight overlay — tinted copies of just the
// picked solids, rebuilt only when the highlight set (or geometry)
// changes. Drawn over the static batch so the base never re-packs.
let hl_key = (vp.geometry_epoch, vp.selection_generation);
if hl_key != inner.cached_highlight_key {
inner.upload_mesh_highlight(
device,
&vp.meshes[..],
&vp.selected_handles,
vp.hover_handle,
);
inner.cached_mesh_key = mesh_key;
inner.cached_highlight_key = hl_key;
}
// Live overlay (command preview / interim / grip drag) — small and
// refreshed every frame it's present, so a drag never re-uploads