perf: eliminate dense edit stalls
Keep resident wires, draw depths, GPU slabs, and edit history incremental across entity changes. Defer rollover work to avoid cursor jumps on dense drawings.
This commit is contained in:
parent
ea93e169ea
commit
da34c6817a
11 changed files with 953 additions and 293 deletions
|
|
@ -435,13 +435,16 @@ impl OpenCADStudio {
|
|||
}
|
||||
CmdResult::CommitSolid { entity, solid } => {
|
||||
let label = self.history_label_from_active_cmd(i, "SOLID");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let pending = self.begin_undo(i, label, 1, true);
|
||||
self.add_solid_model(entity, *solid);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitAndEditText(entity) => {
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
|
|
@ -466,7 +469,10 @@ impl OpenCADStudio {
|
|||
edit_index,
|
||||
} => {
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let delta_safe = entities
|
||||
.iter()
|
||||
.all(|entity| self.delta_add_safe(i, entity));
|
||||
let pending = self.begin_undo(i, label, entities.len(), delta_safe);
|
||||
let mut edit_handle = None;
|
||||
let mut leader_handle = None;
|
||||
for (idx, entity) in entities.into_iter().enumerate() {
|
||||
|
|
@ -494,6 +500,9 @@ impl OpenCADStudio {
|
|||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.ribbon.deactivate_tool();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if let Some(h) = edit_handle {
|
||||
return self.begin_text_edit(h);
|
||||
}
|
||||
|
|
@ -533,7 +542,7 @@ impl OpenCADStudio {
|
|||
}
|
||||
CmdResult::CommitHatch(hatch) => {
|
||||
let label = self.history_label_from_active_cmd(i, "HATCH");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let pending = self.begin_undo(i, label, 1, true);
|
||||
let new_handle = self.tabs[i].scene.add_hatch(hatch);
|
||||
if !new_handle.is_null() {
|
||||
self.tabs[i].scene.select_entity(new_handle, true);
|
||||
|
|
@ -544,6 +553,9 @@ impl OpenCADStudio {
|
|||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::BatchCopy(handles, transforms) => {
|
||||
let label = self.history_label_from_active_cmd(i, "ARRAY");
|
||||
|
|
@ -747,13 +759,17 @@ impl OpenCADStudio {
|
|||
.and_then(|c| c.attreq_take_insert());
|
||||
if let Some(entity) = entity {
|
||||
let label = self.history_label_from_active_cmd(i, "INSERT");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
self.commit_entity(entity);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Inject attdefs so the command enters attr-filling mode.
|
||||
|
|
@ -768,11 +784,19 @@ impl OpenCADStudio {
|
|||
}
|
||||
CmdResult::CommitLiveEntity(entity) => {
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
let handle = self.commit_entity_handle(entity);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if let Some(h) = handle {
|
||||
// Keep the live document Arc unique while later vertices
|
||||
// replace the geometry in place. The final Arc is captured
|
||||
// by UpdateLiveEntity when the command completes.
|
||||
self.defer_live_entity_history_after(i, h);
|
||||
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
cmd.set_live_handle(h);
|
||||
}
|
||||
|
|
@ -800,11 +824,13 @@ impl OpenCADStudio {
|
|||
new.as_entity_mut().set_handle(old_handle);
|
||||
new.as_entity_mut().set_layer(layer);
|
||||
*old = new;
|
||||
self.tabs[i].scene.mark_entity_dirty(handle);
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
if finish {
|
||||
self.finish_live_entity_history(i, handle);
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
|
|
@ -819,10 +845,10 @@ impl OpenCADStudio {
|
|||
CmdResult::RemoveLiveEntity(handle) => {
|
||||
// The command backed off below a valid entity (PLINE Undo at
|
||||
// one remaining vertex): take the live entity out of the
|
||||
// document and keep prompting. No undo bookkeeping — the
|
||||
// create's snapshot already covers the whole in-progress
|
||||
// object as one unit.
|
||||
// document, drop its provisional history entry and keep
|
||||
// prompting. A later second point creates one fresh entry.
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
self.discard_last_undo_entry(i);
|
||||
self.tabs[i].dirty = true;
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
|
|
@ -1790,7 +1816,7 @@ impl OpenCADStudio {
|
|||
// ── Solid3D creation (BOX / SPHERE / CYLINDER) ────────────────
|
||||
CmdResult::CommitSolid3D { mesh_fn } => {
|
||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||
self.push_undo_snapshot(i, "SOLID3D");
|
||||
let pending = self.begin_undo(i, "SOLID3D", 1, true);
|
||||
let entity = empty_solid3d();
|
||||
let handle = self.tabs[i].scene.add_entity(entity);
|
||||
if !handle.is_null() {
|
||||
|
|
@ -1806,6 +1832,9 @@ impl OpenCADStudio {
|
|||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output("Solid created.");
|
||||
}
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
|
|
@ -1861,7 +1890,7 @@ impl OpenCADStudio {
|
|||
}
|
||||
});
|
||||
if let Some((mut mesh, solid)) = result {
|
||||
self.push_undo_snapshot(i, "EXTRUDE");
|
||||
let pending = self.begin_undo(i, "EXTRUDE", 1, true);
|
||||
let new_entity = empty_solid3d();
|
||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||
mesh.name = format!("{}", new_handle.value());
|
||||
|
|
@ -1874,6 +1903,9 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.solid_models.insert(new_handle, solid);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output("EXTRUDE: solid created.");
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_error("EXTRUDE: could not build profile. Select a closed 2D entity (Circle, LwPolyline, etc.).");
|
||||
}
|
||||
|
|
@ -1943,7 +1975,7 @@ impl OpenCADStudio {
|
|||
}
|
||||
});
|
||||
if let Some(mut mesh) = result {
|
||||
self.push_undo_snapshot(i, "REVOLVE");
|
||||
let pending = self.begin_undo(i, "REVOLVE", 1, true);
|
||||
let new_entity = empty_solid3d();
|
||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||
mesh.name = format!("{}", new_handle.value());
|
||||
|
|
@ -1954,6 +1986,9 @@ impl OpenCADStudio {
|
|||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(&format!("REVOLVE: solid created ({:.0}°).", angle_deg));
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error("REVOLVE: could not revolve profile.");
|
||||
|
|
@ -2111,7 +2146,7 @@ impl OpenCADStudio {
|
|||
});
|
||||
|
||||
if let Some(mut mesh) = result {
|
||||
self.push_undo_snapshot(i, "SWEEP");
|
||||
let pending = self.begin_undo(i, "SWEEP", 1, true);
|
||||
let new_entity = empty_solid3d();
|
||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||
mesh.name = format!("{}", new_handle.value());
|
||||
|
|
@ -2121,6 +2156,9 @@ impl OpenCADStudio {
|
|||
.insert(new_handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output("SWEEP: solid created.");
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_error("SWEEP: could not sweep profile along path. Use a closed 2D profile and a Line or Polyline path.");
|
||||
}
|
||||
|
|
@ -2200,7 +2238,7 @@ impl OpenCADStudio {
|
|||
);
|
||||
|
||||
if let Some(mut mesh) = result {
|
||||
self.push_undo_snapshot(i, "LOFT");
|
||||
let pending = self.begin_undo(i, "LOFT", 1, true);
|
||||
let new_entity = empty_solid3d();
|
||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||
mesh.name = format!("{}", new_handle.value());
|
||||
|
|
@ -2213,6 +2251,9 @@ impl OpenCADStudio {
|
|||
"LOFT: solid created from {} profiles.",
|
||||
handles.len()
|
||||
));
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_error("LOFT: could not loft profiles. Ensure sections have the same edge count and are compatible.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,46 @@ impl OpenCADStudio {
|
|||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
pub(super) fn defer_live_entity_history_after(&mut self, i: usize, handle: Handle) {
|
||||
let Some(HistorySnapshot::Delta(delta)) =
|
||||
self.tabs[i].history.undo_stack.last_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if let Some((_, _, after)) = delta
|
||||
.entities
|
||||
.iter_mut()
|
||||
.find(|(entry_handle, _, _)| *entry_handle == handle)
|
||||
{
|
||||
*after = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn finish_live_entity_history(&mut self, i: usize, handle: Handle) {
|
||||
let Some(after) = self.tabs[i].scene.document.get_entity_arc(handle) else {
|
||||
return;
|
||||
};
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let current_layout_after = self.tabs[i].scene.current_layout.clone();
|
||||
let dirty_after = self.tabs[i].dirty;
|
||||
let Some(HistorySnapshot::Delta(delta)) =
|
||||
self.tabs[i].history.undo_stack.last_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some((_, _, entry_after)) = delta
|
||||
.entities
|
||||
.iter_mut()
|
||||
.find(|(entry_handle, _, _)| *entry_handle == handle)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
*entry_after = Some(after);
|
||||
delta.selected_after = selected_after;
|
||||
delta.current_layout_after = current_layout_after;
|
||||
delta.dirty_after = dirty_after;
|
||||
}
|
||||
|
||||
pub(super) fn discard_last_undo_entry(&mut self, i: usize) {
|
||||
if self.tabs[i].history.pending.take().is_some() {
|
||||
self.tabs[i].scene.document.end_entity_change_recording();
|
||||
|
|
@ -296,16 +336,15 @@ impl OpenCADStudio {
|
|||
}
|
||||
|
||||
/// Add is delta-safe only for a plain drawable on an already-existing layer:
|
||||
/// an insert / block / image / dimension add also creates block records,
|
||||
/// image definitions or layers (non-entity state).
|
||||
/// a block / image add also creates block records, image definitions or
|
||||
/// layers (non-entity state). INSERT only references an existing block and
|
||||
/// is entity-delta safe.
|
||||
pub(super) fn delta_add_safe(&self, i: usize, entity: &EntityType) -> bool {
|
||||
if matches!(
|
||||
entity,
|
||||
EntityType::Insert(_)
|
||||
| EntityType::Block(_)
|
||||
EntityType::Block(_)
|
||||
| EntityType::BlockEnd(_)
|
||||
| EntityType::RasterImage(_)
|
||||
| EntityType::Dimension(_)
|
||||
// A viewport commit routes through add_entity_to_layout +
|
||||
// bump_geometry_no_blocks, bypassing the recorded Scene::add_entity
|
||||
// — nothing would be captured, so keep the full snapshot.
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ pub struct HoverDwell {
|
|||
}
|
||||
|
||||
/// How long the cursor must sit still before the idle rollover pick runs.
|
||||
pub const HOVER_DWELL_MS: u128 = 120;
|
||||
pub const HOVER_DWELL_MS: u128 = 500;
|
||||
/// Dense resident sets also retain the previous rollover while moving; this
|
||||
/// threshold gates that extra redraw-avoidance behavior.
|
||||
pub const HOVER_DWELL_DENSE_WIRES: usize = 50_000;
|
||||
|
||||
/// Open multi-functional-grip popup state.
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
|
|||
|
|
@ -518,8 +518,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
self.tabs[i].scene.hidden.remove(&h);
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
// Geometry restored to the backup — re-tessellate just it.
|
||||
self.tabs[i].scene.mark_entity_dirty(h);
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||
self.refresh_selected_grips();
|
||||
}
|
||||
self.tabs[self.active_tab].snap_result = None;
|
||||
|
|
|
|||
|
|
@ -496,6 +496,8 @@ impl OpenCADStudio {
|
|||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let perf_move = std::env::var_os("OCS_PERF").is_some();
|
||||
let move_started = Instant::now();
|
||||
|
||||
// UCS icon grip drag: map the cursor onto the UCS plane and
|
||||
// slide the origin / rotate the axis. Short-circuits pan & snap.
|
||||
|
|
@ -694,6 +696,7 @@ impl OpenCADStudio {
|
|||
|
||||
// ── Grip drag ─────────────────────────────────────────────
|
||||
if let Some(grip) = self.tabs[i].active_grip.clone() {
|
||||
let grip_started = Instant::now();
|
||||
let (vw, vh) = vp_size;
|
||||
let bounds = iced::Rectangle {
|
||||
x: 0.0,
|
||||
|
|
@ -734,9 +737,11 @@ impl OpenCADStudio {
|
|||
// Back up the original geometry so Esc can cancel the drag.
|
||||
self.grip_original = self.tabs[i].scene.document.get_entity(grip.handle).cloned();
|
||||
self.tabs[i].scene.hidden.insert(grip.handle);
|
||||
// Grip drag never changes a block definition — keep the
|
||||
// block cache so the hide doesn't re-tessellate blocks.
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
// Hiding changes exactly one resident run. Publishing a full
|
||||
// delta here made the first grip move rebuild every wire.
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(grip.handle, crate::scene::ChangeKind::Modified)]);
|
||||
self.grip_preview_handle = Some(grip.handle);
|
||||
// Snapshot the entity's glyph quads once so each move can
|
||||
// slide the already-shaped text rather than re-shaping it
|
||||
|
|
@ -760,6 +765,8 @@ impl OpenCADStudio {
|
|||
&& snap.iter().all(|w| w.points.is_empty())
|
||||
&& square_grip;
|
||||
}
|
||||
let setup_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let snap_started = Instant::now();
|
||||
|
||||
// The edited entity is hidden, so it's already absent from
|
||||
// `hit_test_wires` — snap against the set directly, no clone
|
||||
|
|
@ -833,6 +840,8 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
|
||||
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let apply_started = Instant::now();
|
||||
let apply = if grip.is_translate {
|
||||
GripApply::Translate(snapped - grip.last_world)
|
||||
} else {
|
||||
|
|
@ -843,6 +852,8 @@ impl OpenCADStudio {
|
|||
.apply_grip(grip.handle, grip.grip_id, apply);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped;
|
||||
let apply_ms = apply_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let preview_started = Instant::now();
|
||||
// Overlay the moved entity (hidden from the base). Pure text
|
||||
// moved as a whole slides its drag-start glyphs — no re-shaping
|
||||
// and no wire re-tess. Anything else (wire geometry, or a point
|
||||
|
|
@ -865,8 +876,25 @@ impl OpenCADStudio {
|
|||
let preview = self.tabs[i].scene.wire_models_for(&[grip.handle]);
|
||||
self.tabs[i].scene.set_preview_wires(preview);
|
||||
}
|
||||
let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let geometry_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
||||
self.refresh_selected_grips();
|
||||
self.refresh_properties();
|
||||
let grips_ms = grip_started.elapsed().as_secs_f64() * 1000.0 - geometry_ms;
|
||||
// Properties are refreshed when the grip is committed or cancelled.
|
||||
// Rebuilding the inspector on every pointer event adds no drawing
|
||||
// value and can monopolize the UI thread during a drag.
|
||||
let total_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf_move && total_ms >= 50.0 {
|
||||
eprintln!(
|
||||
"[perf] grip-move {:>7.1}ms setup={:.1} snap={:.1} apply={:.1} preview={:.1} grips={:.1}",
|
||||
total_ms,
|
||||
setup_ms,
|
||||
snap_ms,
|
||||
apply_ms,
|
||||
preview_ms,
|
||||
grips_ms,
|
||||
);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
|
|
@ -888,11 +916,17 @@ impl OpenCADStudio {
|
|||
// drag), defer the pick until the cursor stops. The full
|
||||
// pick (wires + hatches + block hatches + shaded meshes) is
|
||||
// O(N) per frame and stalls the cursor on large drawings,
|
||||
// so each move clears the current highlight and resets the
|
||||
// dwell timer — `HoverDwellTick` runs the hit-test only
|
||||
// so each move resets the dwell timer — `HoverDwellTick` runs the hit-test only
|
||||
// once the cursor has been still for `HOVER_DWELL_MS`.
|
||||
if !dragging && self.tabs[i].active_cmd.is_none() {
|
||||
self.tabs[i].scene.set_hover_highlight(None);
|
||||
// On dense drawings, clearing a rollover immediately schedules a
|
||||
// second full scene frame just as motion resumes. Keep the previous
|
||||
// highlight until the next settled pick replaces it.
|
||||
if self.tabs[i].scene.last_tess_wires.get()
|
||||
< crate::app::HOVER_DWELL_DENSE_WIRES
|
||||
{
|
||||
self.tabs[i].scene.set_hover_highlight(None);
|
||||
}
|
||||
self.hover_dwell = Some(crate::app::HoverDwell {
|
||||
last_move_at: Instant::now(),
|
||||
point: p,
|
||||
|
|
@ -1417,6 +1451,13 @@ impl OpenCADStudio {
|
|||
}
|
||||
|
||||
self.sync_dyn_fields();
|
||||
let move_ms = move_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf_move && self.tabs[i].active_cmd.is_some() && move_ms >= 50.0 {
|
||||
eprintln!(
|
||||
"[perf] pointer-move {:>7.1}ms",
|
||||
move_ms,
|
||||
);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
@ -3410,13 +3451,16 @@ impl OpenCADStudio {
|
|||
let Some(dwell) = self.hover_dwell.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
let dwell_ms = crate::app::HOVER_DWELL_MS;
|
||||
if Instant::now()
|
||||
.duration_since(dwell.last_move_at)
|
||||
.as_millis()
|
||||
< crate::app::HOVER_DWELL_MS
|
||||
< dwell_ms
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
let hover_started = Instant::now();
|
||||
let i = dwell.tab;
|
||||
// Re-check the gate — drag / command may have started
|
||||
// between the move that armed the dwell and this tick.
|
||||
|
|
@ -3493,6 +3537,15 @@ impl OpenCADStudio {
|
|||
});
|
||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||
self.hover_dwell = None;
|
||||
let hover_ms = hover_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf && hover_ms >= 5.0 {
|
||||
eprintln!(
|
||||
"[perf] hover-dwell {:>7.1}ms wires={} hit={}",
|
||||
hover_ms,
|
||||
self.tabs[i].scene.last_tess_wires.get(),
|
||||
hovered.is_some(),
|
||||
);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,12 @@ impl Scene {
|
|||
&entity,
|
||||
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
|
||||
);
|
||||
// INSERT invalidates rendered block instances, but it does not mutate
|
||||
// the referenced block definition. Only block sentinels require a
|
||||
// structure image for undo; ordinary owner membership is intrinsic add
|
||||
// bookkeeping and remains in place while an entity delta is undone.
|
||||
let mutates_block_structure =
|
||||
matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
|
||||
let hatch_seed = if let EntityType::Hatch(dxf) = &entity {
|
||||
let color = self.render_style(&entity).0;
|
||||
Self::hatch_model_from_dxf(dxf, color)
|
||||
|
|
@ -208,7 +214,7 @@ impl Scene {
|
|||
// pure-entity delta would be incomplete.
|
||||
if self.is_recording_undo() {
|
||||
self.record_undo_before(handle, None);
|
||||
if creates_layer || affects_blocks || is_image {
|
||||
if creates_layer || mutates_block_structure || is_image {
|
||||
self.poison_undo_recording();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
630
src/scene/mod.rs
630
src/scene/mod.rs
|
|
@ -216,6 +216,55 @@ struct GeometryDelta {
|
|||
full: bool,
|
||||
}
|
||||
|
||||
/// Mutable assembly metadata for one resident wire set. Keeping entity ranges
|
||||
/// beside the flat render Vec lets a one-entity edit splice that run directly;
|
||||
/// rebuilding this directory by grouping every WireModel was the dominant
|
||||
/// edit-time CPU cost on dense drawings.
|
||||
struct ResidentWireSet {
|
||||
epoch: u64,
|
||||
gen: u64,
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
layout: Option<ResidentWireLayout>,
|
||||
}
|
||||
|
||||
struct ResidentWireLayout {
|
||||
/// Entity handles in final submission order. A temporarily hidden entity
|
||||
/// remains here with no range so grip commit can restore it in place.
|
||||
order: Vec<Handle>,
|
||||
/// Flat wire range `(start, len)` for each currently visible entity.
|
||||
ranges: HashMap<Handle, (usize, usize)>,
|
||||
/// First synthesized marker wire. Markers have no entity handle and remain
|
||||
/// at the tail while entity runs are inserted/removed before them.
|
||||
marker_start: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WireGpuPatch {
|
||||
pub(crate) changes: Arc<Vec<(Handle, ChangeKind)>>,
|
||||
pub(crate) runs: Arc<HashMap<Handle, Arc<Vec<WireModel>>>>,
|
||||
pub(crate) index_edits: Arc<Vec<WireIndexEdit>>,
|
||||
pub(crate) new_handles_are_suffix: bool,
|
||||
/// Whether Face3D / generic fill buffers can differ after this patch.
|
||||
pub(crate) face_pass_changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct WireIndexEdit {
|
||||
pub(crate) handle: Handle,
|
||||
pub(crate) start: usize,
|
||||
pub(crate) old_len: usize,
|
||||
pub(crate) new_len: usize,
|
||||
}
|
||||
|
||||
struct DrawDepthCache {
|
||||
epoch: u64,
|
||||
depths: Arc<HashMap<u64, [f32; 2]>>,
|
||||
/// Per-block handles already sorted by effective draw-order key.
|
||||
blocks: HashMap<Handle, Vec<(u64, u64)>>,
|
||||
/// Reverse owner lookup retained so Removed deltas need no document scan.
|
||||
owners: HashMap<u64, Handle>,
|
||||
}
|
||||
|
||||
/// Bound on the geometry-delta ring. A consumer that fell more than this many
|
||||
/// mutations behind (or predates the oldest retained delta) can't be replayed
|
||||
/// and does a one-time full rebuild — the safe fallback, not a correctness hole.
|
||||
|
|
@ -1021,7 +1070,7 @@ pub struct Scene {
|
|||
/// sub-range a block INSERT's children may occupy without crossing the
|
||||
/// insert's siblings. Fill explosion and band wires compose child depths
|
||||
/// as `depth + child_rank * half`.
|
||||
draw_depth_cache: RefCell<Option<(u64, Arc<HashMap<u64, [f32; 2]>>)>>,
|
||||
draw_depth_cache: RefCell<Option<DrawDepthCache>>,
|
||||
/// Cached hatch fill models, keyed by geometry_epoch. View culling
|
||||
/// is handled at draw time via `hatch_skip_flags` in the pipeline,
|
||||
/// not at build time — that lets the GPU buffer stay stable across
|
||||
|
|
@ -1194,7 +1243,7 @@ pub struct Scene {
|
|||
/// camera-keyed re-tessellation anywhere. Rebuilt per key when
|
||||
/// `geometry_epoch` changes; stale entries evicted on insert.
|
||||
#[allow(clippy::type_complexity)]
|
||||
resident_wire_sets: RefCell<HashMap<u64, (u64, u64, Arc<Vec<WireModel>>)>>,
|
||||
resident_wire_sets: RefCell<HashMap<u64, ResidentWireSet>>,
|
||||
/// Memoized `(face3d, other)` split of a resident wire set, keyed by its
|
||||
/// [`WIRE_CONTENT_GEN`] id. `split_face3d_wires` is an O(N) per-wire
|
||||
/// handle lookup + clone that otherwise re-runs every frame. A map, not a
|
||||
|
|
@ -1249,7 +1298,7 @@ pub struct Scene {
|
|||
/// those entities' instance slabs instead of re-uploading every wire. The
|
||||
/// render layer matches `new_gen` against the viewport's content id and
|
||||
/// `prev_gen` against what the GPU currently holds; a mismatch just rebuilds.
|
||||
model_wire_gpu_patch: RefCell<Option<(u64, u64, Arc<Vec<(Handle, ChangeKind)>>)>>,
|
||||
model_wire_gpu_patch: RefCell<Option<(u64, u64, Arc<WireGpuPatch>)>>,
|
||||
/// Active delta-undo recording, or `None` when no entity-only command is
|
||||
/// capturing. Populated by the five mutation primitives via
|
||||
/// [`Scene::record_undo_before`]; consumed by the app (`take_undo_recording`)
|
||||
|
|
@ -3121,10 +3170,10 @@ impl Scene {
|
|||
};
|
||||
{
|
||||
let sets = self.resident_wire_sets.borrow();
|
||||
if let Some((epoch, gen, arc)) = sets.get(&key) {
|
||||
if *epoch == self.geometry_epoch {
|
||||
self.last_model_wire_gen.set(*gen);
|
||||
return Arc::clone(arc);
|
||||
if let Some(set) = sets.get(&key) {
|
||||
if set.epoch == self.geometry_epoch {
|
||||
self.last_model_wire_gen.set(set.gen);
|
||||
return Arc::clone(&set.wires);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3149,6 +3198,7 @@ impl Scene {
|
|||
self.append_scene_markers(&mut wires, bg);
|
||||
}
|
||||
self.apply_refedit_fade(&mut wires, bg);
|
||||
let layout = Self::resident_wire_layout(&wires);
|
||||
self.last_tess_ms
|
||||
.set(t_tess.elapsed().as_secs_f32() * 1000.0);
|
||||
self.last_tess_wires.set(wires.len());
|
||||
|
|
@ -3164,11 +3214,19 @@ impl Scene {
|
|||
// Evict stale entries (older epochs / abandoned keys) so switching
|
||||
// spaces or re-scaling a viewport can't accumulate dead full sets.
|
||||
let cur_epoch = self.geometry_epoch;
|
||||
sets.retain(|_, (e, ..)| *e == cur_epoch);
|
||||
sets.retain(|_, set| set.epoch == cur_epoch);
|
||||
if sets.len() > 8 {
|
||||
sets.clear();
|
||||
}
|
||||
sets.insert(key, (cur_epoch, gen, Arc::clone(&arc)));
|
||||
sets.insert(
|
||||
key,
|
||||
ResidentWireSet {
|
||||
epoch: cur_epoch,
|
||||
gen,
|
||||
wires: Arc::clone(&arc),
|
||||
layout,
|
||||
},
|
||||
);
|
||||
arc
|
||||
}
|
||||
|
||||
|
|
@ -3180,13 +3238,63 @@ impl Scene {
|
|||
pub(crate) fn model_wire_patch_for(
|
||||
&self,
|
||||
gen: u64,
|
||||
) -> Option<(u64, Arc<Vec<(Handle, ChangeKind)>>)> {
|
||||
) -> Option<(u64, Arc<WireGpuPatch>)> {
|
||||
match &*self.model_wire_gpu_patch.borrow() {
|
||||
Some((prev, new, changes)) if *new == gen => Some((*prev, Arc::clone(changes))),
|
||||
Some((prev, new, patch)) if *new == gen => Some((*prev, Arc::clone(patch))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resident_wire_layout(wires: &[WireModel]) -> Option<ResidentWireLayout> {
|
||||
let mut order = Vec::new();
|
||||
let mut ranges = HashMap::default();
|
||||
let mut current: Option<(Handle, usize)> = None;
|
||||
let mut marker_start = wires.len();
|
||||
|
||||
for (index, wire) in wires.iter().enumerate() {
|
||||
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
|
||||
if let Some((handle, start)) = current.take() {
|
||||
if ranges.insert(handle, (start, index - start)).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
marker_start = index;
|
||||
if wires[index..]
|
||||
.iter()
|
||||
.any(|tail| Self::handle_from_wire_name(&tail.name).is_some())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
break;
|
||||
};
|
||||
if current.map(|(h, _)| h) != Some(handle) {
|
||||
if let Some((previous, start)) = current.take() {
|
||||
if ranges.insert(previous, (start, index - start)).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if ranges.contains_key(&handle) {
|
||||
return None;
|
||||
}
|
||||
order.push(handle);
|
||||
current = Some((handle, index));
|
||||
}
|
||||
}
|
||||
if let Some((handle, start)) = current {
|
||||
if ranges
|
||||
.insert(handle, (start, marker_start - start))
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(ResidentWireLayout {
|
||||
order,
|
||||
ranges,
|
||||
marker_start,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bring the resident set for `key` up to the current epoch by replaying only
|
||||
/// the changed entities into the cached assembly. Returns the patched `Arc`,
|
||||
/// or `None` to fall back to a full rebuild — the safe default whenever the
|
||||
|
|
@ -3204,62 +3312,41 @@ impl Scene {
|
|||
anno_scale_override: Option<f32>,
|
||||
frozen_layers: Option<&HashSet<Handle>>,
|
||||
) -> Option<Arc<Vec<WireModel>>> {
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
let t_patch = iced::time::Instant::now();
|
||||
// The entry must exist, be stale, and be uniquely held so we can move
|
||||
// its wires out rather than deep-clone them.
|
||||
let cached_epoch = {
|
||||
let sets = self.resident_wire_sets.borrow();
|
||||
let entry = sets.get(&key)?;
|
||||
if entry.0 == self.geometry_epoch || Arc::strong_count(&entry.2) != 1 {
|
||||
if entry.epoch == self.geometry_epoch || entry.layout.is_none() {
|
||||
return None;
|
||||
}
|
||||
entry.0
|
||||
let strong = Arc::strong_count(&entry.wires);
|
||||
if strong != 1 {
|
||||
if perf {
|
||||
eprintln!("[perf] resident-shared strong={strong}");
|
||||
}
|
||||
return None;
|
||||
}
|
||||
entry.epoch
|
||||
};
|
||||
let deltas = self.replay_since(cached_epoch)?;
|
||||
|
||||
// Take ownership of the cached assembly (guaranteed unique above).
|
||||
// `prev_gen` is the content id the GPU currently holds for this set — the
|
||||
// base the wire-arena patch replays from.
|
||||
let (prev_gen, owned) = {
|
||||
let (prev_gen, mut owned, mut layout) = {
|
||||
let removed = self.resident_wire_sets.borrow_mut().remove(&key)?;
|
||||
(removed.1, Arc::try_unwrap(removed.2).ok()?)
|
||||
(
|
||||
removed.gen,
|
||||
Arc::try_unwrap(removed.wires).ok()?,
|
||||
removed.layout?,
|
||||
)
|
||||
};
|
||||
|
||||
// Group into per-entity runs. Bail if any wire isn't named with a handle
|
||||
// or an entity's wires aren't contiguous — the from-scratch sort puts
|
||||
// each entity's wires in one contiguous run, so a violation means our
|
||||
// move-and-reassemble model doesn't hold and we must full-rebuild.
|
||||
let mut old_runs: HashMap<Handle, Vec<WireModel>> = HashMap::default();
|
||||
// The assembled draw order (one entry per entity, in the order they were
|
||||
// laid out). Lets an all-Modified edit — which never reorders — reassemble
|
||||
// by walking just these (~10k) handles instead of the whole document.
|
||||
let mut old_order: Vec<Handle> = Vec::new();
|
||||
{
|
||||
let mut cur: Option<Handle> = None;
|
||||
let mut run: Vec<WireModel> = Vec::new();
|
||||
for w in owned {
|
||||
let h = Self::handle_from_wire_name(&w.name)?;
|
||||
if Some(h) != cur {
|
||||
if let Some(ph) = cur.take() {
|
||||
if old_runs.insert(ph, std::mem::take(&mut run)).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
cur = Some(h);
|
||||
old_order.push(h);
|
||||
}
|
||||
run.push(w);
|
||||
}
|
||||
if let Some(ph) = cur {
|
||||
if old_runs.insert(ph, run).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if layout.marker_start > owned.len() {
|
||||
return None;
|
||||
}
|
||||
// An all-Modified edit keeps the layout (no entity added / removed), so
|
||||
// the draw order is exactly `old_order` — skip the whole-document scan.
|
||||
let structural = deltas
|
||||
.iter()
|
||||
.any(|(_, k)| !matches!(k, ChangeKind::Modified));
|
||||
|
||||
// Re-tessellate the changed entities, replicating the full build's exact
|
||||
// context (anno derivation, empty selection, no cull / no LOD). Faded
|
||||
|
|
@ -3276,10 +3363,8 @@ impl Scene {
|
|||
let empty_sel: HashSet<Handle> = HashSet::default();
|
||||
let mut new_runs: HashMap<Handle, Vec<WireModel>> = HashMap::default();
|
||||
let mut memo_updates: Vec<(Handle, Arc<Vec<WireModel>>)> = Vec::new();
|
||||
let mut visible_changed: HashSet<Handle> = HashSet::default();
|
||||
for (h, kind) in &deltas {
|
||||
// A changed entity's old run never carries over; unchanged entities
|
||||
// are the only ones left in old_runs after this.
|
||||
old_runs.remove(h);
|
||||
if matches!(kind, ChangeKind::Removed) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -3289,6 +3374,7 @@ impl Scene {
|
|||
if !self.resident_entity_visible(e, block, frozen_layers) {
|
||||
continue;
|
||||
}
|
||||
visible_changed.insert(*h);
|
||||
let raw = tessellate_entity(
|
||||
&self.document,
|
||||
&empty_sel,
|
||||
|
|
@ -3313,51 +3399,123 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
|
||||
// Reassemble, then apply the identical draw-order sort
|
||||
// wires_for_block_culled uses. Emission order IS the final order when the
|
||||
// block has no SortEntitiesTable (the sort below no-ops), so it must match
|
||||
// a from-scratch build: document order. An all-Modified edit never reorders
|
||||
// — reuse the recorded `old_order` (~10k handles) instead of walking the
|
||||
// whole document (~800k). Add/Remove change positions, so fall back to the
|
||||
// document scan. Either way a leftover run (a kept handle we couldn't
|
||||
// place) means an inconsistency we don't patch.
|
||||
let mut new_vec: Vec<WireModel> = Vec::new();
|
||||
if structural {
|
||||
for e in self.document.entities() {
|
||||
let h = e.common().handle;
|
||||
if let Some(run) = new_runs.remove(&h) {
|
||||
new_vec.extend(run);
|
||||
} else if let Some(run) = old_runs.remove(&h) {
|
||||
new_vec.extend(run);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for h in old_order {
|
||||
if let Some(run) = new_runs.remove(&h) {
|
||||
new_vec.extend(run);
|
||||
} else if let Some(run) = old_runs.remove(&h) {
|
||||
new_vec.extend(run);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !new_runs.is_empty() || !old_runs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Keep the submission-order directory current. Adds are inserted by
|
||||
// the same effective SortEntitiesTable key used by the full builder;
|
||||
// hidden Modified entities retain a zero-length placeholder so grip
|
||||
// commit restores them at their original position.
|
||||
{
|
||||
let cache = self.sort_cache.borrow();
|
||||
if let Some((_, ref idx)) = *cache {
|
||||
if let Some(sort_map) = idx.get(&block) {
|
||||
new_vec.sort_by_key(|w| {
|
||||
let key = Self::handle_from_wire_name(&w.name)
|
||||
.map(|h| h.value())
|
||||
.unwrap_or(u64::MAX);
|
||||
sort_map.get(&key).copied().unwrap_or(key)
|
||||
});
|
||||
let sort_map = cache
|
||||
.as_ref()
|
||||
.and_then(|(_, index)| index.get(&block));
|
||||
let effective = |handle: Handle| {
|
||||
sort_map
|
||||
.and_then(|map| map.get(&handle.value()))
|
||||
.copied()
|
||||
.unwrap_or(handle.value())
|
||||
};
|
||||
for &(handle, kind) in &deltas {
|
||||
if matches!(kind, ChangeKind::Removed) {
|
||||
layout.order.retain(|&h| h != handle);
|
||||
continue;
|
||||
}
|
||||
if visible_changed.contains(&handle) && !layout.order.contains(&handle) {
|
||||
let key = effective(handle);
|
||||
let position = layout
|
||||
.order
|
||||
.partition_point(|&existing| effective(existing) <= key);
|
||||
layout.order.insert(position, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arc = Arc::new(new_vec);
|
||||
// Splice only changed runs into the uniquely-owned flat Vec. Same-size
|
||||
// edits overwrite in place. Grip hide/show shifts shallow WireModel
|
||||
// structs once, instead of cloning/grouping all nested geometry.
|
||||
let mut gpu_runs: HashMap<Handle, Arc<Vec<WireModel>>> = HashMap::default();
|
||||
let mut index_edits = Vec::new();
|
||||
for &(handle, kind) in &deltas {
|
||||
let new_run = if matches!(kind, ChangeKind::Removed) {
|
||||
Vec::new()
|
||||
} else {
|
||||
new_runs.remove(&handle).unwrap_or_default()
|
||||
};
|
||||
gpu_runs.insert(handle, Arc::new(new_run.clone()));
|
||||
let new_len = new_run.len();
|
||||
let old_range = layout.ranges.remove(&handle);
|
||||
if old_range.is_none() && new_len == 0 {
|
||||
continue;
|
||||
}
|
||||
let (start, old_len) = match old_range {
|
||||
Some(range) => range,
|
||||
None => {
|
||||
let order_index = layout.order.iter().position(|&h| h == handle)?;
|
||||
let start = layout.order[order_index + 1..]
|
||||
.iter()
|
||||
.find_map(|next| layout.ranges.get(next).map(|range| range.0))
|
||||
.unwrap_or(layout.marker_start);
|
||||
(start, 0)
|
||||
}
|
||||
};
|
||||
if start + old_len > layout.marker_start || layout.marker_start > owned.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if old_len == new_len {
|
||||
for (slot, wire) in owned[start..start + old_len]
|
||||
.iter_mut()
|
||||
.zip(new_run)
|
||||
{
|
||||
*slot = wire;
|
||||
}
|
||||
} else {
|
||||
index_edits.push(WireIndexEdit {
|
||||
handle,
|
||||
start,
|
||||
old_len,
|
||||
new_len,
|
||||
});
|
||||
owned.splice(start..start + old_len, new_run);
|
||||
let delta = new_len as isize - old_len as isize;
|
||||
for range in layout.ranges.values_mut() {
|
||||
if range.0 >= start + old_len {
|
||||
range.0 = (range.0 as isize + delta) as usize;
|
||||
}
|
||||
}
|
||||
layout.marker_start = (layout.marker_start as isize + delta) as usize;
|
||||
}
|
||||
if new_len != 0 {
|
||||
layout.ranges.insert(handle, (start, new_len));
|
||||
}
|
||||
}
|
||||
if !new_runs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let added: HashSet<Handle> = deltas
|
||||
.iter()
|
||||
.filter_map(|&(handle, kind)| {
|
||||
matches!(kind, ChangeKind::Added).then_some(handle)
|
||||
})
|
||||
.collect();
|
||||
let mut saw_added = false;
|
||||
let mut new_handles_are_suffix = true;
|
||||
for handle in &layout.order {
|
||||
if added.contains(handle) {
|
||||
saw_added = true;
|
||||
} else if saw_added && layout.ranges.contains_key(handle) {
|
||||
new_handles_are_suffix = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let face_pass_changed = deltas.iter().any(|&(handle, kind)| {
|
||||
matches!(kind, ChangeKind::Removed)
|
||||
|| matches!(self.document.get_entity(handle), Some(EntityType::Face3D(_)))
|
||||
|| gpu_runs.get(&handle).is_some_and(|run| {
|
||||
run.iter().any(|wire| !wire.fill_tris.is_empty())
|
||||
})
|
||||
});
|
||||
|
||||
let arc = Arc::new(owned);
|
||||
self.last_tess_wires.set(arc.len());
|
||||
self.resident_patch_hits
|
||||
.set(self.resident_patch_hits.get() + 1);
|
||||
|
|
@ -3367,13 +3525,38 @@ impl Scene {
|
|||
// those entities' slabs. Only for the Model set (the arena is model-only);
|
||||
// the render layer verifies prev_gen against what the GPU actually holds.
|
||||
if wire_gpu_patch_enabled() && block == self.model_space_block_handle() {
|
||||
*self.model_wire_gpu_patch.borrow_mut() =
|
||||
Some((prev_gen, gen, Arc::new(deltas.clone())));
|
||||
*self.model_wire_gpu_patch.borrow_mut() = Some((
|
||||
prev_gen,
|
||||
gen,
|
||||
Arc::new(WireGpuPatch {
|
||||
changes: Arc::new(deltas.clone()),
|
||||
runs: Arc::new(gpu_runs),
|
||||
index_edits: Arc::new(index_edits),
|
||||
new_handles_are_suffix,
|
||||
face_pass_changed,
|
||||
}),
|
||||
));
|
||||
}
|
||||
let cur_epoch = self.geometry_epoch;
|
||||
let mut sets = self.resident_wire_sets.borrow_mut();
|
||||
sets.retain(|_, (e, ..)| *e == cur_epoch);
|
||||
sets.insert(key, (cur_epoch, gen, Arc::clone(&arc)));
|
||||
sets.retain(|_, set| set.epoch == cur_epoch);
|
||||
sets.insert(
|
||||
key,
|
||||
ResidentWireSet {
|
||||
epoch: cur_epoch,
|
||||
gen,
|
||||
wires: Arc::clone(&arc),
|
||||
layout: Some(layout),
|
||||
},
|
||||
);
|
||||
if perf {
|
||||
eprintln!(
|
||||
"[perf] resident-patch {:>7.1}ms wires={} changes={}",
|
||||
t_patch.elapsed().as_secs_f64() * 1000.0,
|
||||
arc.len(),
|
||||
deltas.len(),
|
||||
);
|
||||
}
|
||||
Some(arc)
|
||||
}
|
||||
|
||||
|
|
@ -3469,33 +3652,105 @@ impl Scene {
|
|||
/// entities of different types order correctly against each other.
|
||||
pub(super) fn draw_depth_map(&self) -> Arc<HashMap<u64, [f32; 2]>> {
|
||||
{
|
||||
// Draw order depends on each block's entity COUNT (the rank
|
||||
// denominator) and the entities' handles / SortEntitiesTable — none of
|
||||
// which a Modify (move / rotate / scale / colour / hide) touches. So an
|
||||
// all-Modified edit keeps the map; only Add / Remove (count change) or
|
||||
// a table change (which arrives as a `full` delta) rebuilds it.
|
||||
let reuse = {
|
||||
let cache = self.draw_depth_cache.borrow();
|
||||
match *cache {
|
||||
Some((epoch, ref arc))
|
||||
if epoch == self.geometry_epoch
|
||||
|| matches!(
|
||||
self.replay_since(epoch),
|
||||
Some(ref d) if d.iter().all(|&(_, k)| k == ChangeKind::Modified)
|
||||
) =>
|
||||
{
|
||||
Some(Arc::clone(arc))
|
||||
}
|
||||
_ => None,
|
||||
let cache = self.draw_depth_cache.borrow();
|
||||
if let Some(cache) = cache.as_ref() {
|
||||
if cache.epoch == self.geometry_epoch {
|
||||
return Arc::clone(&cache.depths);
|
||||
}
|
||||
};
|
||||
if let Some(arc) = reuse {
|
||||
if let Some((ref mut e, _)) = *self.draw_depth_cache.borrow_mut() {
|
||||
*e = self.geometry_epoch;
|
||||
}
|
||||
}
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
let t_depth = iced::time::Instant::now();
|
||||
|
||||
// Replay Add/Remove into the retained block order. This avoids rescanning
|
||||
// every document entity and re-sorting every block for one new LINE.
|
||||
let stale_cache = self.draw_depth_cache.borrow_mut().take();
|
||||
if let Some(mut cache) = stale_cache {
|
||||
if let Some(deltas) = self.replay_since(cache.epoch) {
|
||||
if deltas
|
||||
.iter()
|
||||
.all(|(_, kind)| matches!(kind, ChangeKind::Modified))
|
||||
{
|
||||
cache.epoch = self.geometry_epoch;
|
||||
let arc = Arc::clone(&cache.depths);
|
||||
*self.draw_depth_cache.borrow_mut() = Some(cache);
|
||||
return arc;
|
||||
}
|
||||
|
||||
let mut depths = (*cache.depths).clone();
|
||||
let mut affected: HashSet<Handle> = HashSet::default();
|
||||
let ms = self.model_space_block_handle();
|
||||
for &(handle, kind) in &deltas {
|
||||
match kind {
|
||||
ChangeKind::Modified => {}
|
||||
ChangeKind::Removed => {
|
||||
if let Some(block) = cache.owners.remove(&handle.value()) {
|
||||
if let Some(order) = cache.blocks.get_mut(&block) {
|
||||
order.retain(|(value, _)| *value != handle.value());
|
||||
}
|
||||
affected.insert(block);
|
||||
}
|
||||
depths.remove(&handle.value());
|
||||
}
|
||||
ChangeKind::Added => {
|
||||
let Some(entity) = self.document.get_entity(handle) else {
|
||||
continue;
|
||||
};
|
||||
if matches!(
|
||||
entity,
|
||||
EntityType::Solid3D(_)
|
||||
| EntityType::Region(_)
|
||||
| EntityType::Body(_)
|
||||
| EntityType::Surface(_)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let common = entity.common();
|
||||
let block = if common.owner_handle.is_null() {
|
||||
ms
|
||||
} else {
|
||||
common.owner_handle
|
||||
};
|
||||
let value = handle.value();
|
||||
// A newly allocated handle cannot already have a
|
||||
// SortEntitiesTable override; its effective key is
|
||||
// therefore its own handle.
|
||||
let order = cache.blocks.entry(block).or_default();
|
||||
let position =
|
||||
order.partition_point(|(_, effective)| *effective <= value);
|
||||
order.insert(position, (value, value));
|
||||
cache.owners.insert(value, block);
|
||||
affected.insert(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
for block in affected {
|
||||
let Some(order) = cache.blocks.get(&block) else {
|
||||
continue;
|
||||
};
|
||||
let denom = order.len() as f32 + 1.0;
|
||||
let half = 1.0 / denom;
|
||||
for (rank, (value, _)) in order.iter().enumerate() {
|
||||
let norm = (rank as f32 + 1.0) / denom;
|
||||
depths.insert(*value, [(norm - 0.5) * 2.0, half]);
|
||||
}
|
||||
}
|
||||
let arc = Arc::new(depths);
|
||||
cache.epoch = self.geometry_epoch;
|
||||
cache.depths = Arc::clone(&arc);
|
||||
*self.draw_depth_cache.borrow_mut() = Some(cache);
|
||||
if perf {
|
||||
eprintln!(
|
||||
"[perf] draw-depth-patch {:>7.1}ms entries={} changes={}",
|
||||
t_depth.elapsed().as_secs_f64() * 1000.0,
|
||||
arc.len(),
|
||||
deltas.len(),
|
||||
);
|
||||
}
|
||||
return arc;
|
||||
}
|
||||
}
|
||||
|
||||
use acadrust::objects::ObjectType;
|
||||
// Per-block SortEntitiesTable overrides: block -> (entity_val -> sort_val).
|
||||
let mut overrides: HashMap<Handle, HashMap<u64, u64>> = HashMap::default();
|
||||
|
|
@ -3540,24 +3795,40 @@ impl Scene {
|
|||
.unwrap_or(hv);
|
||||
by_block.entry(block).or_default().push((hv, eff));
|
||||
}
|
||||
for order in by_block.values_mut() {
|
||||
order.sort_by_key(|(_, effective)| *effective);
|
||||
}
|
||||
let mut depth_map: HashMap<u64, [f32; 2]> = HashMap::default();
|
||||
for (_block, mut v) in by_block {
|
||||
v.sort_by_key(|(_, eff)| *eff);
|
||||
let denom = (v.len() as f32) + 1.0;
|
||||
let mut owners: HashMap<u64, Handle> = HashMap::default();
|
||||
for (&block, order) in &by_block {
|
||||
let denom = (order.len() as f32) + 1.0;
|
||||
// Adjacent ranks are 2/denom apart; a child sub-range of
|
||||
// ±half = ±1/denom around the parent depth never crosses them.
|
||||
let half = 1.0 / denom;
|
||||
for (rank, (hv, _)) in v.into_iter().enumerate() {
|
||||
for (rank, (hv, _)) in order.iter().enumerate() {
|
||||
// Signed (-1,1): back ranks → negative, front → positive,
|
||||
// mid → ~0. The shader applies `z -= draw_depth * BIAS`, so a
|
||||
// default/unranked 0.0 means "no bias" (neutral) — which keeps
|
||||
// 3D mesh faces and transient wires at their real depth.
|
||||
let norm = (rank as f32 + 1.0) / denom; // (0,1)
|
||||
depth_map.insert(hv, [(norm - 0.5) * 2.0, half]);
|
||||
depth_map.insert(*hv, [(norm - 0.5) * 2.0, half]);
|
||||
owners.insert(*hv, block);
|
||||
}
|
||||
}
|
||||
let arc = Arc::new(depth_map);
|
||||
*self.draw_depth_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
||||
*self.draw_depth_cache.borrow_mut() = Some(DrawDepthCache {
|
||||
epoch: self.geometry_epoch,
|
||||
depths: Arc::clone(&arc),
|
||||
blocks: by_block,
|
||||
owners,
|
||||
});
|
||||
if perf {
|
||||
eprintln!(
|
||||
"[perf] draw-depth {:>7.1}ms entries={}",
|
||||
t_depth.elapsed().as_secs_f64() * 1000.0,
|
||||
arc.len(),
|
||||
);
|
||||
}
|
||||
arc
|
||||
}
|
||||
|
||||
|
|
@ -4624,35 +4895,50 @@ impl Scene {
|
|||
|
||||
fn interaction_overlay_wires(
|
||||
&self,
|
||||
base_handles: impl IntoIterator<Item = u64>,
|
||||
base_keys: impl IntoIterator<Item = (u64, u32)>,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
) -> Arc<Vec<WireModel>> {
|
||||
let changed: HashSet<Handle> = changes.iter().map(|(handle, _)| *handle).collect();
|
||||
let mut handles: HashSet<Handle> = base_handles
|
||||
.into_iter()
|
||||
.map(Handle::new)
|
||||
.filter(|handle| !changed.contains(handle))
|
||||
.collect();
|
||||
handles.extend(changes.iter().filter_map(|(handle, kind)| {
|
||||
(!matches!(kind, ChangeKind::Removed)).then_some(*handle)
|
||||
}));
|
||||
|
||||
let memo = self.resident_tess_memo.borrow();
|
||||
let mut wires = Vec::new();
|
||||
let mut misses = Vec::new();
|
||||
for handle in handles {
|
||||
if self.document.get_entity(handle).is_none() {
|
||||
let mut misses: HashMap<Handle, Vec<u32>> = HashMap::default();
|
||||
for (handle, ordinal) in base_keys {
|
||||
let handle = Handle::new(handle);
|
||||
if changed.contains(&handle) || self.document.get_entity(handle).is_none() {
|
||||
continue;
|
||||
}
|
||||
if let Some(entity_wires) = memo.get(&handle) {
|
||||
wires.extend(entity_wires.iter().cloned());
|
||||
if let Some(wire) = memo
|
||||
.get(&handle)
|
||||
.and_then(|entity_wires| entity_wires.get(ordinal as usize))
|
||||
{
|
||||
wires.push(wire.clone());
|
||||
} else {
|
||||
misses.push(handle);
|
||||
misses.entry(handle).or_default().push(ordinal);
|
||||
}
|
||||
}
|
||||
drop(memo);
|
||||
if !misses.is_empty() {
|
||||
wires.extend(self.wire_models_for(&misses));
|
||||
// A memo miss is uncommon (guard change / legacy source). Preserve
|
||||
// correctness without expanding every nearby entity: regenerate that
|
||||
// handle, then retain only the exact wire ordinals the base index
|
||||
// reported.
|
||||
for (handle, ordinals) in misses {
|
||||
let entity_wires = self.wire_models_for(&[handle]);
|
||||
for ordinal in ordinals {
|
||||
if let Some(wire) = entity_wires.get(ordinal as usize) {
|
||||
wires.push(wire.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Changed handles are absent or stale in the immutable base index. The
|
||||
// journal is deliberately small, so append their current runs in full.
|
||||
let changed_live: Vec<Handle> = changes
|
||||
.iter()
|
||||
.filter_map(|(handle, kind)| {
|
||||
(!matches!(kind, ChangeKind::Removed)).then_some(*handle)
|
||||
})
|
||||
.collect();
|
||||
if !changed_live.is_empty() {
|
||||
wires.extend(self.wire_models_for(&changed_live));
|
||||
}
|
||||
Arc::new(wires)
|
||||
}
|
||||
|
|
@ -4663,11 +4949,31 @@ impl Scene {
|
|||
aabb: [f64; 4],
|
||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||
if let Some((base, changes)) = self.interaction_overlay_base() {
|
||||
let handles = base.query_wire_handles_xy(aabb);
|
||||
let local = self.interaction_overlay_wires(handles, &changes);
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
let t0 = iced::time::Instant::now();
|
||||
let keys = base.query_wire_keys_xy(aabb);
|
||||
let key_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
let t_local = iced::time::Instant::now();
|
||||
let local = self.interaction_overlay_wires(keys.iter().copied(), &changes);
|
||||
let local_ms = t_local.elapsed().as_secs_f64() * 1000.0;
|
||||
let t_index = iced::time::Instant::now();
|
||||
let local_index =
|
||||
crate::scene::pick::interaction_index::InteractionIndex::build(&local);
|
||||
return local_index.query_xy(local, aabb);
|
||||
let index_ms = t_index.elapsed().as_secs_f64() * 1000.0;
|
||||
let result = local_index.query_xy(Arc::clone(&local), aabb);
|
||||
let total_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf && total_ms >= 50.0 {
|
||||
eprintln!(
|
||||
"[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} gather={:.1} index={:.1}",
|
||||
total_ms,
|
||||
keys.len(),
|
||||
local.len(),
|
||||
key_ms,
|
||||
local_ms,
|
||||
index_ms,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
self.interaction_index(&wires).query_xy(wires, aabb)
|
||||
}
|
||||
|
|
@ -4681,11 +4987,37 @@ impl Scene {
|
|||
bounds: iced::Rectangle,
|
||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||
if let Some((base, changes)) = self.interaction_overlay_base() {
|
||||
let handles = base.query_wire_handles_screen(screen_rect, view_rot, eye, bounds);
|
||||
let local = self.interaction_overlay_wires(handles, &changes);
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
let t0 = iced::time::Instant::now();
|
||||
let keys = base.query_wire_keys_screen(screen_rect, view_rot, eye, bounds);
|
||||
let key_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
let t_local = iced::time::Instant::now();
|
||||
let local = self.interaction_overlay_wires(keys.iter().copied(), &changes);
|
||||
let local_ms = t_local.elapsed().as_secs_f64() * 1000.0;
|
||||
let t_index = iced::time::Instant::now();
|
||||
let local_index =
|
||||
crate::scene::pick::interaction_index::InteractionIndex::build(&local);
|
||||
return local_index.query_screen(local, screen_rect, view_rot, eye, bounds);
|
||||
let index_ms = t_index.elapsed().as_secs_f64() * 1000.0;
|
||||
let result = local_index.query_screen(
|
||||
Arc::clone(&local),
|
||||
screen_rect,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
let total_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf && total_ms >= 50.0 {
|
||||
eprintln!(
|
||||
"[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} gather={:.1} index={:.1}",
|
||||
total_ms,
|
||||
keys.len(),
|
||||
local.len(),
|
||||
key_ms,
|
||||
local_ms,
|
||||
index_ms,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
self.interaction_index(&wires)
|
||||
.query_screen(wires, screen_rect, view_rot, eye, bounds)
|
||||
|
|
|
|||
|
|
@ -459,6 +459,11 @@ pub struct InteractionIndex {
|
|||
/// index feed an incremental overlay after small edits without retaining
|
||||
/// the old heavyweight wire set or trusting shifted vector indices.
|
||||
wire_handles: Vec<Option<u64>>,
|
||||
/// Stable occurrence number of each wire inside its owning entity run.
|
||||
/// Small-edit overlays use `(handle, ordinal)` to recover only the exact
|
||||
/// nearby wires from the current resident memo instead of cloning every
|
||||
/// wire emitted by a large INSERT.
|
||||
wire_ordinals: Vec<Option<u32>>,
|
||||
segments: SpatialSet<SegmentRef>,
|
||||
snap_points: SpatialSet<SnapPointRef>,
|
||||
key_vertices: SpatialSet<KeyVertexRef>,
|
||||
|
|
@ -526,6 +531,18 @@ impl InteractionIndex {
|
|||
.iter()
|
||||
.map(|wire| wire.name.parse::<u64>().ok())
|
||||
.collect();
|
||||
let mut next_ordinal: rustc_hash::FxHashMap<u64, u32> =
|
||||
rustc_hash::FxHashMap::default();
|
||||
let wire_ordinals: Vec<Option<u32>> = wire_handles
|
||||
.iter()
|
||||
.map(|handle| {
|
||||
let handle = (*handle)?;
|
||||
let ordinal = next_ordinal.entry(handle).or_default();
|
||||
let current = *ordinal;
|
||||
*ordinal += 1;
|
||||
Some(current)
|
||||
})
|
||||
.collect();
|
||||
let mut wire_entries = Vec::with_capacity(wires.len());
|
||||
let mut segment_entries = Vec::new();
|
||||
let mut snap_point_entries = Vec::new();
|
||||
|
|
@ -701,6 +718,7 @@ impl InteractionIndex {
|
|||
Self {
|
||||
wires,
|
||||
wire_handles,
|
||||
wire_ordinals,
|
||||
segments,
|
||||
snap_points,
|
||||
key_vertices,
|
||||
|
|
@ -717,29 +735,40 @@ impl InteractionIndex {
|
|||
base_radius_px.max(self.max_line_half_width_px)
|
||||
}
|
||||
|
||||
fn queried_wire_handles(&self, mut indices: Vec<u32>) -> Vec<u64> {
|
||||
fn queried_wire_keys(&self, mut indices: Vec<u32>) -> Vec<(u64, u32)> {
|
||||
indices.extend_from_slice(&self.unbounded_wires);
|
||||
let mut handles: Vec<u64> = indices
|
||||
let mut keys: Vec<(u64, u32)> = indices
|
||||
.into_iter()
|
||||
.filter_map(|index| self.wire_handles.get(index as usize).copied().flatten())
|
||||
.filter_map(|index| {
|
||||
self.wire_handles
|
||||
.get(index as usize)
|
||||
.copied()
|
||||
.flatten()
|
||||
.zip(
|
||||
self.wire_ordinals
|
||||
.get(index as usize)
|
||||
.copied()
|
||||
.flatten(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
handles.sort_unstable();
|
||||
handles.dedup();
|
||||
handles
|
||||
keys.sort_unstable();
|
||||
keys.dedup();
|
||||
keys
|
||||
}
|
||||
|
||||
pub fn query_wire_handles_xy(&self, aabb: [f64; 4]) -> Vec<u64> {
|
||||
self.queried_wire_handles(self.wires.query_xy(aabb))
|
||||
pub fn query_wire_keys_xy(&self, aabb: [f64; 4]) -> Vec<(u64, u32)> {
|
||||
self.queried_wire_keys(self.wires.query_xy(aabb))
|
||||
}
|
||||
|
||||
pub fn query_wire_handles_screen(
|
||||
pub fn query_wire_keys_screen(
|
||||
&self,
|
||||
screen_rect: [f32; 4],
|
||||
view_rot: Mat4,
|
||||
eye: DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<u64> {
|
||||
self.queried_wire_handles(self.wires.query_screen(screen_rect, view_rot, eye, bounds))
|
||||
) -> Vec<(u64, u32)> {
|
||||
self.queried_wire_keys(self.wires.query_screen(screen_rect, view_rot, eye, bounds))
|
||||
}
|
||||
|
||||
pub fn query_xy(&self, wires: Arc<Vec<WireModel>>, aabb: [f64; 4]) -> InteractionCandidates {
|
||||
|
|
|
|||
|
|
@ -226,7 +226,6 @@ pub struct Pipeline {
|
|||
pub cached_text_source: Option<std::sync::Arc<Vec<text_gpu::TextVertex>>>,
|
||||
pub cached_mesh_source: Option<std::sync::Arc<Vec<MeshLodSet>>>,
|
||||
pub cached_face3d_source: Option<std::sync::Arc<Vec<WireModel>>>,
|
||||
pub cached_face3d_wire_source: Option<std::sync::Arc<Vec<WireModel>>>,
|
||||
pub cached_face3d_depth_source:
|
||||
Option<std::sync::Arc<rustc_hash::FxHashMap<u64, [f32; 2]>>>,
|
||||
pub cached_fill_mode: bool,
|
||||
|
|
@ -253,12 +252,10 @@ pub struct Pipeline {
|
|||
/// re-upload with their tint.
|
||||
#[allow(dead_code)] // per-mesh LOD path, bypassed by the batched mesh draw
|
||||
pub cached_mesh_key: (u64, u64),
|
||||
/// `(geometry_epoch, face3d_fill_active)` the Face3D edge/fill buffers were
|
||||
/// uploaded for. The buffers are world-space and selection-independent, so
|
||||
/// they only change when the geometry changes or the 3D-fill mode toggles —
|
||||
/// never on a pan/orbit. Keyed separately from `cached_epoch` (which carries
|
||||
/// `camera_generation`) so a camera move no longer re-walks every wire to
|
||||
/// rebuild the Face3D fill buffer.
|
||||
/// `(wire_content_id, face3d_fill_active)` the Face3D edge/fill buffers were
|
||||
/// uploaded for. A stable content id avoids retaining the resident wire Arc:
|
||||
/// that Arc must stay uniquely owned by Scene so a small edit can splice it
|
||||
/// in place instead of rebuilding the whole drawing.
|
||||
pub cached_face3d_key: (u64, bool),
|
||||
/// Handle → indices into the resident wire set, built once per wire upload
|
||||
/// (when `cached_wire_id` changes). Lets the selection/hover xray overlay
|
||||
|
|
@ -1477,7 +1474,6 @@ impl Pipeline {
|
|||
cached_text_source: None,
|
||||
cached_mesh_source: None,
|
||||
cached_face3d_source: None,
|
||||
cached_face3d_wire_source: None,
|
||||
cached_face3d_depth_source: None,
|
||||
cached_fill_mode: false,
|
||||
cached_epoch: (u64::MAX, u64::MAX, u64::MAX),
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ struct Slab {
|
|||
inst_len: u32,
|
||||
const_off: u32,
|
||||
const_len: u32,
|
||||
/// Entity-level draw depth used when this slab was emitted. Individual
|
||||
/// consts may carry block-local offsets around it; structural edits shift
|
||||
/// the whole slab by the base-depth delta instead of flattening those
|
||||
/// offsets.
|
||||
base_depth: f32,
|
||||
}
|
||||
|
||||
pub struct WireArena {
|
||||
|
|
@ -76,6 +81,9 @@ pub struct WireArena {
|
|||
/// slab's draw_depth (denominator change) without re-emitting geometry.
|
||||
consts_cpu: Vec<WireConst>,
|
||||
slabs: FxHashMap<Handle, Slab>,
|
||||
/// Temporarily hidden Modified slabs. Grip drag blanks these but keeps their
|
||||
/// offsets so commit/cancel can restore the original submission order.
|
||||
vacant: FxHashMap<Handle, Slab>,
|
||||
/// Tombstoned instances (blanked, not reclaimed) — past half the tail a patch
|
||||
/// bails so the caller compacts with a full rebuild.
|
||||
tombstoned: u32,
|
||||
|
|
@ -84,6 +92,8 @@ pub struct WireArena {
|
|||
/// them black in filled-with-edges modes. The regular and mesh-edge subsets
|
||||
/// of the resident set each get their own arena so both patch incrementally.
|
||||
mesh_edge: bool,
|
||||
/// Conservative submission-order sensitivity of current arena content.
|
||||
order_sensitive: bool,
|
||||
}
|
||||
|
||||
fn handle_of(w: &WireModel) -> Option<Handle> {
|
||||
|
|
@ -105,7 +115,7 @@ pub fn is_mesh_edge(w: &WireModel, mesh_names: &rustc_hash::FxHashSet<u64>) -> b
|
|||
/// Surface) are excluded from `draw_depth_map`, so their fallback edge wires
|
||||
/// get draw_depth 0.0. Two coincident opaque such wires share a z-bias and
|
||||
/// resolve by submission order, which a tail relocation would flip.
|
||||
fn append_unsafe(wires: &[&WireModel], depth_map: &FxHashMap<u64, [f32; 2]>) -> bool {
|
||||
fn order_sensitive(wires: &[&WireModel], depth_map: &FxHashMap<u64, [f32; 2]>) -> bool {
|
||||
wires.iter().any(|w| {
|
||||
w.color[3] < 0.999
|
||||
|| handle_of(w).map_or(true, |h| !depth_map.contains_key(&h.value()))
|
||||
|
|
@ -125,6 +135,37 @@ pub fn build_handle_index(wires: &[WireModel]) -> std::sync::Arc<FxHashMap<u64,
|
|||
std::sync::Arc::new(index)
|
||||
}
|
||||
|
||||
/// Apply the resident Vec's exact splice operations to the selection/text
|
||||
/// handle index. Avoids reparsing every wire name after a one-entity patch.
|
||||
pub(crate) fn patch_handle_index(
|
||||
index: &mut std::sync::Arc<FxHashMap<u64, Vec<u32>>>,
|
||||
edits: &[crate::scene::WireIndexEdit],
|
||||
) {
|
||||
let index = std::sync::Arc::make_mut(index);
|
||||
for edit in edits {
|
||||
index.remove(&edit.handle.value());
|
||||
let old_end = edit.start + edit.old_len;
|
||||
let delta = edit.new_len as isize - edit.old_len as isize;
|
||||
if delta != 0 {
|
||||
for slots in index.values_mut() {
|
||||
for slot in slots {
|
||||
if *slot as usize >= old_end {
|
||||
*slot = (*slot as isize + delta) as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if edit.new_len != 0 {
|
||||
index.insert(
|
||||
edit.handle.value(),
|
||||
(edit.start..edit.start + edit.new_len)
|
||||
.map(|slot| slot as u32)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Group `wires` (draw-order sorted, entity-contiguous) into per-handle ranges.
|
||||
fn handle_ranges(wires: &[&WireModel]) -> Option<Vec<(Handle, usize, usize)>> {
|
||||
let mut out: Vec<(Handle, usize, usize)> = Vec::new();
|
||||
|
|
@ -215,6 +256,11 @@ impl WireArena {
|
|||
for (h, i, j) in ranges {
|
||||
let inst_off = instances.len() as u32;
|
||||
let const_off = consts_cpu.len() as u32;
|
||||
let base_depth = if mesh_edge {
|
||||
0.0
|
||||
} else {
|
||||
depth_map.get(&h.value()).map_or(0.0, |d| d[0])
|
||||
};
|
||||
for &w in &wires[i..j] {
|
||||
let wire_id = consts_cpu.len() as u32;
|
||||
// 3D mesh outline edges are occluded by true depth and must NOT
|
||||
|
|
@ -232,6 +278,7 @@ impl WireArena {
|
|||
inst_len: instances.len() as u32 - inst_off,
|
||||
const_off,
|
||||
const_len: consts_cpu.len() as u32 - const_off,
|
||||
base_depth,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -267,8 +314,10 @@ impl WireArena {
|
|||
const_tail,
|
||||
consts_cpu,
|
||||
slabs,
|
||||
vacant: FxHashMap::default(),
|
||||
tombstoned: 0,
|
||||
mesh_edge,
|
||||
order_sensitive: order_sensitive(wires, depth_map),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -287,34 +336,33 @@ impl WireArena {
|
|||
&mut self,
|
||||
queue: &wgpu::Queue,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
wires: &[&WireModel],
|
||||
runs: &FxHashMap<Handle, Vec<&WireModel>>,
|
||||
new_handles_are_suffix: bool,
|
||||
depth_map: &FxHashMap<u64, [f32; 2]>,
|
||||
) -> bool {
|
||||
let Some(ranges) = handle_ranges(wires) else {
|
||||
return false;
|
||||
};
|
||||
let range_of: FxHashMap<Handle, (usize, usize)> =
|
||||
ranges.into_iter().map(|(h, i, j)| (h, (i, j))).collect();
|
||||
let append_unsafe = append_unsafe(wires, depth_map);
|
||||
|
||||
let mut structural = false;
|
||||
let depth_structural = changes
|
||||
.iter()
|
||||
.any(|(_, kind)| !matches!(kind, ChangeKind::Modified));
|
||||
for &(h, kind) in changes {
|
||||
let new_range = range_of.get(&h).copied();
|
||||
let run = runs.get(&h).map(Vec::as_slice).unwrap_or(&[]);
|
||||
|
||||
// Removed / now-hidden ⇒ tombstone the slab. A handle not in THIS
|
||||
// arena's subset (it belongs to the other batch) simply isn't in its
|
||||
// slabs, so this is a no-op for it.
|
||||
if matches!(kind, ChangeKind::Removed) || new_range.is_none() {
|
||||
if matches!(kind, ChangeKind::Removed) || run.is_empty() {
|
||||
if let Some(slab) = self.slabs.remove(&h) {
|
||||
let blanks = vec![blank_instance(); slab.inst_len as usize];
|
||||
self.write_insts(queue, slab.inst_off, &blanks);
|
||||
self.tombstoned += slab.inst_len;
|
||||
structural = true;
|
||||
if matches!(kind, ChangeKind::Modified) {
|
||||
self.vacant.insert(h, slab);
|
||||
}
|
||||
}
|
||||
if matches!(kind, ChangeKind::Removed) {
|
||||
self.vacant.remove(&h);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (i, j) = new_range.unwrap();
|
||||
let run = &wires[i..j];
|
||||
|
||||
// Emit into fresh, run-local const slots (patched to absolute below).
|
||||
let mut insts: Vec<WireInstance> = Vec::new();
|
||||
|
|
@ -328,7 +376,22 @@ impl WireArena {
|
|||
}
|
||||
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])
|
||||
};
|
||||
|
||||
if !self.slabs.contains_key(&h)
|
||||
&& self
|
||||
.vacant
|
||||
.get(&h)
|
||||
.is_some_and(|s| s.inst_len == inst_len && s.const_len == const_len)
|
||||
{
|
||||
let slab = self.vacant.remove(&h).unwrap();
|
||||
self.tombstoned = self.tombstoned.saturating_sub(slab.inst_len);
|
||||
self.slabs.insert(h, slab);
|
||||
}
|
||||
let in_place = self
|
||||
.slabs
|
||||
.get(&h)
|
||||
|
|
@ -356,6 +419,7 @@ impl WireArena {
|
|||
const_off as u64 * csz,
|
||||
bytemuck::cast_slice(&csts),
|
||||
);
|
||||
self.slabs.get_mut(&h).unwrap().base_depth = base_depth;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -364,7 +428,12 @@ impl WireArena {
|
|||
// draw-order depth, or the mesh-edge arena (all its wires are forced
|
||||
// to depth 0, so coincident edges resolve by submission order). Fall
|
||||
// back to a full rebuild instead.
|
||||
if append_unsafe || self.mesh_edge {
|
||||
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
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if self.inst_tail + inst_len > self.inst_cap
|
||||
|
|
@ -372,7 +441,7 @@ impl WireArena {
|
|||
{
|
||||
return false;
|
||||
}
|
||||
structural = true;
|
||||
self.vacant.remove(&h);
|
||||
if let Some(s) = self.slabs.remove(&h) {
|
||||
let blanks = vec![blank_instance(); s.inst_len as usize];
|
||||
self.write_insts(queue, s.inst_off, &blanks);
|
||||
|
|
@ -396,30 +465,21 @@ impl WireArena {
|
|||
inst_len,
|
||||
const_off,
|
||||
const_len,
|
||||
base_depth,
|
||||
},
|
||||
);
|
||||
self.order_sensitive |= run_order_sensitive;
|
||||
}
|
||||
|
||||
if structural {
|
||||
if depth_structural {
|
||||
// The entity count changed ⇒ draw_depth_map re-normalised every
|
||||
// entity's z-bias. Refresh each live slab's draw_depth from the new
|
||||
// depth map and re-upload the whole (small) const buffer; the instance
|
||||
// buffer is untouched.
|
||||
// A slab whose consts carry DIFFERENT depths holds block-band wires
|
||||
// with composed per-child offsets (`depth_override`); a flat refresh
|
||||
// to the entity's base depth would erase them. Fall back to a full
|
||||
// rebuild, which recomputes each wire's composed depth.
|
||||
if !self.mesh_edge {
|
||||
for slab in self.slabs.values() {
|
||||
let first = self.consts_cpu[slab.const_off as usize].draw_depth;
|
||||
for k in 1..slab.const_len {
|
||||
if self.consts_cpu[(slab.const_off + k) as usize].draw_depth != first {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (h, slab) in &self.slabs {
|
||||
// Preserve block-local depth composition. A slab may contain many
|
||||
// different child offsets around its entity base; shifting every
|
||||
// const by the base delta keeps those offsets intact.
|
||||
for (h, slab) in &mut self.slabs {
|
||||
// Mesh-edge wires keep depth 0 (no draw-order bias); regular wires
|
||||
// take the re-normalised map value.
|
||||
let dd = if self.mesh_edge {
|
||||
|
|
@ -427,14 +487,16 @@ impl WireArena {
|
|||
} else {
|
||||
depth_map.get(&h.value()).map_or(0.0, |d| d[0])
|
||||
};
|
||||
let delta = dd - slab.base_depth;
|
||||
for k in 0..slab.const_len {
|
||||
self.consts_cpu[(slab.const_off + k) as usize].draw_depth = dd;
|
||||
self.consts_cpu[(slab.const_off + k) as usize].draw_depth += delta;
|
||||
}
|
||||
slab.base_depth = dd;
|
||||
}
|
||||
queue.write_buffer(&self.const_buf, 0, bytemuck::cast_slice(&self.consts_cpu));
|
||||
if self.tombstoned > self.inst_tail / 2 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if self.tombstoned > self.inst_tail / 2 {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ pub struct ViewportData {
|
|||
/// drops off-canvas viewports, so this lets the slot detect when it has been
|
||||
/// reused by a different viewport and reset its (index-addressed) caches.
|
||||
pub(in crate::scene) instance_id: u64,
|
||||
pub(in crate::scene) wires: Arc<Vec<WireModel>>,
|
||||
/// Weak resident source. Scene owns the strong Arc; retaining another one
|
||||
/// in the previous shader Primitive prevented the next UI event from
|
||||
/// splicing a changed entity in place and forced a full rebuild.
|
||||
pub(in crate::scene) wires: std::sync::Weak<Vec<WireModel>>,
|
||||
/// This content viewport's non-rectangular clip boundary (paper layouts
|
||||
/// only), as a polygon already projected into the viewport's render-target
|
||||
/// NDC. The GPU stamps it into the stencil so content is clipped to the
|
||||
|
|
@ -120,8 +123,7 @@ pub struct ViewportData {
|
|||
/// GPU wire-arena handoff (`OCS_WIRE_GPU_PATCH`): `(prev_gen, changed)` when
|
||||
/// this Model set reached `wire_content_id` by an incremental resident patch,
|
||||
/// so `prepare` can patch just those entities' slabs. `None` ⇒ full build.
|
||||
pub(in crate::scene) wire_patch:
|
||||
Option<(u64, Arc<Vec<(acadrust::Handle, crate::scene::ChangeKind)>>)>,
|
||||
pub(in crate::scene) wire_patch: Option<(u64, Arc<crate::scene::WireGpuPatch>)>,
|
||||
/// Selected handles only (no hover) — solid meshes tint these blue.
|
||||
pub(in crate::scene) selected_handles: Arc<rustc_hash::FxHashSet<acadrust::Handle>>,
|
||||
/// Currently hovered handle — solid meshes tint it orange.
|
||||
|
|
@ -268,7 +270,6 @@ impl shader::Primitive for Primitive {
|
|||
inner.cached_text_source = None;
|
||||
inner.cached_mesh_source = None;
|
||||
inner.cached_face3d_source = None;
|
||||
inner.cached_face3d_wire_source = None;
|
||||
inner.cached_face3d_depth_source = None;
|
||||
inner.render_sig = u64::MAX;
|
||||
}
|
||||
|
|
@ -326,6 +327,9 @@ impl shader::Primitive for Primitive {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
let Some(vp_wires) = vp.wires.upgrade() else {
|
||||
continue;
|
||||
};
|
||||
// Third component is the *selected-set* signature (not
|
||||
// selection_generation, which also bumps on hover) so a rollover
|
||||
// doesn't re-upload the static hatch / face3d buffers.
|
||||
|
|
@ -385,33 +389,36 @@ impl shader::Primitive for Primitive {
|
|||
// Face3D edge/fill buffers are world-space and selection-independent
|
||||
// (upload_face3d takes no selection input), so they only change with
|
||||
// the geometry or the 3D-fill toggle — never on a pan/orbit. Gating
|
||||
// on its three source Arcs avoids rebuilding it when another entity
|
||||
// category alone advances `geometry_epoch`.
|
||||
// on its category sources plus the stable wire content id avoids
|
||||
// rebuilding it when another entity category alone changes. Never
|
||||
// retain `vp.wires` here: Scene needs unique ownership to splice a
|
||||
// one-entity edit into the resident set.
|
||||
let face_pass_unchanged = vp
|
||||
.wire_patch
|
||||
.as_ref()
|
||||
.is_some_and(|(_, patch)| !patch.face_pass_changed);
|
||||
let face3d_changed = inner
|
||||
.cached_face3d_source
|
||||
.as_ref()
|
||||
.map_or(true, |source| !Arc::ptr_eq(source, &vp.face3d_wires))
|
||||
|| inner
|
||||
.cached_face3d_wire_source
|
||||
.as_ref()
|
||||
.map_or(true, |source| !Arc::ptr_eq(source, &vp.wires))
|
||||
|| inner
|
||||
.cached_face3d_depth_source
|
||||
.as_ref()
|
||||
.map_or(true, |source| !Arc::ptr_eq(source, &vp.draw_depths));
|
||||
.map_or(true, |source| !Arc::ptr_eq(source, &vp.draw_depths))
|
||||
|| (inner.cached_face3d_key.0 != vp.wire_content_id
|
||||
&& !face_pass_unchanged);
|
||||
if face3d_changed || face3d_fill_active != inner.cached_face3d_key.1 {
|
||||
inner.upload_face3d(
|
||||
device,
|
||||
&vp.face3d_wires[..],
|
||||
&vp.wires[..],
|
||||
&vp_wires[..],
|
||||
!face3d_fill_active,
|
||||
&vp.draw_depths,
|
||||
);
|
||||
inner.cached_face3d_source = Some(Arc::clone(&vp.face3d_wires));
|
||||
inner.cached_face3d_wire_source = Some(Arc::clone(&vp.wires));
|
||||
inner.cached_face3d_depth_source = Some(Arc::clone(&vp.draw_depths));
|
||||
inner.cached_face3d_key = (vp.geometry_epoch, face3d_fill_active);
|
||||
}
|
||||
inner.cached_face3d_key = (vp.wire_content_id, face3d_fill_active);
|
||||
// Wire buffers are world-space, so a camera move alone doesn't
|
||||
// change them — only the view_proj uniform (uploaded every frame).
|
||||
// Gate the upload on the wire content id instead of the camera tick:
|
||||
|
|
@ -435,55 +442,120 @@ impl shader::Primitive for Primitive {
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if crate::scene::wire_gpu_patch_enabled() && inner.wire_const_bgl.is_some() {
|
||||
use crate::scene::pipeline::wire_arena::{self, WireArena};
|
||||
// Split the resident set into the regular 2D wires and the
|
||||
// mesh/solid EDGE wires (which need the mesh-edge draw
|
||||
// treatment), one arena each, so both patch incrementally.
|
||||
// Fill-only wires (no segments) are drawn in the face3d pass,
|
||||
// not here, so they go in neither.
|
||||
let mesh_names: rustc_hash::FxHashSet<u64> = vp
|
||||
.wires
|
||||
.iter()
|
||||
.filter(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty())
|
||||
.filter_map(|w| w.name.parse::<u64>().ok())
|
||||
.collect();
|
||||
let regular: Vec<&crate::scene::WireModel> = vp
|
||||
.wires
|
||||
.iter()
|
||||
.filter(|w| {
|
||||
!w.points.is_empty() && !wire_arena::is_mesh_edge(w, &mesh_names)
|
||||
})
|
||||
.collect();
|
||||
let mesh: Vec<&crate::scene::WireModel> = vp
|
||||
.wires
|
||||
.iter()
|
||||
.filter(|w| wire_arena::is_mesh_edge(w, &mesh_names))
|
||||
.collect();
|
||||
let bgl = inner.wire_const_bgl.as_ref().unwrap();
|
||||
let base_ok = vp
|
||||
.wire_patch
|
||||
.as_ref()
|
||||
.map_or(false, |(base, changes)| {
|
||||
inner.wire_arena_id == *base && !changes.is_empty()
|
||||
.map_or(false, |(base, patch)| {
|
||||
inner.wire_arena_id == *base && !patch.changes.is_empty()
|
||||
});
|
||||
let changes = vp.wire_patch.as_ref().map(|(_, c)| c);
|
||||
let patch = vp.wire_patch.as_ref().map(|(_, patch)| patch);
|
||||
if _perf {
|
||||
eprintln!(
|
||||
"[perf] arena-base ok={} held={} patch={:?} changes={}",
|
||||
base_ok,
|
||||
inner.wire_arena_id,
|
||||
vp.wire_patch.as_ref().map(|(base, _)| *base),
|
||||
patch.map_or(0, |p| p.changes.len()),
|
||||
);
|
||||
}
|
||||
|
||||
// Each batch: patch from the shared base if possible, else
|
||||
// rebuild just that batch. They advance together.
|
||||
// Split only changed runs on a patch. The previous path
|
||||
// scanned/parses all resident wires repeatedly here even
|
||||
// though WireArena emits just one changed entity.
|
||||
let mut regular_changed: rustc_hash::FxHashMap<
|
||||
acadrust::Handle,
|
||||
Vec<&crate::scene::WireModel>,
|
||||
> = rustc_hash::FxHashMap::default();
|
||||
let mut mesh_changed: rustc_hash::FxHashMap<
|
||||
acadrust::Handle,
|
||||
Vec<&crate::scene::WireModel>,
|
||||
> = rustc_hash::FxHashMap::default();
|
||||
if let Some(patch) = patch {
|
||||
for &(handle, _) in patch.changes.iter() {
|
||||
let run = patch
|
||||
.runs
|
||||
.get(&handle)
|
||||
.map(|wires| wires.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let mesh_entity = run
|
||||
.iter()
|
||||
.any(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty());
|
||||
regular_changed.insert(
|
||||
handle,
|
||||
run.iter()
|
||||
.filter(|w| !w.points.is_empty() && !mesh_entity)
|
||||
.collect(),
|
||||
);
|
||||
mesh_changed.insert(
|
||||
handle,
|
||||
run.iter()
|
||||
.filter(|w| !w.points.is_empty() && mesh_entity)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let reg_ok = base_ok
|
||||
&& inner.wire_arena.as_mut().map_or(false, |a| {
|
||||
a.patch(queue, changes.unwrap(), ®ular, &vp.draw_depths)
|
||||
let patch = patch.unwrap();
|
||||
a.patch(
|
||||
queue,
|
||||
&patch.changes,
|
||||
®ular_changed,
|
||||
patch.new_handles_are_suffix,
|
||||
&vp.draw_depths,
|
||||
)
|
||||
});
|
||||
if !reg_ok {
|
||||
inner.wire_arena =
|
||||
WireArena::build(device, queue, ®ular, &vp.draw_depths, bgl, false);
|
||||
}
|
||||
let mesh_ok = base_ok
|
||||
&& inner.wire_arena_mesh.as_mut().map_or(false, |a| {
|
||||
a.patch(queue, changes.unwrap(), &mesh, &vp.draw_depths)
|
||||
let patch = patch.unwrap();
|
||||
a.patch(
|
||||
queue,
|
||||
&patch.changes,
|
||||
&mesh_changed,
|
||||
patch.new_handles_are_suffix,
|
||||
&vp.draw_depths,
|
||||
)
|
||||
});
|
||||
if !mesh_ok {
|
||||
inner.wire_arena_mesh =
|
||||
WireArena::build(device, queue, &mesh, &vp.draw_depths, bgl, true);
|
||||
if !reg_ok || !mesh_ok {
|
||||
// Initial upload or a patch that outgrew arena capacity:
|
||||
// only then pay the full regular/mesh split.
|
||||
let mesh_names: rustc_hash::FxHashSet<u64> = vp_wires
|
||||
.iter()
|
||||
.filter(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty())
|
||||
.filter_map(|w| w.name.parse::<u64>().ok())
|
||||
.collect();
|
||||
if !reg_ok {
|
||||
let regular: Vec<&crate::scene::WireModel> = vp_wires
|
||||
.iter()
|
||||
.filter(|w| {
|
||||
!w.points.is_empty()
|
||||
&& !wire_arena::is_mesh_edge(w, &mesh_names)
|
||||
})
|
||||
.collect();
|
||||
inner.wire_arena = WireArena::build(
|
||||
device,
|
||||
queue,
|
||||
®ular,
|
||||
&vp.draw_depths,
|
||||
bgl,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if !mesh_ok {
|
||||
let mesh: Vec<&crate::scene::WireModel> = vp_wires
|
||||
.iter()
|
||||
.filter(|w| wire_arena::is_mesh_edge(w, &mesh_names))
|
||||
.collect();
|
||||
inner.wire_arena_mesh = WireArena::build(
|
||||
device,
|
||||
queue,
|
||||
&mesh,
|
||||
&vp.draw_depths,
|
||||
bgl,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
_patched = reg_ok && mesh_ok;
|
||||
|
||||
|
|
@ -493,7 +565,15 @@ impl shader::Primitive for Primitive {
|
|||
let mut gpus = reg.wire_gpus();
|
||||
gpus.extend(me.wire_gpus());
|
||||
inner.gpu_wires = std::sync::Arc::new(gpus);
|
||||
inner.wire_handle_index = wire_arena::build_handle_index(&vp.wires[..]);
|
||||
if _patched {
|
||||
wire_arena::patch_handle_index(
|
||||
&mut inner.wire_handle_index,
|
||||
&patch.unwrap().index_edits,
|
||||
);
|
||||
} else {
|
||||
inner.wire_handle_index =
|
||||
wire_arena::build_handle_index(&vp_wires[..]);
|
||||
}
|
||||
inner.wire_arena_id = vp.wire_content_id;
|
||||
arena_served = true;
|
||||
} else {
|
||||
|
|
@ -515,7 +595,7 @@ impl shader::Primitive for Primitive {
|
|||
Some(entry) => entry,
|
||||
None => {
|
||||
let entry =
|
||||
inner.build_wire_buffers(device, &vp.wires[..], &vp.draw_depths);
|
||||
inner.build_wire_buffers(device, &vp_wires[..], &vp.draw_depths);
|
||||
pipeline
|
||||
.wire_buffer_cache
|
||||
.insert(vp.wire_content_id, entry.clone());
|
||||
|
|
@ -548,7 +628,7 @@ impl shader::Primitive for Primitive {
|
|||
"[perf] wire {:>7.1}ms {:<18} wires={} gpu_instances={}",
|
||||
_t0.elapsed().as_secs_f64() * 1000.0,
|
||||
outcome,
|
||||
vp.wires.len(),
|
||||
vp_wires.len(),
|
||||
gi,
|
||||
);
|
||||
}
|
||||
|
|
@ -561,7 +641,7 @@ impl shader::Primitive for Primitive {
|
|||
if sel_key != inner.cached_selection {
|
||||
inner.upload_selected_wires(
|
||||
device,
|
||||
&vp.wires[..],
|
||||
&vp_wires[..],
|
||||
&vp.selected_handles,
|
||||
vp.hover_handle,
|
||||
&vp.draw_depths,
|
||||
|
|
@ -571,7 +651,7 @@ impl shader::Primitive for Primitive {
|
|||
// base text buffer.
|
||||
inner.upload_text_highlight(
|
||||
device,
|
||||
&vp.wires[..],
|
||||
&vp_wires[..],
|
||||
&vp.selected_handles,
|
||||
vp.hover_handle,
|
||||
);
|
||||
|
|
@ -1310,13 +1390,27 @@ impl Scene {
|
|||
// overlay buffer below), so the base id is the source's stable content
|
||||
// gen — a drag or camera move never re-uploads the base wire set.
|
||||
let wire_content_id = self.last_model_wire_gen.get();
|
||||
let wire_patch = self.model_wire_patch_for(wire_content_id);
|
||||
// Split Face3D wires from the rest. The split is content-only (keyed
|
||||
// by the wire-set content id), so while the geometry is unchanged it's
|
||||
// memoized rather than re-walking every wire (handle lookup + clone)
|
||||
// each frame — for every source, since all ids are stable now.
|
||||
let (face3d_wires, other_arc) = {
|
||||
let cached = { self.split_cache.borrow().get(&wire_content_id).cloned() };
|
||||
let (fa, oa) = cached.unwrap_or_else(|| {
|
||||
let inherited_empty = if let Some((base, patch)) = wire_patch.as_ref() {
|
||||
if patch.face_pass_changed {
|
||||
None
|
||||
} else {
|
||||
self.split_cache
|
||||
.borrow()
|
||||
.get(base)
|
||||
.filter(|(_, others)| others.is_none())
|
||||
.cloned()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (fa, oa) = cached.or(inherited_empty).unwrap_or_else(|| {
|
||||
// No Face3D wire at all (pure 2-D drawings, mesh imports):
|
||||
// "others" would be a wire-for-wire copy of the base set —
|
||||
// mark it `None` and use the base set directly instead of
|
||||
|
|
@ -1337,6 +1431,10 @@ impl Scene {
|
|||
c.insert(wire_content_id, (fa.clone(), oa.clone()));
|
||||
(fa, oa)
|
||||
});
|
||||
self.split_cache
|
||||
.borrow_mut()
|
||||
.entry(wire_content_id)
|
||||
.or_insert_with(|| (fa.clone(), oa.clone()));
|
||||
(fa, oa.unwrap_or_else(|| Arc::clone(&base_arc)))
|
||||
};
|
||||
// Base wire set — the cached `other` Arc directly, never cloned to
|
||||
|
|
@ -1517,7 +1615,7 @@ impl Scene {
|
|||
};
|
||||
Some(ViewportData {
|
||||
instance_id,
|
||||
wires: all_wires,
|
||||
wires: Arc::downgrade(&all_wires),
|
||||
clip_boundary_ndc,
|
||||
preview_wires,
|
||||
face3d_wires,
|
||||
|
|
@ -1552,7 +1650,7 @@ impl Scene {
|
|||
geometry_epoch: self.geometry_epoch,
|
||||
camera_generation: self.camera_generation,
|
||||
wire_content_id,
|
||||
wire_patch: self.model_wire_patch_for(wire_content_id),
|
||||
wire_patch,
|
||||
selected_handles: Arc::new(self.selected.iter().copied().collect()),
|
||||
hover_handle: self.hover_highlight,
|
||||
selection_generation: self.selection_generation,
|
||||
|
|
|
|||
Loading…
Reference in a new issue