From 73b8dbe9d4ec4f86080036c424b8882f128c6a3b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 28 Jul 2026 15:22:57 +0300 Subject: [PATCH] fix(render): prevent live polyline flicker Reject segment-count-changing arena patches before mutating GPU buffers so repeated live updates rebuild from a consistent state. --- src/modules/draw/draw/polyline.rs | 53 +++++++++++++++++ src/scene/pipeline/wire_arena.rs | 99 ++++++++++++++++++++++++------- 2 files changed, 132 insertions(+), 20 deletions(-) diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index d60f3f460..e0ff065d1 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -431,3 +431,56 @@ impl CadCommand for PlineCommand { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["PLINE"] }); // PlineCommand + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preview_contains_only_the_pending_segment() { + let mut command = PlineCommand::new(); + command.on_point(DVec3::new(0.0, 0.0, 0.0)); + command.on_point(DVec3::new(10.0, 0.0, 0.0)); + command.on_point(DVec3::new(10.0, 5.0, 0.0)); + + let preview = command + .on_mouse_move(DVec3::new(15.0, 5.0, 0.0)) + .expect("polyline with vertices should have a preview"); + + assert_eq!( + preview.points, + vec![[10.0, 5.0, 0.0], [15.0, 5.0, 0.0]] + ); + } + + #[test] + fn committed_vertices_update_one_live_polyline() { + let mut command = PlineCommand::new(); + assert!(matches!( + command.on_point(DVec3::new(0.0, 0.0, 0.0)), + CmdResult::NeedPoint + )); + + match command.on_point(DVec3::new(10.0, 0.0, 0.0)) { + CmdResult::CommitLiveEntity(EntityType::LwPolyline(polyline)) => { + assert_eq!(polyline.vertices.len(), 2); + } + _ => panic!("the first segment should create a live polyline"), + } + + let handle = Handle::new(42); + command.set_live_handle(handle); + match command.on_point(DVec3::new(10.0, 5.0, 0.0)) { + CmdResult::UpdateLiveEntity { + handle: updated, + entity: EntityType::LwPolyline(polyline), + finish, + } => { + assert_eq!(updated, handle); + assert_eq!(polyline.vertices.len(), 3); + assert!(!finish); + } + _ => panic!("later segments should update the same live polyline"), + } + } +} diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index f791ff923..cd1960707 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -9,9 +9,9 @@ // // * Modify in place — a move / rotate / scale / colour change keeps the // entity's segment count, so its slab is overwritten where it sits. -// * Add / a Modify whose segment count changed — bump-allocate a fresh slab at -// the tail (tombstone the old one); the instance buffer only grows by that -// entity. +// * Add — bump-allocate a fresh slab at the tail. +// * A Modify whose segment count changed — reject the patch before touching +// GPU state, so the caller can rebuild this wire arena cleanly. // * Erase — tombstone the slab (blank instances that render nothing). // // Two correctness points make add/remove safe: @@ -72,6 +72,14 @@ struct Slab { base_depth: f32, } +struct PreparedPatchRun { + insts: Vec, + csts: Vec, + base_depth: f32, + aabb: [f32; 4], + order_sensitive: bool, +} + pub struct WireArena { inst_buf: wgpu::Buffer, inst_cap: u32, @@ -499,6 +507,65 @@ impl WireArena { let depth_structural = changes .iter() .any(|(_, kind)| !matches!(kind, ChangeKind::Modified)); + + // Prepare and validate every visible run before mutating either GPU + // buffer. Growing/shrinking a Modified slab used to tombstone the old + // range and append a new one. Repeated live-polyline updates could then + // leave the arena half-patched when a later guard requested a rebuild, + // producing alternating old/new submissions. A shape change now takes + // the clean full-arena fallback, while the scene itself still + // re-tessellates only the named entity. + let mut prepared: FxHashMap = FxHashMap::default(); + for &(h, kind) in changes { + let run = runs.get(&h).map(Vec::as_slice).unwrap_or(&[]); + if matches!(kind, ChangeKind::Removed) || run.is_empty() { + continue; + } + + let mut insts: Vec = Vec::new(); + let mut csts: Vec = Vec::new(); + for &w in run { + let wire_id = csts.len() as u32; + let dd = if self.mesh_edge { + 0.0 + } else { + wire_draw_depth(w, depth_map) + }; + let (mut wi, c) = emit_wire_native(w, wire_id, w.color, dd); + insts.append(&mut wi); + csts.push(c); + } + let inst_len = insts.len() as u32; + let const_len = csts.len() as u32; + + if matches!(kind, ChangeKind::Modified) + && self + .slabs + .get(&h) + .or_else(|| self.vacant.get(&h)) + .is_some_and(|slab| { + slab.inst_len != inst_len || slab.const_len != const_len + }) + { + return false; + } + + prepared.insert( + h, + PreparedPatchRun { + insts, + csts, + base_depth: if self.mesh_edge { + 0.0 + } else { + depth_map.get(&h.value()).map_or(0.0, |d| d[0]) + }, + aabb: run_aabb(run), + order_sensitive: order_sensitive(run, depth_map), + }, + ); + } + for &(h, kind) in changes { let run = runs.get(&h).map(Vec::as_slice).unwrap_or(&[]); @@ -520,24 +587,17 @@ impl WireArena { continue; } - // Emit into fresh, run-local const slots (patched to absolute below). - let mut insts: Vec = Vec::new(); - let mut csts: Vec = Vec::new(); - for &w in run { - let wire_id = csts.len() as u32; - let dd = if self.mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) }; - let (mut wi, c) = emit_wire_native(w, wire_id, w.color, dd); - insts.append(&mut wi); - csts.push(c); - } + let PreparedPatchRun { + mut insts, + csts, + base_depth, + aabb, + order_sensitive: run_order_sensitive, + } = prepared + .remove(&h) + .expect("visible wire patch run was prepared"); let inst_len = insts.len() as u32; let const_len = csts.len() as u32; - let base_depth = if self.mesh_edge { - 0.0 - } else { - depth_map.get(&h.value()).map_or(0.0, |d| d[0]) - }; - let aabb = run_aabb(run); if !self.slabs.contains_key(&h) && self @@ -589,7 +649,6 @@ impl WireArena { // back to a full rebuild instead. let is_new = !self.slabs.contains_key(&h); let preserves_submission_order = is_new && new_handles_are_suffix; - let run_order_sensitive = order_sensitive(run, depth_map); if (self.order_sensitive || run_order_sensitive || self.mesh_edge) && !preserves_submission_order {