perf: eliminate per-frame re-tessellation and GPU re-upload in large files

Add two complementary render caches that make mouse navigation essentially
free regardless of file size:

Option A – Wire tessellation cache: entity_wires_arc() caches the
tessellated Vec<WireModel> keyed by geometry_epoch and returns an Arc,
so navigation frames (no preview wires) incur zero Vec clones and zero
tessellation work. Primitive::wires changed to Arc<Vec<WireModel>>.

Option B – GPU buffer cache: Pipeline stores cached_epoch; prepare()
skips all upload_wires/hatches/images/meshes calls when the epoch is
unchanged, writing only the 192-byte camera uniform instead.

geometry_epoch is a process-wide AtomicU64 incremented by every Scene
mutation (add/erase entity, selection, layer visibility, layout switch,
populate_*, grip, transform, copy, clear, preview wires). Using a global
counter rather than a per-scene counter prevents two tabs that happen to
have the same local bump count from sharing a cached_epoch value, which
was causing the first file's geometry to remain on screen after opening a
second file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-26 02:13:31 +03:00
commit d1ecaf4695
5 changed files with 225 additions and 20 deletions

View file

@ -47,6 +47,14 @@ use iced::time::Duration;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
/// Global counter so every Scene and every geometry mutation gets a
/// process-wide unique epoch. This prevents two different tabs (Scenes)
/// from ever sharing the same epoch value, which would cause the shared
/// GPU Pipeline to skip re-uploading geometry when switching tabs.
static GEOMETRY_EPOCH: AtomicU64 = AtomicU64::new(1);
pub struct Scene {
pub camera: Rc<RefCell<Camera>>,
@ -60,6 +68,14 @@ pub struct Scene {
/// Committed-segment wire drawn during multi-point commands (normal colour).
pub interim_wire: Option<WireModel>,
pub camera_generation: u64,
/// Incremented whenever geometry-affecting state changes (entities, selection,
/// preview wires, layer visibility, layout). The GPU pipeline uses this to
/// skip re-uploading unchanged geometry buffers every frame.
pub geometry_epoch: u64,
/// Cached tessellation of all visible entity wires for the current layout.
/// Keyed by `geometry_epoch`; invalidated automatically when the epoch changes.
/// Uses `Arc` so `build_primitive()` avoids a full Vec clone during navigation.
wire_cache: RefCell<Option<(u64, Arc<Vec<WireModel>>)>>,
/// Active layout name — "Model" or a paper space layout name.
pub current_layout: String,
/// GPU render data for hatch fills, keyed by the DXF entity Handle.
@ -89,6 +105,8 @@ impl Scene {
preview_wires: vec![],
interim_wire: None,
camera_generation: 0,
geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
wire_cache: RefCell::new(None),
current_layout: "Model".to_string(),
hatches: HashMap::new(),
meshes: HashMap::new(),
@ -99,6 +117,10 @@ impl Scene {
}
}
pub fn bump_geometry(&mut self) {
self.geometry_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
}
/// Public accessor for the block-record handle of the current layout.
/// Used by external callers (e.g. `commit_entity`) that need the handle
/// without going through private API.
@ -343,14 +365,31 @@ impl Scene {
.collect()
}
/// Build WireModels from all document entities + optional preview wire.
pub fn entity_wires(&self) -> Vec<WireModel> {
/// Build WireModels from all document entities for the current layout.
/// Returns a shared `Arc` so `build_primitive()` can skip the clone during
/// navigation frames where no preview wires are active.
pub(super) fn entity_wires_arc(&self) -> Arc<Vec<WireModel>> {
{
let cache = self.wire_cache.borrow();
if let Some((cached_epoch, ref arc)) = *cache {
if cached_epoch == self.geometry_epoch {
return Arc::clone(arc);
}
}
}
let layout_block = self.current_layout_block_handle();
let mut wires: Vec<WireModel> = self.wires_for_block(layout_block);
let mut wires = self.wires_for_block(layout_block);
if self.current_layout != "Model" {
wires.extend(self.viewport_content_wires(layout_block, None, None));
}
wires
let arc = Arc::new(wires);
*self.wire_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
arc
}
/// Build WireModels from all document entities + optional preview wire.
pub fn entity_wires(&self) -> Vec<WireModel> {
(*self.entity_wires_arc()).clone()
}
/// Wires that should participate in hit-testing, snapping, and selection.
@ -1126,6 +1165,7 @@ impl Scene {
self.current_layout = "Model".to_string();
}
self.bump_geometry();
true
}
@ -1224,6 +1264,7 @@ impl Scene {
if let Some(model) = mesh_seed {
self.meshes.insert(handle, model);
}
self.bump_geometry();
}
handle
}
@ -1646,6 +1687,7 @@ impl Scene {
self.images.insert(handle, model);
}
}
self.bump_geometry();
}
pub fn populate_hatches_from_document(&mut self) {
@ -1677,6 +1719,7 @@ impl Scene {
self.hatches.insert(handle, m);
}
}
self.bump_geometry();
}
/// Tessellate all `Solid3D` entities in the current document into
@ -1712,6 +1755,7 @@ impl Scene {
self.meshes.insert(handle, m);
}
}
self.bump_geometry();
}
/// Rebuild hatch / image / mesh caches after the document is modified
@ -1783,17 +1827,20 @@ impl Scene {
self.meshes = HashMap::new();
*self.camera.borrow_mut() = Camera::default();
self.camera_generation += 1;
self.bump_geometry();
}
// ── Preview wire ──────────────────────────────────────────────────────
pub fn set_preview_wires(&mut self, wires: Vec<WireModel>) {
self.preview_wires = wires;
self.bump_geometry();
}
pub fn clear_preview_wire(&mut self) {
self.preview_wires = vec![];
self.interim_wire = None;
self.bump_geometry();
}
pub fn wire_models_for(&self, handles: &[acadrust::Handle]) -> Vec<WireModel> {
@ -1820,6 +1867,7 @@ impl Scene {
pub fn set_interim_wire(&mut self, w: WireModel) {
self.interim_wire = Some(w);
self.bump_geometry();
}
// ── Selection ─────────────────────────────────────────────────────────
@ -1829,10 +1877,12 @@ impl Scene {
self.selected.clear();
}
self.selected.insert(handle);
self.bump_geometry();
}
pub fn deselect_all(&mut self) {
self.selected.clear();
self.bump_geometry();
}
pub fn selected_entities(&self) -> Vec<(Handle, &EntityType)> {
@ -1873,6 +1923,7 @@ impl Scene {
}
self.document.objects.remove(gh);
}
self.bump_geometry();
}
// ── Group helpers ──────────────────────────────────────────────────────
@ -1955,6 +2006,7 @@ impl Scene {
for h in to_add {
self.selected.insert(h);
}
self.bump_geometry();
}
// ── Layer helpers ──────────────────────────────────────────────────────
@ -1963,6 +2015,7 @@ impl Scene {
if let Some(layer) = self.document.layers.get_mut(name) {
layer.flags.off = !layer.flags.off;
}
self.bump_geometry();
}
pub fn toggle_layer_lock(&mut self, name: &str) {
@ -1990,6 +2043,7 @@ impl Scene {
}
}
}
self.bump_geometry();
}
pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec<Handle> {
@ -2015,6 +2069,7 @@ impl Scene {
}
new_handles.push(h);
}
self.bump_geometry();
new_handles
}
@ -2040,6 +2095,7 @@ impl Scene {
}
_ => {}
}
self.bump_geometry();
}
// ── Hit-test convenience: wire name → Handle ──────────────────────────

View file

@ -52,6 +52,9 @@ pub struct Pipeline {
gpu_images: Vec<ImageGpu>,
gpu_meshes: Vec<MeshGpu>,
pub viewcube: ViewCubePipeline,
/// Last geometry epoch for which GPU buffers were uploaded.
/// Initialized to u64::MAX so the first frame always uploads.
pub cached_epoch: u64,
}
impl Pipeline {
@ -476,6 +479,7 @@ impl Pipeline {
gpu_images: vec![],
gpu_meshes: vec![],
viewcube,
cached_epoch: u64::MAX,
}
}

View file

@ -9,6 +9,8 @@ use iced::mouse;
use iced::widget::shader::{self, Viewport};
use iced::{Rectangle, Size};
use std::sync::Arc;
use super::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
use super::pipeline::Pipeline;
use super::tessellate;
@ -75,7 +77,7 @@ pub struct CameraState {
#[derive(Debug)]
pub struct Primitive {
pub(super) wires: Vec<WireModel>,
pub(super) wires: Arc<Vec<WireModel>>,
pub(super) hatches: Vec<HatchModel>,
/// Wipeout fills — rendered in a separate pass AFTER wires.
pub(super) wipeout_hatches: Vec<HatchModel>,
@ -89,6 +91,7 @@ pub struct Primitive {
/// Background color used to clear the MSAA buffer at the start of each frame.
pub(super) bg_color: [f32; 4],
pub(super) show_viewcube: bool,
pub(super) geometry_epoch: u64,
}
// ── shader::Primitive impl ────────────────────────────────────────────────
@ -116,11 +119,14 @@ impl shader::Primitive for Primitive {
pipeline.ensure_depth_texture(device, clip_size);
pipeline.viewcube.ensure_depth_texture(device, full_size);
pipeline.upload_uniforms(queue, &self.uniforms);
pipeline.upload_hatches(device, &self.hatches);
pipeline.upload_wipeouts(device, &self.wipeout_hatches);
pipeline.upload_images(device, queue, &self.images);
pipeline.upload_meshes(device, &self.meshes);
pipeline.upload_wires(device, &self.wires);
if self.geometry_epoch != pipeline.cached_epoch {
pipeline.upload_hatches(device, &self.hatches);
pipeline.upload_wipeouts(device, &self.wipeout_hatches);
pipeline.upload_images(device, queue, &self.images);
pipeline.upload_meshes(device, &self.meshes);
pipeline.upload_wires(device, &self.wires[..]);
pipeline.cached_epoch = self.geometry_epoch;
}
if self.show_viewcube {
pipeline.viewcube.upload(
queue,
@ -226,11 +232,17 @@ impl Scene {
let cam = self.camera.borrow();
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
let mut all_wires = self.entity_wires();
if let Some(iw) = &self.interim_wire {
all_wires.push(iw.clone());
}
all_wires.extend(self.preview_wires.iter().cloned());
let entity_arc = self.entity_wires_arc();
let all_wires = if self.interim_wire.is_none() && self.preview_wires.is_empty() {
entity_arc
} else {
let mut v = (*entity_arc).clone();
if let Some(iw) = &self.interim_wire {
v.push(iw.clone());
}
v.extend(self.preview_wires.iter().cloned());
Arc::new(v)
};
let bg_color = if self.current_layout == "Model" {
self.bg_color
@ -249,6 +261,7 @@ impl Scene {
hover_region,
bg_color,
show_viewcube,
geometry_epoch: self.geometry_epoch,
}
}
@ -266,11 +279,17 @@ impl Scene {
None => return self.build_primitive(hover_region, bounds, false),
};
let mut all_wires = self.model_wires_for_viewport(vp_handle);
if let Some(iw) = &self.interim_wire {
all_wires.push(iw.clone());
}
all_wires.extend(self.preview_wires.iter().cloned());
let base_wires = self.model_wires_for_viewport(vp_handle);
let all_wires = if self.interim_wire.is_none() && self.preview_wires.is_empty() {
Arc::new(base_wires)
} else {
let mut v = base_wires;
if let Some(iw) = &self.interim_wire {
v.push(iw.clone());
}
v.extend(self.preview_wires.iter().cloned());
Arc::new(v)
};
Primitive {
wires: all_wires,
@ -283,6 +302,7 @@ impl Scene {
hover_region,
bg_color: self.bg_color,
show_viewcube: false,
geometry_epoch: self.geometry_epoch,
}
}