From 690971c44e162113c2e41cbaa38397649882d0dc Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Mon, 7 Sep 2026 14:32:55 +0100 Subject: [PATCH] Make tessellation memo and resident wire set zoom-independent With GPU analytical rendering handling circles, arcs, and ellipses, tessellation geometry is now camera- and zoom-independent. - Remove wpp from the tessellation memo guard hash to prevent clearing all cached entities when zooming across quantization bands. - Remove quantized_wpp from resident_wire_key and resident_wires_for so all zoom levels reuse the cached resident set directly with zero CPU re-tessellation. - Fix clippy slice_from_raw_parts lint in dwg-thumbnailer without suppression. - Update zoom test to assert Arc pointer equality across camera zooms. --- crates/dwg-thumbnailer/src/lib.rs | 2 +- src/scene/mod.rs | 66 +++++++++++++------------------ src/scene/paper.rs | 14 ++----- 3 files changed, 33 insertions(+), 49 deletions(-) diff --git a/crates/dwg-thumbnailer/src/lib.rs b/crates/dwg-thumbnailer/src/lib.rs index 736b8b0c0..28deb0399 100644 --- a/crates/dwg-thumbnailer/src/lib.rs +++ b/crates/dwg-thumbnailer/src/lib.rs @@ -259,7 +259,7 @@ pub unsafe extern "C" fn dwg_thumbnail_png( #[no_mangle] pub unsafe extern "C" fn dwg_thumbnail_free(ptr: *mut u8, len: usize) { if !ptr.is_null() && len != 0 { - drop(Box::from_raw(std::slice::from_raw_parts_mut(ptr, len))); + drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len))); } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1aa4c7269..b1aea6480 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2232,7 +2232,6 @@ impl Scene { self.annotation_all_visible(), None, None, - None, ); let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed); self.last_model_wire_gen.set(gen); @@ -5162,9 +5161,9 @@ impl Scene { pub(super) fn model_tile_wires_arc( &self, _tile_idx: usize, - cam: &Camera, + _cam: &Camera, _cam_aspect: f32, - tile_pixel_height: f32, + _tile_pixel_height: f32, ) -> Arc> { // In a BEDIT block editor the resident set is the edited block's own // (block-local) entities; otherwise model space. (#261) @@ -5175,12 +5174,7 @@ impl Scene { &self.document, &self.document.header.current_annotation_scale, ); - let wpp = if tile_pixel_height > 0.0 { - Some((2.0 * cam.ortho_size()) / tile_pixel_height) - } else { - self.world_per_pixel() - }; - self.resident_wires_for(block, None, scale, None, None, wpp) + self.resident_wires_for(block, None, scale, None, None) } /// Unified static-hold wire builder — the ONE tessellation path every @@ -5229,7 +5223,6 @@ impl Scene { annotation_scale_handle: Option, frozen_layers: Option<&HashSet>, style_viewport: Option, - wpp: Option, ) -> Arc> { // Normalize an inert anno override away so distinct viewport scales // share one resident set when annotation can't change the wires. @@ -5246,7 +5239,6 @@ impl Scene { self.paper_bg_color }; let all_visible = self.annotation_all_visible(); - let quantized_wpp = Self::quantize_wpp(wpp); let key = self.resident_wire_key( block, bg, @@ -5255,7 +5247,6 @@ impl Scene { all_visible, frozen_layers, style_viewport, - quantized_wpp, ); { let sets = self.resident_wire_sets.borrow(); @@ -5280,18 +5271,17 @@ impl Scene { all_visible, frozen_layers, style_viewport, - quantized_wpp, ) { return arc; } - // Build once: full tessellation, no cull (region = None), zoom LOD - // bounded by quantized_wpp for every space. + // Build once: full tessellation, no cull, no zoom LOD — the resident + // set is zoom-independent (GPU analytical circles/arcs/ellipses). let t_tess = iced::time::Instant::now(); let mut wires = self.wires_for_block_culled( block, None, - quantized_wpp, + None, frozen_layers, anno_scale_override, annotation_scale_handle, @@ -5361,7 +5351,6 @@ impl Scene { all_visible: bool, frozen_layers: Option<&HashSet>, style_viewport: Option, - quantized_wpp: Option, ) -> u64 { let mut key: u64 = 0xcbf2_9ce4_8422_2325; let mut mix = @@ -5376,9 +5365,9 @@ impl Scene { mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0)); mix(all_visible as u64); mix(self.viewport_style_key(style_viewport)); - mix(quantized_wpp - .map(|w| w.to_bits() as u64) - .unwrap_or(u64::MAX)); + // quantized_wpp removed: GPU analytical rendering for circles/arcs/ + // ellipses makes tessellation zoom-independent, so the resident set is + // shared across all zoom levels. match frozen_layers { Some(frozen) => { let mut signature = 0u64; @@ -5491,7 +5480,6 @@ impl Scene { all_visible: bool, frozen_layers: Option<&HashSet>, style_viewport: Option, - quantized_wpp: Option, ) -> Option>> { if self.viewport_style_key(style_viewport) != 0 { return None; @@ -5575,7 +5563,7 @@ impl Scene { e, Some(&blk), None, - quantized_wpp, + None, anno_scale_override.is_some(), ); memo_updates.push((*h, Arc::new(raw.clone()))); @@ -5857,7 +5845,7 @@ impl Scene { } let layout_block = self.current_layout_block_handle(); let scale = self.paper_annotation_scale_handle(); - let base = self.resident_wires_for(layout_block, None, scale, None, None, None); + let base = self.resident_wires_for(layout_block, None, scale, None, None); let mut wires = (*base).clone(); // The overall "sheet" viewport now IS the paper view itself, so its own // border rectangle must not be drawn as an entity on the sheet. @@ -9112,7 +9100,10 @@ impl Scene { let guard = { let mut g: u64 = 0xcbf2_9ce4_8422_2325; let mut mix = |x: u64| g = g.rotate_left(13) ^ x; - mix(wpp.map(|w| w.to_bits() as u64).unwrap_or(u64::MAX)); + // wpp removed from guard: GPU analytical rendering handles + // circles/arcs/ellipses, Point ignores wpp, and Light (the + // only remaining wpp consumer) is rare enough that its stale + // glyphs don't justify clearing every memoized entity on zoom. if let Some(v) = view_aabb { for c in v { mix(c.to_bits() as u64); @@ -10230,7 +10221,6 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}", scale, None, None, - None, ); let wire_points = wires.iter().flat_map(|wire| wire.key_vertices.iter().copied()); let mesh_points = self.meshes.iter().filter_map(|(&handle, set)| { @@ -11244,7 +11234,6 @@ mod layout_cache_tests { scale, None, None, - None, ); let generation = s.last_model_wire_gen.get(); let bounds = s.model_space_extents().unwrap(); @@ -11260,17 +11249,17 @@ mod layout_cache_tests { } #[test] - fn circle_wires_scale_with_zoom_level() { + fn resident_wires_are_zoom_independent_and_gpu_analytical() { let mut s = Scene::new(); let mut circle = acadrust::entities::Circle::default(); circle.radius = 100.0; let handle = s.add_entity(EntityType::Circle(circle)); - // Far camera: distance is large -> wpp is large + // Far camera: distance is large let mut cam_far = Camera::default(); cam_far.distance = 1000.0; - // Close camera: distance is small -> wpp is small + // Close camera: distance is small let mut cam_close = Camera::default(); cam_close.distance = 1.0; @@ -11279,19 +11268,20 @@ mod layout_cache_tests { .iter() .find(|w| w.name == handle.value().to_string()) .unwrap(); - let far_pts = circle_wire_far.points.len(); let wires_close = s.model_tile_wires_arc(0, &cam_close, 1.0, 1000.0); - let circle_wire_close = wires_close - .iter() - .find(|w| w.name == handle.value().to_string()) - .unwrap(); - let close_pts = circle_wire_close.points.len(); + // Zooming must reuse the exact same resident wire set (zero re-tessellation) assert!( - close_pts > far_pts, - "Close camera points ({close_pts}) should exceed far camera points ({far_pts})" + Arc::ptr_eq(&wires_far, &wires_close), + "Resident wires must be identical Arc across zoom levels" ); - assert!(close_pts > 500, "Close points ({close_pts}) should be > 500"); + // Circle must carry TangentGeom for GPU analytical rendering + assert_eq!(circle_wire_far.tangent_geoms.len(), 1); + assert!(matches!( + circle_wire_far.tangent_geoms[0], + crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. } + | crate::scene::model::wire_model::TangentGeom::Circle { .. } + )); } } diff --git a/src/scene/paper.rs b/src/scene/paper.rs index ed6e75e38..450074122 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -819,7 +819,7 @@ impl Scene { fn model_wires_for_viewport( &self, vp_handle: Handle, - screen_height_px: f32, + _screen_height_px: f32, ) -> Arc> { use rustc_hash::FxHashSet as HSet; @@ -829,17 +829,12 @@ impl Scene { // wheel tick whenever the drawing contained one annotative object. // Explicit viewport annotation-scale changes still rebuild the resident // set; PSLTSCALE is a viewport GPU uniform. - let (frozen, wpp) = match self.document.get_entity(vp_handle) { + let frozen = match self.document.get_entity(vp_handle) { Some(EntityType::Viewport(vp)) => { let f: HSet = vp.frozen_layers.iter().cloned().collect(); - let w = if screen_height_px > 0.0 && vp.view_height > 0.0 { - Some((vp.view_height as f32) / screen_height_px) - } else { - None - }; - (f, w) + f } - _ => (HSet::default(), None), + _ => HSet::default(), }; let scale_handle = self.viewport_scale_handle(vp_handle); @@ -849,7 +844,6 @@ impl Scene { scale_handle, Some(&frozen), Some(vp_handle), - wpp, ) }