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.
This commit is contained in:
Hakan Seven committed 2026-07-28 15:22:57 +03:00
1 parent 8a5bca8aa0
commit 73b8dbe9d4
2 files changed
+132 -20

No files matched your search

+53
View File
@@ -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"),
}
}
}
+79 -20
View File
@@ -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<WireInstance>,
csts: Vec<WireConst>,
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<Handle, PreparedPatchRun> = 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<WireInstance> = Vec::new();
let mut csts: Vec<WireConst> = 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<WireInstance> = Vec::new();
let mut csts: Vec<WireConst> = 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
{