perf: cache paper-canvas tessellation and viewport projections (Option L)
Add two epoch-keyed caches to eliminate per-frame work in paper-space layouts: paper_sheet_cache — caches wires_for_block(layout_block) as Arc<Vec<WireModel>>. Previously paper_sheet_wires() re-tessellated all paper-space entities (title block, annotations, viewport borders) on every mouse-move frame. Now it is an O(1) Arc clone. entity_wires_arc() reuses the same cache so a geometry-change frame triggers only one tessellation pass even when both the GPU renderer and the Iced canvas are active. paper_projected_cache — caches projected+clipped wires per viewport handle. viewport_content_wires() now uses model_wires_for_viewport_arc() for the tessellation step (already O(1) via viewport_wire_cache) and stores the projection+clip result keyed by (vp_handle, epoch). Navigation frames (pan/zoom on the paper sheet) are O(N_viewports) Arc/slice copies instead of O(N_entities × N_points × N_viewports) tessellate+project work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bfd8d0411a
commit
9a1211842d
1 changed files with 60 additions and 55 deletions
115
src/scene/mod.rs
115
src/scene/mod.rs
|
|
@ -91,6 +91,13 @@ pub struct Scene {
|
|||
/// Per-viewport wire cache for paper-space rendering.
|
||||
/// Maps vp_handle → (geometry_epoch, Arc<Vec<WireModel>>).
|
||||
viewport_wire_cache: RefCell<HashMap<Handle, (u64, Arc<Vec<WireModel>>)>>,
|
||||
/// Cached tessellation of paper-space layout block entities (title block, annotations, etc.).
|
||||
/// Separate from `wire_cache` so paper_canvas_wires() doesn't re-tessellate on every frame.
|
||||
paper_sheet_cache: RefCell<Option<(u64, Arc<Vec<WireModel>>)>>,
|
||||
/// Per-viewport projected wire cache for the paper canvas (2-D Iced widget).
|
||||
/// Stores projected + clipped wires in paper-space coordinates.
|
||||
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
||||
paper_projected_cache: RefCell<HashMap<Handle, (u64, 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.
|
||||
|
|
@ -128,6 +135,8 @@ impl Scene {
|
|||
image_cache: RefCell::new(None),
|
||||
mesh_cache: RefCell::new(None),
|
||||
viewport_wire_cache: RefCell::new(HashMap::new()),
|
||||
paper_sheet_cache: RefCell::new(None),
|
||||
paper_projected_cache: RefCell::new(HashMap::new()),
|
||||
current_layout: "Model".to_string(),
|
||||
hatches: HashMap::new(),
|
||||
meshes: HashMap::new(),
|
||||
|
|
@ -386,6 +395,24 @@ impl Scene {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Cached tessellation of the current layout block's paper-space entities.
|
||||
/// Shared by both `entity_wires_arc()` and `paper_canvas_wires()` so a single
|
||||
/// cache miss triggers only one tessellation pass, not two.
|
||||
fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
||||
{
|
||||
let cache = self.paper_sheet_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 arc = Arc::new(self.wires_for_block(layout_block));
|
||||
*self.paper_sheet_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
||||
arc
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
|
@ -399,7 +426,9 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
let mut wires = self.wires_for_block(layout_block);
|
||||
// Reuse the paper_sheet_cache to avoid a duplicate tessellation pass
|
||||
// when both entity_wires_arc() and paper_canvas_wires() are called in the same frame.
|
||||
let mut wires = (*self.paper_sheet_wires_arc()).clone();
|
||||
if self.current_layout != "Model" {
|
||||
wires.extend(self.viewport_content_wires(layout_block, None, None));
|
||||
}
|
||||
|
|
@ -699,7 +728,6 @@ impl Scene {
|
|||
exclude_vp: Option<Handle>,
|
||||
) -> Vec<WireModel> {
|
||||
use acadrust::entities::Viewport;
|
||||
use std::collections::HashSet as HSet;
|
||||
|
||||
let viewports: Vec<&Viewport> = self
|
||||
.document
|
||||
|
|
@ -720,21 +748,26 @@ impl Scene {
|
|||
return vec![];
|
||||
}
|
||||
|
||||
let model_block = self.model_space_block_handle();
|
||||
let mut result = Vec::new();
|
||||
|
||||
for vp in viewports {
|
||||
// ── Per-viewport frozen layer set ─────────────────────────────
|
||||
let frozen: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
||||
let vp_handle = vp.common.handle;
|
||||
|
||||
// ── Fast path: return cached projected wires ──────────────────
|
||||
{
|
||||
let cache = self.paper_projected_cache.borrow();
|
||||
if let Some((cached_epoch, ref wires)) = cache.get(&vp_handle) {
|
||||
if *cached_epoch == self.geometry_epoch {
|
||||
result.extend_from_slice(wires);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cache miss: compute projection ────────────────────────────
|
||||
|
||||
// ── View coordinate frame ─────────────────────────────────────
|
||||
// Use camera_for_viewport so the axes match the GPU renderer exactly.
|
||||
// GPU uses look_at_rh(eye, target, rotation*Y), which gives:
|
||||
// screen_right = normalize((target-eye) × up) = rotation * X
|
||||
// screen_up = rotation * Y
|
||||
// The old Z×vd cross-product gives the opposite sign for views
|
||||
// that have a Y component in view_direction (front/back), causing mirroring.
|
||||
let cam_frame = match self.camera_for_viewport(vp.common.handle) {
|
||||
let cam_frame = match self.camera_for_viewport(vp_handle) {
|
||||
Some(c) => c,
|
||||
None => continue,
|
||||
};
|
||||
|
|
@ -761,34 +794,10 @@ impl Scene {
|
|||
let hw = (vp.width / 2.0) as f32;
|
||||
let hh = (vp.height / 2.0) as f32;
|
||||
|
||||
// ── Collect model wires with per-vp layer freeze ──────────────
|
||||
let model_wires: Vec<WireModel> = self
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
let c = e.common();
|
||||
if c.invisible || matches!(e, EntityType::Viewport(_)) {
|
||||
return false;
|
||||
}
|
||||
// Global layer visibility.
|
||||
if self.document.layers.get(&c.layer)
|
||||
.map(|l| l.flags.off || l.flags.frozen)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Per-viewport frozen layers.
|
||||
if !frozen.is_empty() {
|
||||
if let Some(lh) = self.document.layers.get(&c.layer).map(|l| l.handle) {
|
||||
if frozen.contains(&lh) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.belongs_to_visible_block(c.handle, c.owner_handle, model_block)
|
||||
})
|
||||
.flat_map(|e| self.tessellate_one(e))
|
||||
.collect();
|
||||
// ── Use cached tessellation (model_wires_for_viewport_arc) ────
|
||||
// This eliminates the per-frame tessellate_one() loop that was here
|
||||
// previously; tessellation is now O(1) on navigation frames.
|
||||
let model_wires = self.model_wires_for_viewport_arc(vp_handle);
|
||||
|
||||
// ── Project and clip wires into viewport ──────────────────────
|
||||
let vp_x0 = pcx - hw;
|
||||
|
|
@ -796,10 +805,7 @@ impl Scene {
|
|||
let vp_y0 = pcy - hh;
|
||||
let vp_y1 = pcy + hh;
|
||||
|
||||
// ── Perspective setup ─────────────────────────────────────────
|
||||
// camera_dist: how far the camera is from the target plane.
|
||||
// Derived from lens_length (mm, 35mm-film equiv.) and view_height:
|
||||
// tan(fov_v/2) = 12 / lens_length → camera_dist = view_height * lens_length / 24
|
||||
let use_perspective = vp.status.perspective && vp.lens_length > 1.0;
|
||||
let camera_dist = if use_perspective {
|
||||
(vp.view_height as f32 * vp.lens_length as f32 / 24.0).max(0.001)
|
||||
|
|
@ -807,7 +813,9 @@ impl Scene {
|
|||
0.0
|
||||
};
|
||||
|
||||
for wire in &model_wires {
|
||||
let mut projected: Vec<WireModel> = Vec::new();
|
||||
|
||||
for wire in model_wires.iter() {
|
||||
// Project 3-D model points onto view plane → paper space.
|
||||
let projected_pts: Vec<[f32; 3]> = wire.points.iter().map(|&[mx, my, mz]| {
|
||||
if mx.is_nan() || my.is_nan() || mz.is_nan() {
|
||||
|
|
@ -817,12 +825,9 @@ impl Scene {
|
|||
let u = mp.dot(view_right);
|
||||
let v = mp.dot(view_up);
|
||||
if use_perspective {
|
||||
// Eye direction (target → camera) = rotation * Z.
|
||||
let d_vd = mp.dot(cam_frame.rotation * glam::Vec3::Z);
|
||||
// Forward distance from camera (positive = in front of camera).
|
||||
let fwd = camera_dist - d_vd;
|
||||
if fwd <= 0.001 {
|
||||
// Point is behind or at the camera — discard.
|
||||
return [f32::NAN; 3];
|
||||
}
|
||||
let factor = camera_dist / fwd;
|
||||
|
|
@ -832,14 +837,12 @@ impl Scene {
|
|||
}
|
||||
}).collect();
|
||||
|
||||
// Fast AABB pre-reject: skip entirely if no finite point is
|
||||
// anywhere near the viewport.
|
||||
// Fast AABB pre-reject.
|
||||
let any_near = projected_pts.iter().any(|&[x, y, _]| {
|
||||
x.is_finite() && y.is_finite()
|
||||
&& x >= vp_x0 - 1.0 && x <= vp_x1 + 1.0
|
||||
&& y >= vp_y0 - 1.0 && y <= vp_y1 + 1.0
|
||||
});
|
||||
// Also keep wires whose AABB overlaps the viewport (partial overlap).
|
||||
let (min_x, max_x, min_y, max_y) = projected_pts.iter()
|
||||
.filter(|p| p[0].is_finite())
|
||||
.fold(
|
||||
|
|
@ -854,7 +857,6 @@ impl Scene {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Cohen-Sutherland clipping: clip every segment to the viewport.
|
||||
let clipped = clip_polyline_to_rect(
|
||||
&projected_pts, vp_x0, vp_y0, vp_x1, vp_y1, pcz,
|
||||
);
|
||||
|
|
@ -866,10 +868,14 @@ impl Scene {
|
|||
let mut out = wire.clone();
|
||||
out.points = clipped;
|
||||
out.color = [r * 0.80, g * 0.80, b * 0.80, a * 0.85];
|
||||
// Line weights are paper-space pen widths — independent of viewport scale.
|
||||
out.line_weight_px = wire.line_weight_px;
|
||||
result.push(out);
|
||||
projected.push(out);
|
||||
}
|
||||
|
||||
// Store in cache, then extend result.
|
||||
self.paper_projected_cache.borrow_mut()
|
||||
.insert(vp_handle, (self.geometry_epoch, projected.clone()));
|
||||
result.extend(projected);
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -2287,8 +2293,7 @@ impl Scene {
|
|||
}
|
||||
|
||||
pub(super) fn paper_sheet_wires(&self) -> Vec<WireModel> {
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
self.wires_for_block(layout_block)
|
||||
(*self.paper_sheet_wires_arc()).clone()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue