perf: eliminate initial hover freeze

This commit is contained in:
Hakan Seven committed 2026-07-25 10:09:23 +03:00
1 parent 47423eae18
commit 2bc4c1c8f3
7 files changed
+554 -126

No files matched your search

+26
View File
@@ -735,6 +735,19 @@ pub(super) struct OpenCADStudio {
/// drawings in a file manager produces exactly that (one process per file,
/// all arriving at once), which makes this queue load-bearing, not polish.
pub(super) pending_opens: std::collections::VecDeque<PathBuf>,
/// One global interaction-index build at a time. Large drawings can each
/// hold millions of entries, so file-open bursts must not multiply peak
/// CPU and memory by the number of tabs.
active_interaction_index: Option<(u64, u64, usize)>,
/// Latest resident source requested by each waiting tab. Jobs run
/// serially behind `active_interaction_index`.
queued_interaction_indices: std::collections::VecDeque<(
u64,
u64,
usize,
std::sync::Arc<Vec<crate::scene::WireModel>>,
f32,
)>,
// ── Unsaved-changes dialog ────────────────────────────────────────────
/// Set when the user tries to close a tab or quit while there are unsaved changes.
@@ -1576,6 +1589,17 @@ pub enum Message {
/// Timer pulse while a rollover hit-test is queued; fires when the
/// cursor has been still long enough to safely run the pick.
HoverDwellTick,
/// Large projected interaction index finished preparing off the UI thread.
InteractionIndexReady {
tab_id: u64,
epoch: u64,
source: usize,
wires: std::sync::Weak<Vec<crate::scene::WireModel>>,
index: std::sync::Arc<
crate::scene::pick::interaction_index::InteractionIndex,
>,
build_ms: f64,
},
WindowResized(f32, f32),
/// Enter pressed globally — finalises the active command (no text-input involvement).
CommandFinalize,
@@ -2461,6 +2485,8 @@ impl OpenCADStudio {
plot_prev: None,
opening: None,
pending_opens: std::collections::VecDeque::new(),
active_interaction_index: None,
queued_interaction_indices: std::collections::VecDeque::new(),
pending_close: None,
active_save_jobs: std::collections::HashMap::new(),
save_job_serial: 0,
+11 -1
View File
@@ -616,7 +616,17 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.tabs[i].dirty = false;
self.tabs[i].history = crate::app::document::HistoryState::default();
self.refresh_selected_grips();
Task::batch([thumbs_task, self.drain_pending_open()])
#[cfg(not(target_arch = "wasm32"))]
let interaction_task = {
let wires = self.tabs[i].scene.hit_test_wires();
let screen_height = self.tabs[i].scene.selection.borrow().vp_size.1;
self.prepare_interaction_index_task(i, wires, screen_height)
.unwrap_or_else(Task::none)
};
#[cfg(target_arch = "wasm32")]
let interaction_task = Task::none();
let pending_open_task = self.drain_pending_open();
Task::batch([thumbs_task, pending_open_task, interaction_task])
}
pub(super) fn on_wblock_save_result_some(&mut self, block_name: String, path: std::path::PathBuf) -> Task<Message> {
+66
View File
@@ -1661,6 +1661,72 @@ impl OpenCADStudio {
Message::HoverDwellTick => self.on_hover_dwell_tick(),
Message::InteractionIndexReady {
tab_id,
epoch,
source,
wires,
index,
build_ms,
} => {
if self.active_interaction_index == Some((tab_id, epoch, source)) {
self.active_interaction_index = None;
}
let installed = self
.tabs
.iter()
.position(|tab| tab.id == tab_id)
.is_some_and(|i| {
self.tabs[i].scene.install_prepared_interaction_index(
epoch, source, wires, index,
)
});
if std::env::var_os("OCS_PERF").is_some() {
eprintln!(
"[perf] interaction-index-bg {:>7.1}ms installed={installed}",
build_ms,
);
}
while let Some((
queued_tab,
queued_epoch,
queued_source,
queued_wires,
screen_height,
)) = self.queued_interaction_indices.pop_front()
{
let Some(i) = self.tabs.iter().position(|tab| tab.id == queued_tab) else {
continue;
};
let stale = self.tabs[i].scene.geometry_epoch != queued_epoch
|| std::sync::Arc::as_ptr(&queued_wires) as usize != queued_source;
let (wires, screen_height) = if stale {
(
self.tabs[i].scene.hit_test_wires(),
self.tabs[i].scene.selection.borrow().vp_size.1,
)
} else {
(queued_wires, screen_height)
};
if let Some(task) = self.prepare_interaction_index_task(
i,
wires,
screen_height,
) {
return task;
}
}
if self.active_interaction_index.is_none() && !self.tabs.is_empty() {
let i = self.active_tab.min(self.tabs.len() - 1);
let wires = self.tabs[i].scene.hit_test_wires();
let screen_height = self.tabs[i].scene.selection.borrow().vp_size.1;
self.prepare_interaction_index_task(i, wires, screen_height)
.unwrap_or_else(Task::none)
} else {
Task::none()
}
}
Message::VisibilityPick(idx) => {
if let Some(popup) = self.visibility_popup.take() {
self.apply_visibility_state(popup.insert_handle, idx);
+98 -15
View File
@@ -19,6 +19,7 @@ use acadrust::types::Color as AcadColor;
use acadrust::{EntityType as AcadEntityType, Handle};
use iced::time::Instant;
use iced::{mouse, Point, Task};
use std::sync::Arc;
/// Pixel radius for grabbing a UCS-icon grip (origin dot or an axis tip).
const UCS_GRIP_HIT_PX: f32 = 9.0;
@@ -3484,8 +3485,17 @@ impl OpenCADStudio {
.viewport_edit_frame(canvas_sz)
.map(|(cam, _)| cam);
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
if let Some(task) =
self.prepare_interaction_index_task(i, Arc::clone(&all_wires), bounds.height)
{
// Keep the dwell armed. While the index is pending later timer
// ticks return immediately; once installed, the next tick performs
// the exact pick at the latest cursor position.
return task;
}
let hover_world = self.cursor_model_point(i, &edit_cam, p, bounds);
let hover_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
let candidate_started = Instant::now();
let hover_candidates = self.tabs[i].scene.interaction_hover_candidates_near(
all_wires,
hover_world,
view_rot,
@@ -3493,13 +3503,18 @@ impl OpenCADStudio {
bounds,
scene::pick::hit_test::CLICK_THRESHOLD_PX * 2.0,
);
let candidate_ms = candidate_started.elapsed().as_secs_f64() * 1000.0;
let candidate_count = hover_candidates.len();
let handles_started = Instant::now();
let candidate_handles = self.tabs[i]
.scene
.interaction_candidate_handles(&hover_candidates);
let handles_ms = handles_started.elapsed().as_secs_f64() * 1000.0;
// Mirror the click-selection pick order so the rollover
// highlights every selectable object: wire → hatch →
// block-internal hatch → shaded 3D solid body.
let hovered = scene::pick::hit_test::click_hit(
let wire_started = Instant::now();
let mut hovered = scene::pick::hit_test::click_hit(
p,
&hover_candidates,
view_rot,
@@ -3507,9 +3522,14 @@ impl OpenCADStudio {
bounds,
self.tabs[i].scene.document.header.lineweight_display,
)
.and_then(|s| Scene::handle_from_wire_name(s))
.or_else(|| {
scene::pick::hit_test::click_hit_hatch(
.and_then(Scene::handle_from_wire_name);
let wire_ms = wire_started.elapsed().as_secs_f64() * 1000.0;
let mut hatch_ms = 0.0;
let mut insert_ms = 0.0;
let mut solid_ms = 0.0;
if hovered.is_none() {
let started = Instant::now();
hovered = scene::pick::hit_test::click_hit_hatch(
p,
&self.tabs[i]
.scene
@@ -3518,27 +3538,35 @@ impl OpenCADStudio {
eye,
bounds,
candidate_handles.as_ref(),
)
})
.or_else(|| {
scene::pick::hit_test::click_hit_insert_hatch(
);
hatch_ms = started.elapsed().as_secs_f64() * 1000.0;
}
if hovered.is_none() {
let started = Instant::now();
hovered = scene::pick::hit_test::click_hit_insert_hatch(
p,
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
view_rot,
eye,
bounds,
candidate_handles.as_ref(),
)
})
.or_else(|| {
self.tabs[i]
);
insert_ms = started.elapsed().as_secs_f64() * 1000.0;
}
if hovered.is_none() {
let started = Instant::now();
hovered = self.tabs[i]
.scene
.solid_click_hit(p, view_rot, eye, bounds, candidate_handles.as_ref())
});
.solid_hover_hit(p, view_rot, eye, bounds, candidate_handles.as_ref());
solid_ms = started.elapsed().as_secs_f64() * 1000.0;
}
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-detail query={candidate_ms:.1} handles={handles_ms:.1} wire={wire_ms:.1} hatch={hatch_ms:.1} insert={insert_ms:.1} solid={solid_ms:.1} candidates={candidate_count}",
);
eprintln!(
"[perf] hover-dwell {:>7.1}ms wires={} hit={}",
hover_ms,
@@ -3549,6 +3577,61 @@ impl OpenCADStudio {
Task::none()
}
pub(in crate::app) fn prepare_interaction_index_task(
&mut self,
i: usize,
wires: Arc<Vec<crate::scene::WireModel>>,
screen_height_px: f32,
) -> Option<Task<Message>> {
let (epoch, source) = self.tabs[i]
.scene
.interaction_index_build_key(&wires, screen_height_px)?;
let tab_id = self.tabs[i].id;
let key = (tab_id, epoch, source);
if let Some(active) = self.active_interaction_index {
if active != key {
if let Some(position) = self
.queued_interaction_indices
.iter()
.position(|(queued_tab, ..)| *queued_tab == tab_id)
{
self.queued_interaction_indices.remove(position);
}
self.queued_interaction_indices.push_back((
tab_id,
epoch,
source,
wires,
screen_height_px,
));
}
return Some(Task::none());
}
self.tabs[i]
.scene
.mark_interaction_index_pending(epoch, source);
self.active_interaction_index = Some(key);
let weak = Arc::downgrade(&wires);
Some(Task::perform(
async move {
let started = std::time::Instant::now();
let index = Arc::new(
crate::scene::pick::interaction_index::InteractionIndex::build(&wires),
);
index.prepare_screen();
Message::InteractionIndexReady {
tab_id,
epoch,
source,
wires: weak,
index,
build_ms: started.elapsed().as_secs_f64() * 1000.0,
}
},
|message| message,
))
}
pub(super) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
let i = self.active_tab;
// A BEDIT block editor locks the active space; finish it with