perf: remove large drawing stalls
Keep resident geometry and GPU resources stable across edits, MSPACE zoom, and Model/Paper switches. Add unified PERF output for tracing remaining costs.
This commit is contained in:
parent
2bc4c1c8f3
commit
12bf126646
26 changed files with 794 additions and 325 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -74,7 +74,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/OpenAEC-Foundation/acadifc?rev=fb4140aa7ed82bb8e35e724985e95debad4d8666#fb4140aa7ed82bb8e35e724985e95debad4d8666"
|
source = "git+https://github.com/OpenAEC-Foundation/acadifc?rev=cd90c256a5e7d115f6e995275d6b3117d5775da9#cd90c256a5e7d115f6e995275d6b3117d5775da9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ lyon_tessellation = "1.0.20"
|
||||||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
|
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Track the verified DBCOLOR round-trip fix.
|
# Track the verified DWG round-trip, I/O, and unified PERF fixes.
|
||||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc", rev = "fb4140aa7ed82bb8e35e724985e95debad4d8666" }
|
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc", rev = "cd90c256a5e7d115f6e995275d6b3117d5775da9" }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
# Native enables the plugin host runtime (out-of-process plugins).
|
# Native enables the plugin host runtime (out-of-process plugins).
|
||||||
|
|
|
||||||
|
|
@ -234,10 +234,11 @@ impl OpenCADStudio {
|
||||||
// Toggle the per-rebuild wire-tessellation readout overlay.
|
// Toggle the per-rebuild wire-tessellation readout overlay.
|
||||||
"PERF" => {
|
"PERF" => {
|
||||||
self.perf_hud = !self.perf_hud;
|
self.perf_hud = !self.perf_hud;
|
||||||
|
crate::perf::set_ui_enabled(self.perf_hud);
|
||||||
self.command_line.push_info(if self.perf_hud {
|
self.command_line.push_info(if self.perf_hud {
|
||||||
"PERF HUD on — shows last wire re-tessellation cost"
|
"PERF panel on — tracing render and interaction costs"
|
||||||
} else {
|
} else {
|
||||||
"PERF HUD off"
|
"PERF panel off"
|
||||||
});
|
});
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,7 @@ pub fn start_allowed(cmd: &str) -> bool {
|
||||||
| "DONATE"
|
| "DONATE"
|
||||||
| "WEBVERSION"
|
| "WEBVERSION"
|
||||||
| "HELP"
|
| "HELP"
|
||||||
|
| "PERF"
|
||||||
| "CUI"
|
| "CUI"
|
||||||
| "ALIASEDIT"
|
| "ALIASEDIT"
|
||||||
| "CUILOAD"
|
| "CUILOAD"
|
||||||
|
|
|
||||||
|
|
@ -460,9 +460,9 @@ impl OpenCADStudio {
|
||||||
.filter(|h| scene.document.get_entity(*h).is_some())
|
.filter(|h| scene.document.get_entity(*h).is_some())
|
||||||
.collect();
|
.collect();
|
||||||
scene.selected = restored;
|
scene.selected = restored;
|
||||||
// Do not call set_current_layout here: it bumps geometry immediately.
|
// Entity deltas currently preserve the layout; direct assignment avoids
|
||||||
// Entity deltas currently preserve the layout; direct assignment also
|
// clearing layout render caches between steps and keeps future widened
|
||||||
// keeps future widened deltas batchable.
|
// deltas batchable.
|
||||||
scene.current_layout = if undo {
|
scene.current_layout = if undo {
|
||||||
d.current_layout_before.clone()
|
d.current_layout_before.clone()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,8 @@ pub(super) struct OpenCADStudio {
|
||||||
/// press-drag draws a rectangle marquee.
|
/// press-drag draws a rectangle marquee.
|
||||||
pick_drag_rect: bool,
|
pick_drag_rect: bool,
|
||||||
/// Frame-budget HUD (Phase 5.3): overlays the last wire re-tessellation
|
/// Frame-budget HUD (Phase 5.3): overlays the last wire re-tessellation
|
||||||
/// cost on the active viewport. Toggled by the `PERF` command.
|
/// cost and the shared performance trace on the active viewport. Toggled
|
||||||
|
/// by the `PERF` command.
|
||||||
perf_hud: bool,
|
perf_hud: bool,
|
||||||
/// When set, the cycling list box is open: (canvas point, candidates).
|
/// When set, the cycling list box is open: (canvas point, candidates).
|
||||||
cycle_candidates: Option<(iced::Point, Vec<acadrust::Handle>)>,
|
cycle_candidates: Option<(iced::Point, Vec<acadrust::Handle>)>,
|
||||||
|
|
@ -1501,6 +1502,10 @@ pub enum Message {
|
||||||
CommandHistoryCopy,
|
CommandHistoryCopy,
|
||||||
/// Clear every line from the command-line history.
|
/// Clear every line from the command-line history.
|
||||||
CommandHistoryClear,
|
CommandHistoryClear,
|
||||||
|
/// Copy every line currently retained by the PERF panel.
|
||||||
|
PerfCopy,
|
||||||
|
/// Clear the PERF panel's retained trace.
|
||||||
|
PerfClear,
|
||||||
/// Text-editor action from the read-only history dropdown. Only
|
/// Text-editor action from the read-only history dropdown. Only
|
||||||
/// non-editing actions (cursor moves, selection, scroll) are applied so
|
/// non-editing actions (cursor moves, selection, scroll) are applied so
|
||||||
/// the log stays read-only while remaining drag-selectable and copyable.
|
/// the log stays read-only while remaining drag-selectable and copyable.
|
||||||
|
|
|
||||||
|
|
@ -819,8 +819,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
let clone_started = std::time::Instant::now();
|
let clone_started = std::time::Instant::now();
|
||||||
let mut snapshot = self.tabs[i].scene.document.clone();
|
let mut snapshot = self.tabs[i].scene.document.clone();
|
||||||
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if std::env::var_os("OCS_PERF").is_some() {
|
if crate::perf::enabled() {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] save-snapshot {:.1}ms entities={} objects={} purpose={purpose:?}",
|
"[perf] save-snapshot {:.1}ms entities={} objects={} purpose={purpose:?}",
|
||||||
clone_ms,
|
clone_ms,
|
||||||
snapshot.entities().count(),
|
snapshot.entities().count(),
|
||||||
|
|
@ -848,8 +848,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
png,
|
png,
|
||||||
viewport,
|
viewport,
|
||||||
);
|
);
|
||||||
if std::env::var_os("OCS_PERF").is_some() {
|
if crate::perf::enabled() {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] save-thumbnail {:.1}ms wires={}",
|
"[perf] save-thumbnail {:.1}ms wires={}",
|
||||||
started.elapsed().as_secs_f64() * 1000.0,
|
started.elapsed().as_secs_f64() * 1000.0,
|
||||||
wires.len(),
|
wires.len(),
|
||||||
|
|
|
||||||
|
|
@ -1010,6 +1010,20 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::PerfCopy => {
|
||||||
|
let text = crate::perf::snapshot_text();
|
||||||
|
if text.is_empty() {
|
||||||
|
Task::none()
|
||||||
|
} else {
|
||||||
|
iced::clipboard::write(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PerfClear => {
|
||||||
|
crate::perf::clear();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::CommandHistoryEdit(action) => {
|
Message::CommandHistoryEdit(action) => {
|
||||||
// Read-only: drop edits, keep selection / cursor / scroll so
|
// Read-only: drop edits, keep selection / cursor / scroll so
|
||||||
// the user can still highlight and Ctrl+C the log.
|
// the user can still highlight and Ctrl+C the log.
|
||||||
|
|
@ -1681,8 +1695,8 @@ impl OpenCADStudio {
|
||||||
epoch, source, wires, index,
|
epoch, source, wires, index,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
if std::env::var_os("OCS_PERF").is_some() {
|
if crate::perf::enabled() {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] interaction-index-bg {:>7.1}ms installed={installed}",
|
"[perf] interaction-index-bg {:>7.1}ms installed={installed}",
|
||||||
build_ms,
|
build_ms,
|
||||||
);
|
);
|
||||||
|
|
@ -3729,6 +3743,8 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::EnterViewport(handle) => {
|
Message::EnterViewport(handle) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
let perf = crate::perf::enabled();
|
||||||
|
let total = Instant::now();
|
||||||
// Clear paper-space selection before entering model space.
|
// Clear paper-space selection before entering model space.
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.tabs[i].scene.active_viewport = Some(handle);
|
self.tabs[i].scene.active_viewport = Some(handle);
|
||||||
|
|
@ -3736,13 +3752,32 @@ impl OpenCADStudio {
|
||||||
// centre so pan/zoom, paper↔model and the display all agree —
|
// centre so pan/zoom, paper↔model and the display all agree —
|
||||||
// otherwise the camera auto-fits to the model while the cursor
|
// otherwise the camera auto-fits to the model while the cursor
|
||||||
// math stays at the origin, jittering as pan toggles the two.
|
// math stays at the origin, jittering as pan toggles the two.
|
||||||
|
let phase = Instant::now();
|
||||||
self.tabs[i].scene.normalize_active_viewport_view();
|
self.tabs[i].scene.normalize_active_viewport_view();
|
||||||
|
let normalize_ms = phase.elapsed().as_secs_f64() * 1000.0;
|
||||||
// Grid/snap follow the entered viewport.
|
// Grid/snap follow the entered viewport.
|
||||||
|
let phase = Instant::now();
|
||||||
self.adopt_view_display(i);
|
self.adopt_view_display(i);
|
||||||
|
let display_ms = phase.elapsed().as_secs_f64() * 1000.0;
|
||||||
// Adopt the entered viewport's own per-viewport UCS.
|
// Adopt the entered viewport's own per-viewport UCS.
|
||||||
|
let phase = Instant::now();
|
||||||
self.tabs[i].refresh_active_ucs();
|
self.tabs[i].refresh_active_ucs();
|
||||||
|
let ucs_ms = phase.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
let phase = Instant::now();
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
|
let properties_ms = phase.elapsed().as_secs_f64() * 1000.0;
|
||||||
self.command_line.push_output("MSPACE");
|
self.command_line.push_output("MSPACE");
|
||||||
|
if perf {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] viewport-enter total={:.2}ms normalize={:.2}ms display={:.2}ms ucs={:.2}ms properties={:.2}ms handle={}",
|
||||||
|
total.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
normalize_ms,
|
||||||
|
display_ms,
|
||||||
|
ucs_ms,
|
||||||
|
properties_ms,
|
||||||
|
handle.value(),
|
||||||
|
);
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -497,7 +497,7 @@ impl OpenCADStudio {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let perf_move = std::env::var_os("OCS_PERF").is_some();
|
let perf_move = crate::perf::enabled();
|
||||||
let move_started = Instant::now();
|
let move_started = Instant::now();
|
||||||
|
|
||||||
// UCS icon grip drag: map the cursor onto the UCS plane and
|
// UCS icon grip drag: map the cursor onto the UCS plane and
|
||||||
|
|
@ -606,6 +606,9 @@ impl OpenCADStudio {
|
||||||
drop(sel);
|
drop(sel);
|
||||||
self.tabs[i].scene.orbit_active_viewport(dx, dy);
|
self.tabs[i].scene.orbit_active_viewport(dx, dy);
|
||||||
self.tabs[i].scene.camera_generation += 1;
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.record_nav_perf(crate::scene::NavPerfOp::Rotate, move_started);
|
||||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
} else if self.tabs[i].scene.current_layout == "Model" {
|
} else if self.tabs[i].scene.current_layout == "Model" {
|
||||||
|
|
@ -621,6 +624,9 @@ impl OpenCADStudio {
|
||||||
drop(sel);
|
drop(sel);
|
||||||
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy, pivot);
|
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy, pivot);
|
||||||
self.tabs[i].scene.camera_generation += 1;
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.record_nav_perf(crate::scene::NavPerfOp::Rotate, move_started);
|
||||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
@ -654,6 +660,9 @@ impl OpenCADStudio {
|
||||||
// drawing as the view pans under it (#234).
|
// drawing as the view pans under it (#234).
|
||||||
self.reproject_box_anchor(i, vp_size.0, vp_size.1);
|
self.reproject_box_anchor(i, vp_size.0, vp_size.1);
|
||||||
}
|
}
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.record_nav_perf(crate::scene::NavPerfOp::Pan, move_started);
|
||||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
@ -886,7 +895,7 @@ impl OpenCADStudio {
|
||||||
// value and can monopolize the UI thread during a drag.
|
// value and can monopolize the UI thread during a drag.
|
||||||
let total_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
let total_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if perf_move && total_ms >= 50.0 {
|
if perf_move && total_ms >= 50.0 {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] grip-move {:>7.1}ms setup={:.1} snap={:.1} apply={:.1} preview={:.1} grips={:.1}",
|
"[perf] grip-move {:>7.1}ms setup={:.1} snap={:.1} apply={:.1} preview={:.1} grips={:.1}",
|
||||||
total_ms,
|
total_ms,
|
||||||
setup_ms,
|
setup_ms,
|
||||||
|
|
@ -1453,9 +1462,18 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
self.sync_dyn_fields();
|
self.sync_dyn_fields();
|
||||||
let move_ms = move_started.elapsed().as_secs_f64() * 1000.0;
|
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 {
|
if perf_move
|
||||||
eprintln!(
|
&& (self.tabs[i].active_cmd.is_some()
|
||||||
"[perf] pointer-move {:>7.1}ms",
|
|| self.tabs[i].scene.active_viewport.is_some())
|
||||||
|
&& move_ms >= 16.7
|
||||||
|
{
|
||||||
|
let mode = if self.tabs[i].active_cmd.is_some() {
|
||||||
|
"command"
|
||||||
|
} else {
|
||||||
|
"MSPACE"
|
||||||
|
};
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] pointer-move mode={mode:<7} {:>7.1}ms",
|
||||||
move_ms,
|
move_ms,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -3250,12 +3268,16 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
if is_double {
|
if is_double {
|
||||||
self.tabs[i].scene.fit_all();
|
self.tabs[i].scene.fit_all();
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.record_nav_perf(crate::scene::NavPerfOp::Zoom, now);
|
||||||
self.command_line.push_output("Zoom Extents");
|
self.command_line.push_output("Zoom Extents");
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
|
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
|
||||||
|
let nav_started = Instant::now();
|
||||||
let s = match delta {
|
let s = match delta {
|
||||||
mouse::ScrollDelta::Lines { y, .. } => y,
|
mouse::ScrollDelta::Lines { y, .. } => y,
|
||||||
mouse::ScrollDelta::Pixels { y, .. } => y * 0.01,
|
mouse::ScrollDelta::Pixels { y, .. } => y * 0.01,
|
||||||
|
|
@ -3310,6 +3332,9 @@ impl OpenCADStudio {
|
||||||
// as the view zooms under it (#234).
|
// as the view zooms under it (#234).
|
||||||
self.reproject_box_anchor(i, vw, vh);
|
self.reproject_box_anchor(i, vw, vh);
|
||||||
}
|
}
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.record_nav_perf(crate::scene::NavPerfOp::Zoom, nav_started);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3460,7 +3485,7 @@ impl OpenCADStudio {
|
||||||
{
|
{
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
let hover_started = Instant::now();
|
let hover_started = Instant::now();
|
||||||
let i = dwell.tab;
|
let i = dwell.tab;
|
||||||
// Re-check the gate — drag / command may have started
|
// Re-check the gate — drag / command may have started
|
||||||
|
|
@ -3564,10 +3589,10 @@ impl OpenCADStudio {
|
||||||
self.hover_dwell = None;
|
self.hover_dwell = None;
|
||||||
let hover_ms = hover_started.elapsed().as_secs_f64() * 1000.0;
|
let hover_ms = hover_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
if perf && hover_ms >= 5.0 {
|
if perf && hover_ms >= 5.0 {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[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}",
|
"[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!(
|
crate::perf_record!(
|
||||||
"[perf] hover-dwell {:>7.1}ms wires={} hit={}",
|
"[perf] hover-dwell {:>7.1}ms wires={} hit={}",
|
||||||
hover_ms,
|
hover_ms,
|
||||||
self.tabs[i].scene.last_tess_wires.get(),
|
self.tabs[i].scene.last_tess_wires.get(),
|
||||||
|
|
@ -3642,21 +3667,29 @@ impl OpenCADStudio {
|
||||||
);
|
);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
let perf = crate::perf::enabled();
|
||||||
|
let perf_total = Instant::now();
|
||||||
|
let perf_from = self.tabs[i].scene.current_layout.clone();
|
||||||
let going_to_paper = name != "Model";
|
let going_to_paper = name != "Model";
|
||||||
// Persist the camera of the layout we're leaving BEFORE switching
|
// Persist the camera of the layout we're leaving BEFORE switching
|
||||||
// so returning to it restores where the user left off (the
|
// so returning to it restores where the user left off (the
|
||||||
// periodic sync only fires on a tick, which may not have run
|
// periodic sync only fires on a tick, which may not have run
|
||||||
// since the last pan/zoom).
|
// since the last pan/zoom).
|
||||||
|
let perf_phase = Instant::now();
|
||||||
self.tabs[i].scene.sync_camera_to_document();
|
self.tabs[i].scene.sync_camera_to_document();
|
||||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||||
|
let sync_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||||
// Cancel any pending rename/context-menu and active viewport when switching.
|
// Cancel any pending rename/context-menu and active viewport when switching.
|
||||||
self.layout_rename_state = None;
|
self.layout_rename_state = None;
|
||||||
self.layout_context_menu = None;
|
self.layout_context_menu = None;
|
||||||
self.tabs[i].scene.active_viewport = None;
|
self.tabs[i].scene.active_viewport = None;
|
||||||
self.tabs[i].scene.set_current_layout(name);
|
let perf_phase = Instant::now();
|
||||||
|
self.tabs[i].scene.set_current_layout(name.clone());
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
|
let switch_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||||
// UCS follows the pane: model header UCS in the Model tab, none
|
// UCS follows the pane: model header UCS in the Model tab, none
|
||||||
// in plain paper space (a viewport's UCS is adopted on entry).
|
// in plain paper space (a viewport's UCS is adopted on entry).
|
||||||
|
let perf_phase = Instant::now();
|
||||||
self.tabs[i].refresh_active_ucs();
|
self.tabs[i].refresh_active_ucs();
|
||||||
self.tabs[i].scene.restore_saved_camera();
|
self.tabs[i].scene.restore_saved_camera();
|
||||||
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
||||||
|
|
@ -3664,15 +3697,31 @@ impl OpenCADStudio {
|
||||||
// sheet viewport in paper space, the model tile in model space)
|
// sheet viewport in paper space, the model tile in model space)
|
||||||
// so model and each layout keep independent grid state.
|
// so model and each layout keep independent grid state.
|
||||||
self.adopt_view_display(i);
|
self.adopt_view_display(i);
|
||||||
|
let restore_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||||
// Paper-space tools live in the right-edge side toolbar now, so
|
// Paper-space tools live in the right-edge side toolbar now, so
|
||||||
// entering/leaving a layout no longer hijacks the ribbon tab.
|
// entering/leaving a layout no longer hijacks the ribbon tab.
|
||||||
let _ = going_to_paper;
|
let _ = going_to_paper;
|
||||||
// Refresh VP freeze columns for the new layout.
|
// Refresh VP freeze columns for the new layout.
|
||||||
|
let perf_phase = Instant::now();
|
||||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||||
let vp_info = self.tabs[i].scene.viewport_list();
|
let vp_info = self.tabs[i].scene.viewport_list();
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.sync_with_viewports(&doc_layers, vp_info);
|
.sync_with_viewports(&doc_layers, vp_info);
|
||||||
|
let layers_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
if perf {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] layout-switch from={} to={} total={:.2}ms sync={:.2}ms switch={:.2}ms restore={:.2}ms layers={:.2}ms epoch={}",
|
||||||
|
perf_from,
|
||||||
|
name,
|
||||||
|
perf_total.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
sync_ms,
|
||||||
|
switch_ms,
|
||||||
|
restore_ms,
|
||||||
|
layers_ms,
|
||||||
|
self.tabs[i].scene.geometry_epoch,
|
||||||
|
);
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ use crate::scene::{VIEWCUBE_PAD, VIEWCUBE_REGION_PX};
|
||||||
use crate::ui::wrap_bar::DensitySwap;
|
use crate::ui::wrap_bar::DensitySwap;
|
||||||
use crate::ui::wrap_bar::WrapFlow;
|
use crate::ui::wrap_bar::WrapFlow;
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, canvas, column, container, mouse_area, pane_grid, responsive, row, shader, stack, text,
|
button, canvas, column, container, mouse_area, pane_grid, responsive, row, scrollable, shader,
|
||||||
Row, Space,
|
stack, text, Row, Space,
|
||||||
};
|
};
|
||||||
use iced::window;
|
use iced::window;
|
||||||
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
||||||
|
|
@ -1121,32 +1121,44 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frame-budget HUD (Phase 5.3): toggle with the PERF command. Shows
|
// Shared performance panel: terminal PERF lines plus the current
|
||||||
// the cost of the most recent wire re-tessellation — the work avoided
|
// tessellation summary. Copy / Clear mirror the command-history panel.
|
||||||
// by a warm wire cache — so render-path changes can be compared
|
if self.perf_hud {
|
||||||
// PR-to-PR. Reads ~0 ms while panning/zooming on a hit cache.
|
|
||||||
if self.perf_hud && !tab.is_start {
|
|
||||||
let s = &tab.scene;
|
let s = &tab.scene;
|
||||||
let label = format!(
|
let perf_w = if render_bar_w.is_finite() && render_bar_w > 1.0 {
|
||||||
|
render_bar_w
|
||||||
|
} else {
|
||||||
|
320.0
|
||||||
|
};
|
||||||
|
let summary = format!(
|
||||||
"tess {:.1} ms · {} wires · epoch {}",
|
"tess {:.1} ms · {} wires · epoch {}",
|
||||||
s.last_tess_ms.get(),
|
s.last_tess_ms.get(),
|
||||||
s.last_tess_wires.get(),
|
s.last_tess_wires.get(),
|
||||||
s.geometry_epoch,
|
s.geometry_epoch,
|
||||||
);
|
);
|
||||||
let panel = container(text(label).size(12).color(Color {
|
let trace = crate::perf::snapshot_text();
|
||||||
r: 0.6,
|
let trace = if trace.is_empty() {
|
||||||
g: 1.0,
|
"No samples yet".to_string()
|
||||||
b: 0.6,
|
} else {
|
||||||
a: 1.0,
|
trace
|
||||||
}))
|
};
|
||||||
.padding(6)
|
let perf_button_style = |_: &Theme, status: button::Status| button::Style {
|
||||||
.style(|_: &Theme| container::Style {
|
background: Some(Background::Color(if matches!(status, button::Status::Hovered) {
|
||||||
background: Some(Background::Color(Color {
|
Color {
|
||||||
r: 0.08,
|
r: 0.24,
|
||||||
g: 0.08,
|
g: 0.24,
|
||||||
b: 0.08,
|
b: 0.24,
|
||||||
a: 0.85,
|
a: 1.0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Color {
|
||||||
|
r: 0.14,
|
||||||
|
g: 0.14,
|
||||||
|
b: 0.14,
|
||||||
|
a: 1.0,
|
||||||
|
}
|
||||||
})),
|
})),
|
||||||
|
text_color: Color::WHITE,
|
||||||
border: Border {
|
border: Border {
|
||||||
color: Color {
|
color: Color {
|
||||||
r: 0.35,
|
r: 0.35,
|
||||||
|
|
@ -1158,6 +1170,62 @@ impl OpenCADStudio {
|
||||||
radius: 3.0.into(),
|
radius: 3.0.into(),
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
};
|
||||||
|
let copy_btn = button(
|
||||||
|
row![
|
||||||
|
crate::ui::icons::tinted(
|
||||||
|
crate::ui::icons::COPY,
|
||||||
|
11.0,
|
||||||
|
Color::from_rgb(0.7, 0.85, 1.0),
|
||||||
|
),
|
||||||
|
text("Copy").size(11),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.on_press(Message::PerfCopy)
|
||||||
|
.style(perf_button_style)
|
||||||
|
.padding([2, 6]);
|
||||||
|
let clear_btn = button(
|
||||||
|
row![
|
||||||
|
crate::ui::icons::tinted(
|
||||||
|
crate::ui::icons::TRASH,
|
||||||
|
11.0,
|
||||||
|
Color::from_rgb(1.0, 0.55, 0.55),
|
||||||
|
),
|
||||||
|
text("Clear").size(11),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.on_press(Message::PerfClear)
|
||||||
|
.style(perf_button_style)
|
||||||
|
.padding([2, 6]);
|
||||||
|
let header = row![
|
||||||
|
text("PERF").size(12).color(Color::from_rgb(0.6, 1.0, 0.6)),
|
||||||
|
Space::new().width(iced::Length::Fill),
|
||||||
|
copy_btn,
|
||||||
|
clear_btn,
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center);
|
||||||
|
let log = scrollable(text(trace).size(11).color(Color::from_rgb(0.8, 0.9, 0.8)))
|
||||||
|
.height(iced::Length::Fixed(220.0))
|
||||||
|
.width(iced::Length::Fill);
|
||||||
|
let panel = container(
|
||||||
|
column![
|
||||||
|
header,
|
||||||
|
text(summary).size(11).color(Color::from_rgb(0.6, 1.0, 0.6)),
|
||||||
|
log,
|
||||||
|
]
|
||||||
|
.spacing(5),
|
||||||
|
)
|
||||||
|
.width(iced::Length::Fixed(perf_w))
|
||||||
|
.padding(6)
|
||||||
|
.style(|_: &Theme| container::Style {
|
||||||
|
background: None,
|
||||||
|
border: Border::default(),
|
||||||
|
..Default::default()
|
||||||
});
|
});
|
||||||
viewport_stack = viewport_stack.push(position_canvas_overlay(
|
viewport_stack = viewport_stack.push(position_canvas_overlay(
|
||||||
iced::Point::new(12.0, 40.0),
|
iced::Point::new(12.0, 40.0),
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,7 @@ fn save_owned_as_version_inner(
|
||||||
backup: bool,
|
backup: bool,
|
||||||
clone_ms: f64,
|
clone_ms: f64,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
let total_started = std::time::Instant::now();
|
let total_started = std::time::Instant::now();
|
||||||
doc.version = version;
|
doc.version = version;
|
||||||
let styles_started = std::time::Instant::now();
|
let styles_started = std::time::Instant::now();
|
||||||
|
|
@ -484,7 +484,7 @@ fn save_owned_as_version_inner(
|
||||||
return Err(format!("replace {}: {error}", path.display()));
|
return Err(format!("replace {}: {error}", path.display()));
|
||||||
}
|
}
|
||||||
if perf {
|
if perf {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] save total={:.1}ms clone={:.1} styles={:.1} dimensions={:.1} write={:.1} entities={} objects={} path={}",
|
"[perf] save total={:.1}ms clone={:.1} styles={:.1} dimensions={:.1} write={:.1} entities={} objects={} path={}",
|
||||||
total_started.elapsed().as_secs_f64() * 1000.0,
|
total_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
clone_ms,
|
clone_ms,
|
||||||
|
|
@ -567,7 +567,7 @@ pub fn save_to_bytes(
|
||||||
ext: &str,
|
ext: &str,
|
||||||
version: acadrust::DxfVersion,
|
version: acadrust::DxfVersion,
|
||||||
) -> Result<Vec<u8>, String> {
|
) -> Result<Vec<u8>, String> {
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
let total_started = std::time::Instant::now();
|
let total_started = std::time::Instant::now();
|
||||||
let clone_started = std::time::Instant::now();
|
let clone_started = std::time::Instant::now();
|
||||||
let mut doc = doc.clone();
|
let mut doc = doc.clone();
|
||||||
|
|
@ -590,7 +590,7 @@ pub fn save_to_bytes(
|
||||||
};
|
};
|
||||||
if perf {
|
if perf {
|
||||||
let bytes = result.as_ref().map_or(0, Vec::len);
|
let bytes = result.as_ref().map_or(0, Vec::len);
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] save-bytes total={:.1}ms clone={:.1} styles={:.1} dimensions={:.1} write={:.1} bytes={} entities={} objects={}",
|
"[perf] save-bytes total={:.1}ms clone={:.1} styles={:.1} dimensions={:.1} write={:.1} bytes={} entities={} objects={}",
|
||||||
total_started.elapsed().as_secs_f64() * 1000.0,
|
total_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
clone_ms,
|
clone_ms,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ pub mod modules;
|
||||||
pub mod patreon;
|
pub mod patreon;
|
||||||
pub mod videos;
|
pub mod videos;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
|
pub mod perf;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
pub mod snap;
|
pub mod snap;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ mod modules;
|
||||||
mod patreon;
|
mod patreon;
|
||||||
mod videos;
|
mod videos;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
|
mod perf;
|
||||||
mod scene;
|
mod scene;
|
||||||
mod snap;
|
mod snap;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
|
||||||
62
src/perf.rs
Normal file
62
src/perf.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
//! Shared performance trace for terminal output and the in-app `PERF` panel.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::fmt;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
const MAX_LINES: usize = 500;
|
||||||
|
|
||||||
|
static UI_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
static ENV_ENABLED: OnceLock<bool> = OnceLock::new();
|
||||||
|
static LINES: OnceLock<Mutex<VecDeque<String>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn lines() -> &'static Mutex<VecDeque<String>> {
|
||||||
|
LINES.get_or_init(|| Mutex::new(VecDeque::with_capacity(MAX_LINES)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when tracing was requested with `PERF=1` or the in-app panel is open.
|
||||||
|
pub fn enabled() -> bool {
|
||||||
|
UI_ENABLED.load(Ordering::Relaxed)
|
||||||
|
|| *ENV_ENABLED.get_or_init(|| std::env::var_os("PERF").is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable or disable collection driven by the in-app `PERF` command.
|
||||||
|
pub fn set_ui_enabled(enabled: bool) {
|
||||||
|
UI_ENABLED.store(enabled, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write one performance line to stderr and retain it for the in-app panel.
|
||||||
|
pub fn record(args: fmt::Arguments<'_>) {
|
||||||
|
if !enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let line = args.to_string();
|
||||||
|
eprintln!("{line}");
|
||||||
|
let mut entries = lines().lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
if entries.len() == MAX_LINES {
|
||||||
|
entries.pop_front();
|
||||||
|
}
|
||||||
|
entries.push_back(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plain-text snapshot used by the panel and its Copy button.
|
||||||
|
pub fn snapshot_text() -> String {
|
||||||
|
let entries = lines().lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.map(String::as_str)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear() {
|
||||||
|
lines().lock().unwrap_or_else(|e| e.into_inner()).clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! perf_record {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
$crate::perf::record(format_args!($($arg)*))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -487,24 +487,14 @@ impl Scene {
|
||||||
/// `OpenCADStudio_Camera_<layout>` named View is honoured only as a
|
/// `OpenCADStudio_Camera_<layout>` named View is honoured only as a
|
||||||
/// backward-compatible fallback for files saved under the previous scheme.
|
/// backward-compatible fallback for files saved under the previous scheme.
|
||||||
fn apply_sheet_viewport_camera(&mut self) -> bool {
|
fn apply_sheet_viewport_camera(&mut self) -> bool {
|
||||||
let layout_block = self.current_layout_block_handle();
|
// The Layout object already owns the authoritative sheet-viewport
|
||||||
let sheet_vp = if layout_block.is_null() {
|
// handle. Do not scan every drawing entity on each Model↔Paper switch.
|
||||||
None
|
let sheet_vp = match self
|
||||||
} else {
|
.document
|
||||||
self.document
|
.get_entity(self.current_layout_sheet_viewport_handle())
|
||||||
.entities()
|
{
|
||||||
.filter_map(|e| {
|
Some(EntityType::Viewport(vp)) => Some(vp.clone()),
|
||||||
if let EntityType::Viewport(vp) = e {
|
_ => None,
|
||||||
Some(vp)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.find(|vp| {
|
|
||||||
vp.common.owner_handle == layout_block
|
|
||||||
&& !self.is_content_viewport_in_layout(vp, layout_block)
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let vp = match sheet_vp {
|
let vp = match sheet_vp {
|
||||||
|
|
@ -590,38 +580,21 @@ impl Scene {
|
||||||
|
|
||||||
// The sheet viewport entity is the authoritative paper-space view;
|
// The sheet viewport entity is the authoritative paper-space view;
|
||||||
// it round-trips natively, so no named-View side-channel is needed.
|
// it round-trips natively, so no named-View side-channel is needed.
|
||||||
let layout_block = self.current_layout_block_handle();
|
let sheet_handle = self.current_layout_sheet_viewport_handle();
|
||||||
if !layout_block.is_null() {
|
if let Some(EntityType::Viewport(vp)) =
|
||||||
let sheet_handle = self
|
self.document.get_entity_mut(sheet_handle)
|
||||||
.document
|
{
|
||||||
.entities()
|
// AutoCAD stores the paper-space view position in
|
||||||
.filter_map(|e| {
|
// `view_center` (DCS) with `view_target` at the origin —
|
||||||
if let EntityType::Viewport(vp) = e {
|
// writing it the other way round shifts the layout and
|
||||||
Some(vp)
|
// crashes nothing but renders the sheet off-place. Paper
|
||||||
} else {
|
// space is always a plan view, so DCS == WCS XY here.
|
||||||
None
|
vp.view_center =
|
||||||
}
|
acadrust::types::Vector3::new(target_wcs.x, target_wcs.y, 0.0);
|
||||||
})
|
vp.view_target = acadrust::types::Vector3::ZERO;
|
||||||
.find(|vp| {
|
vp.view_direction = vd3;
|
||||||
vp.common.owner_handle == layout_block && !self.is_content_viewport_in_layout(vp, layout_block)
|
vp.view_height = view_height as f64;
|
||||||
})
|
vp.twist_angle = twist;
|
||||||
.map(|vp| vp.common.handle);
|
|
||||||
|
|
||||||
if let Some(handle) = sheet_handle {
|
|
||||||
if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) {
|
|
||||||
// AutoCAD stores the paper-space view position in
|
|
||||||
// `view_center` (DCS) with `view_target` at the origin —
|
|
||||||
// writing it the other way round shifts the layout and
|
|
||||||
// crashes nothing but renders the sheet off-place. Paper
|
|
||||||
// space is always a plan view, so DCS == WCS XY here.
|
|
||||||
vp.view_center =
|
|
||||||
acadrust::types::Vector3::new(target_wcs.x, target_wcs.y, 0.0);
|
|
||||||
vp.view_target = acadrust::types::Vector3::ZERO;
|
|
||||||
vp.view_direction = vd3;
|
|
||||||
vp.view_height = view_height as f64;
|
|
||||||
vp.twist_angle = twist;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -352,10 +352,9 @@ pub(crate) fn tessellate_entity(
|
||||||
view_aabb: Option<[f32; 4]>,
|
view_aabb: Option<[f32; 4]>,
|
||||||
// World units per screen pixel for LOD culling. `None` = no LOD.
|
// World units per screen pixel for LOD culling. `None` = no LOD.
|
||||||
world_per_pixel: Option<f32>,
|
world_per_pixel: Option<f32>,
|
||||||
// True only when tessellating content shown INSIDE a paper-space viewport,
|
// True only when tessellating content shown inside a paper-space viewport.
|
||||||
// where PSLTSCALE scales linetypes by the viewport scale. Model-space (and
|
// Retained through recursive INSERT expansion; PSLTSCALE itself is applied
|
||||||
// paper-sheet) rendering passes false: a drawing's annotation scale must
|
// by the viewport's GPU uniform so it never changes resident wire content.
|
||||||
// not resize model-space linetypes (that is MSLTSCALE, off here).
|
|
||||||
paper_space: bool,
|
paper_space: bool,
|
||||||
) -> Vec<WireModel> {
|
) -> Vec<WireModel> {
|
||||||
let h = e.common().handle;
|
let h = e.common().handle;
|
||||||
|
|
@ -507,19 +506,10 @@ pub(crate) fn tessellate_entity(
|
||||||
let entity_color = fade_if_locked(document, e, entity_color, bg_color);
|
let entity_color = fade_if_locked(document, e, entity_color, bg_color);
|
||||||
let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32;
|
let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32;
|
||||||
let lt_name = view::render::linetype_name_for(document, e);
|
let lt_name = view::render::linetype_name_for(document, e);
|
||||||
// PSLTSCALE: scale linetype dashes by the viewport scale so they appear
|
// Paper-space linetype scaling belongs to the viewport uniform. Keeping
|
||||||
// uniform in paper space. Only applies to content shown inside a paper-space
|
// resident geometry at its model-space scale prevents every MSPACE wheel
|
||||||
// viewport (`paper_space`); model space uses LTSCALE × CELTSCALE unscaled by
|
// tick from rebuilding and uploading the viewport's complete wire set.
|
||||||
// the annotation scale, otherwise a drawing at e.g. CANNOSCALE 10:1 draws
|
let pslt_factor = 1.0_f32;
|
||||||
// its linetypes 10× off.
|
|
||||||
let pslt_factor = if paper_space && document.header.paper_space_linetype_scaling {
|
|
||||||
anno_scale
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let pattern_length = pattern_length * pslt_factor;
|
|
||||||
let pattern = pattern.map(|v| v * pslt_factor);
|
|
||||||
|
|
||||||
// ── Proxy entity: draw its cached preview ───────────────────────────────
|
// ── Proxy entity: draw its cached preview ───────────────────────────────
|
||||||
//
|
//
|
||||||
// An entity from an application we have no reader for (e.g. an Autodesk
|
// An entity from an application we have no reader for (e.g. an Autodesk
|
||||||
|
|
|
||||||
332
src/scene/mod.rs
332
src/scene/mod.rs
|
|
@ -265,6 +265,24 @@ struct DrawDepthCache {
|
||||||
owners: HashMap<u64, Handle>,
|
owners: HashMap<u64, Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PaperViewportCache {
|
||||||
|
epoch: u64,
|
||||||
|
layout: String,
|
||||||
|
layout_block: Handle,
|
||||||
|
sheet: Handle,
|
||||||
|
content: Arc<Vec<Handle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PaperSheetRenderCache {
|
||||||
|
epoch: u64,
|
||||||
|
layout: String,
|
||||||
|
selected: u64,
|
||||||
|
paper_bg: [f32; 4],
|
||||||
|
hatches: Arc<Vec<HatchModel>>,
|
||||||
|
wipeouts: Arc<Vec<HatchModel>>,
|
||||||
|
images: Arc<Vec<ImageModel>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Bound on the geometry-delta ring. A consumer that fell more than this many
|
/// 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
|
/// 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.
|
/// and does a one-time full rebuild — the safe fallback, not a correctness hole.
|
||||||
|
|
@ -924,6 +942,33 @@ fn transform_block_mesh_lod_set(
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) enum NavPerfOp {
|
||||||
|
Pan,
|
||||||
|
Zoom,
|
||||||
|
Rotate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavPerfOp {
|
||||||
|
pub(in crate::scene) fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Pan => "pan",
|
||||||
|
Self::Zoom => "zoom",
|
||||||
|
Self::Rotate => "rotate",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(in crate::scene) struct NavPerfSample {
|
||||||
|
pub(in crate::scene) op: NavPerfOp,
|
||||||
|
pub(in crate::scene) space: &'static str,
|
||||||
|
pub(in crate::scene) mode: &'static str,
|
||||||
|
pub(in crate::scene) started: iced::time::Instant,
|
||||||
|
pub(in crate::scene) input_ms: f64,
|
||||||
|
pub(in crate::scene) build_ms: f64,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
pub camera: Rc<RefCell<Camera>>,
|
pub camera: Rc<RefCell<Camera>>,
|
||||||
/// Model-space tiled viewport layout. One full-window tile by default;
|
/// Model-space tiled viewport layout. One full-window tile by default;
|
||||||
|
|
@ -1091,14 +1136,14 @@ pub struct Scene {
|
||||||
/// Keyed by `(geometry_epoch, selection_generation)` — selected hatches
|
/// Keyed by `(geometry_epoch, selection_generation)` — selected hatches
|
||||||
/// are tinted, so a select/deselect must rebuild even when the geometry
|
/// are tinted, so a select/deselect must rebuild even when the geometry
|
||||||
/// is unchanged (issue #71).
|
/// is unchanged (issue #71).
|
||||||
hatch_cache: RefCell<Option<(u64, u64, Arc<Vec<HatchModel>>)>>,
|
hatch_cache: RefCell<HashMap<String, (u64, u64, Arc<Vec<HatchModel>>)>>,
|
||||||
/// Cached wipeout fill models, keyed by geometry_epoch. Same
|
/// Cached wipeout fill models, keyed by geometry_epoch. Same
|
||||||
/// reasoning as `hatch_cache`.
|
/// reasoning as `hatch_cache`.
|
||||||
wipeout_cache: RefCell<Option<(u64, Arc<Vec<HatchModel>>)>>,
|
wipeout_cache: RefCell<HashMap<String, (u64, Arc<Vec<HatchModel>>)>>,
|
||||||
/// Cached image models, keyed by geometry_epoch. No camera key needed here.
|
/// Cached image models, keyed by geometry_epoch. No camera key needed here.
|
||||||
image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,
|
image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,
|
||||||
/// Cached mesh models, keyed by geometry_epoch.
|
/// Cached mesh models, keyed by geometry_epoch.
|
||||||
mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshLodSet>>)>>,
|
mesh_cache: RefCell<HashMap<String, (u64, Arc<Vec<MeshLodSet>>)>>,
|
||||||
/// Picking mesh source for a non-model active space, keyed by geometry epoch
|
/// Picking mesh source for a non-model active space, keyed by geometry epoch
|
||||||
/// and interaction block. Model/MSPACE reuse `mesh_cache` directly.
|
/// and interaction block. Model/MSPACE reuse `mesh_cache` directly.
|
||||||
interaction_mesh_cache: RefCell<Option<(u64, u64, Arc<Vec<MeshLodSet>>)>>,
|
interaction_mesh_cache: RefCell<Option<(u64, u64, Arc<Vec<MeshLodSet>>)>>,
|
||||||
|
|
@ -1120,10 +1165,13 @@ pub struct Scene {
|
||||||
/// filtered variants. Viewports sharing a frozen set share one entry (like
|
/// filtered variants. Viewports sharing a frozen set share one entry (like
|
||||||
/// the resident wire set). Empty for a viewport with no frozen layers (it
|
/// the resident wire set). Empty for a viewport with no frozen layers (it
|
||||||
/// reuses the unfiltered `*_arc` sets directly).
|
/// reuses the unfiltered `*_arc` sets directly).
|
||||||
frozen_hatch_cache: RefCell<HashMap<u64, (u64, u64, Arc<Vec<HatchModel>>)>>,
|
frozen_hatch_cache:
|
||||||
frozen_wipeout_cache: RefCell<HashMap<u64, (u64, Arc<Vec<HatchModel>>)>>,
|
RefCell<HashMap<(String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>,
|
||||||
|
frozen_wipeout_cache:
|
||||||
|
RefCell<HashMap<(String, u64), (u64, Arc<Vec<HatchModel>>)>>,
|
||||||
frozen_image_cache: RefCell<HashMap<u64, (u64, Arc<Vec<ImageModel>>)>>,
|
frozen_image_cache: RefCell<HashMap<u64, (u64, Arc<Vec<ImageModel>>)>>,
|
||||||
frozen_mesh_cache: RefCell<HashMap<u64, (u64, Arc<Vec<MeshLodSet>>)>>,
|
frozen_mesh_cache:
|
||||||
|
RefCell<HashMap<(String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
|
||||||
/// Cached block-INSERT hatches for hit-testing, keyed by geometry_epoch.
|
/// Cached block-INSERT hatches for hit-testing, keyed by geometry_epoch.
|
||||||
/// Building this explodes every model-space INSERT, so without the cache a
|
/// Building this explodes every model-space INSERT, so without the cache a
|
||||||
/// heavy block-instanced drawing re-explodes thousands of inserts on every
|
/// heavy block-instanced drawing re-explodes thousands of inserts on every
|
||||||
|
|
@ -1132,9 +1180,18 @@ pub struct Scene {
|
||||||
insert_hatch_cache: RefCell<Option<(u64, u64, Arc<HashMap<Handle, Vec<HatchModel>>>)>>,
|
insert_hatch_cache: RefCell<Option<(u64, u64, Arc<HashMap<Handle, Vec<HatchModel>>>)>>,
|
||||||
/// Sheet "dressing" cache over the unified resident set: the paper sheet
|
/// Sheet "dressing" cache over the unified resident set: the paper sheet
|
||||||
/// drops its own border wire and appends the printable-area guide.
|
/// drops its own border wire and appends the printable-area guide.
|
||||||
/// `(geometry_epoch, content gen, wires)` — the base is camera-independent
|
/// Per-layout `(geometry_epoch, content gen, wires)` — the base is
|
||||||
/// (no cull / LOD anywhere), so paper pan/zoom never re-tessellates it.
|
/// camera-independent, so paper pan/zoom and Model↔Paper tab switches keep
|
||||||
paper_sheet_cache: RefCell<Option<(u64, u64, Arc<Vec<WireModel>>)>>,
|
/// every already-visited sheet warm.
|
||||||
|
paper_sheet_cache: RefCell<HashMap<String, (u64, u64, Arc<Vec<WireModel>>)>>,
|
||||||
|
/// Layout viewport handles, collected from the owning block record once per
|
||||||
|
/// geometry epoch. Avoids walking every document entity on every Paper
|
||||||
|
/// pan/zoom frame just to rediscover the same handful of viewports.
|
||||||
|
paper_viewport_cache: RefCell<HashMap<String, PaperViewportCache>>,
|
||||||
|
/// Stable Paper sheet fill/image sources. Without this cache every camera
|
||||||
|
/// movement scanned the whole document for wipeouts and recreated the same
|
||||||
|
/// Arcs, making Paper frame construction CPU-bound on large drawings.
|
||||||
|
paper_sheet_render_cache: RefCell<HashMap<String, PaperSheetRenderCache>>,
|
||||||
/// Per-viewport projected wire cache for paper-space content viewports.
|
/// Per-viewport projected wire cache for paper-space content viewports.
|
||||||
/// Stores projected + clipped wires in paper-space coordinates.
|
/// Stores projected + clipped wires in paper-space coordinates.
|
||||||
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
|
||||||
|
|
@ -1191,12 +1248,9 @@ pub struct Scene {
|
||||||
/// Multiplier applied to Text/MText/Dimension sizes during tessellation.
|
/// Multiplier applied to Text/MText/Dimension sizes during tessellation.
|
||||||
/// 1.0 = no scaling. 50.0 = "1:50" drawing scale.
|
/// 1.0 = no scaling. 50.0 = "1:50" drawing scale.
|
||||||
pub annotation_scale: f32,
|
pub annotation_scale: f32,
|
||||||
/// Cached per-epoch: does annotation/viewport scale actually change wire
|
/// Cached per-epoch: does annotation scale actually change wire geometry?
|
||||||
/// output? True iff PSLTSCALE is on (linetype dashes scale with the
|
/// PSLTSCALE is handled by a per-viewport GPU uniform, so only real
|
||||||
/// viewport) or the drawing has an annotative entity (text/dim/mleader
|
/// annotative entities require a scale-specific resident set.
|
||||||
/// sizing). When false the per-viewport anno scale is inert, so every
|
|
||||||
/// content viewport collapses onto ONE shared resident set instead of a
|
|
||||||
/// full re-tessellation per distinct viewport scale. `(epoch, flag)`.
|
|
||||||
annotation_affects_wires: std::cell::Cell<Option<(u64, bool)>>,
|
annotation_affects_wires: std::cell::Cell<Option<(u64, bool)>>,
|
||||||
/// Cached model-space bounding box, keyed by geometry_epoch.
|
/// Cached model-space bounding box, keyed by geometry_epoch.
|
||||||
/// Avoids re-tessellating all entities on every ZOOM E / auto-fit call.
|
/// Avoids re-tessellating all entities on every ZOOM E / auto-fit call.
|
||||||
|
|
@ -1204,10 +1258,12 @@ pub struct Scene {
|
||||||
/// Reverse map: entity_handle → block_record_handle, built from entity_handles lists.
|
/// Reverse map: entity_handle → block_record_handle, built from entity_handles lists.
|
||||||
/// Keyed by geometry_epoch. Eliminates the O(B) fallback scan in belongs_to_visible_block.
|
/// Keyed by geometry_epoch. Eliminates the O(B) fallback scan in belongs_to_visible_block.
|
||||||
entity_block_map_cache: RefCell<Option<(u64, HashMap<Handle, Handle>)>>,
|
entity_block_map_cache: RefCell<Option<(u64, HashMap<Handle, Handle>)>>,
|
||||||
/// Tessellated block definitions in block-local coords, keyed by geometry_epoch.
|
/// Tessellated block definitions in block-local coords, keyed by render
|
||||||
/// Lets Insert tessellation transform-copy cached wires instead of
|
/// background and block epoch. Model and Paper adapt black/white colours
|
||||||
/// clone+explode+re-tessellate per reference.
|
/// differently; retaining both variants prevents a full block rebuild on
|
||||||
block_defn_cache: RefCell<Option<(u64, Arc<cache::block_cache::BlockCache>)>>,
|
/// every layout-tab switch.
|
||||||
|
block_defn_cache:
|
||||||
|
RefCell<HashMap<[u32; 4], (u64, Arc<cache::block_cache::BlockCache>)>>,
|
||||||
/// Spatial index + always-emit list for top-level entities
|
/// Spatial index + always-emit list for top-level entities
|
||||||
/// (Phase 2.1). Lazily rebuilt by `entity_index()` on
|
/// (Phase 2.1). Lazily rebuilt by `entity_index()` on
|
||||||
/// `geometry_epoch` change. See `EntityIndex` for what each side
|
/// `geometry_epoch` change. See `EntityIndex` for what each side
|
||||||
|
|
@ -1246,6 +1302,9 @@ pub struct Scene {
|
||||||
/// scene-render cache). See [`Scene::navigating_lod`].
|
/// scene-render cache). See [`Scene::navigating_lod`].
|
||||||
nav_last_gen: std::cell::Cell<u64>,
|
nav_last_gen: std::cell::Cell<u64>,
|
||||||
nav_changed_at: std::cell::Cell<Option<iced::time::Instant>>,
|
nav_changed_at: std::cell::Cell<Option<iced::time::Instant>>,
|
||||||
|
/// Latest pan / zoom / rotate event waiting for the renderer. Present only
|
||||||
|
/// while PERF tracing is enabled; consumed by the active shader primitive.
|
||||||
|
nav_perf_pending: std::cell::Cell<Option<NavPerfSample>>,
|
||||||
/// Unified static-hold wire cache — ONE infrastructure for EVERY space
|
/// Unified static-hold wire cache — ONE infrastructure for EVERY space
|
||||||
/// (Model tiles, BEDIT block editor, the paper sheet block, and paper
|
/// (Model tiles, BEDIT block editor, the paper sheet block, and paper
|
||||||
/// content viewports): the FULL, un-culled, LOD-free tessellation per
|
/// content viewports): the FULL, un-culled, LOD-free tessellation per
|
||||||
|
|
@ -1364,10 +1423,10 @@ impl Scene {
|
||||||
interaction_handle_index_cache: RefCell::new(None),
|
interaction_handle_index_cache: RefCell::new(None),
|
||||||
sort_cache: RefCell::new(None),
|
sort_cache: RefCell::new(None),
|
||||||
draw_depth_cache: RefCell::new(None),
|
draw_depth_cache: RefCell::new(None),
|
||||||
hatch_cache: RefCell::new(None),
|
hatch_cache: RefCell::new(HashMap::default()),
|
||||||
wipeout_cache: RefCell::new(None),
|
wipeout_cache: RefCell::new(HashMap::default()),
|
||||||
image_cache: RefCell::new(None),
|
image_cache: RefCell::new(None),
|
||||||
mesh_cache: RefCell::new(None),
|
mesh_cache: RefCell::new(HashMap::default()),
|
||||||
interaction_mesh_cache: RefCell::new(None),
|
interaction_mesh_cache: RefCell::new(None),
|
||||||
mesh_pick_lookup_cache: RefCell::new(None),
|
mesh_pick_lookup_cache: RefCell::new(None),
|
||||||
frozen_hatch_cache: RefCell::new(HashMap::default()),
|
frozen_hatch_cache: RefCell::new(HashMap::default()),
|
||||||
|
|
@ -1375,7 +1434,9 @@ impl Scene {
|
||||||
frozen_image_cache: RefCell::new(HashMap::default()),
|
frozen_image_cache: RefCell::new(HashMap::default()),
|
||||||
frozen_mesh_cache: RefCell::new(HashMap::default()),
|
frozen_mesh_cache: RefCell::new(HashMap::default()),
|
||||||
insert_hatch_cache: RefCell::new(None),
|
insert_hatch_cache: RefCell::new(None),
|
||||||
paper_sheet_cache: RefCell::new(None),
|
paper_sheet_cache: RefCell::new(HashMap::default()),
|
||||||
|
paper_viewport_cache: RefCell::new(HashMap::default()),
|
||||||
|
paper_sheet_render_cache: RefCell::new(HashMap::default()),
|
||||||
paper_projected_cache: RefCell::new(HashMap::default()),
|
paper_projected_cache: RefCell::new(HashMap::default()),
|
||||||
current_layout: "Model".to_string(),
|
current_layout: "Model".to_string(),
|
||||||
block_edit_block: None,
|
block_edit_block: None,
|
||||||
|
|
@ -1394,7 +1455,7 @@ impl Scene {
|
||||||
annotation_affects_wires: std::cell::Cell::new(None),
|
annotation_affects_wires: std::cell::Cell::new(None),
|
||||||
model_extents_cache: RefCell::new(None),
|
model_extents_cache: RefCell::new(None),
|
||||||
entity_block_map_cache: RefCell::new(None),
|
entity_block_map_cache: RefCell::new(None),
|
||||||
block_defn_cache: RefCell::new(None),
|
block_defn_cache: RefCell::new(HashMap::default()),
|
||||||
entity_index_cache: RefCell::new(None),
|
entity_index_cache: RefCell::new(None),
|
||||||
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||||
last_world_per_pixel: std::cell::Cell::new(0.0),
|
last_world_per_pixel: std::cell::Cell::new(0.0),
|
||||||
|
|
@ -1404,6 +1465,7 @@ impl Scene {
|
||||||
last_model_wire_gen: std::cell::Cell::new(0),
|
last_model_wire_gen: std::cell::Cell::new(0),
|
||||||
nav_last_gen: std::cell::Cell::new(0),
|
nav_last_gen: std::cell::Cell::new(0),
|
||||||
nav_changed_at: std::cell::Cell::new(None),
|
nav_changed_at: std::cell::Cell::new(None),
|
||||||
|
nav_perf_pending: std::cell::Cell::new(None),
|
||||||
resident_wire_sets: RefCell::new(HashMap::default()),
|
resident_wire_sets: RefCell::new(HashMap::default()),
|
||||||
split_cache: RefCell::new(HashMap::default()),
|
split_cache: RefCell::new(HashMap::default()),
|
||||||
tess_memo: RefCell::new(HashMap::default()),
|
tess_memo: RefCell::new(HashMap::default()),
|
||||||
|
|
@ -1465,19 +1527,20 @@ impl Scene {
|
||||||
/// Built single-threaded — recursive nested expansion makes parallelization
|
/// Built single-threaded — recursive nested expansion makes parallelization
|
||||||
/// fiddly and the cache only rebuilds when geometry actually changes.
|
/// fiddly and the cache only rebuilds when geometry actually changes.
|
||||||
pub(super) fn block_cache_arc(&self) -> Arc<cache::block_cache::BlockCache> {
|
pub(super) fn block_cache_arc(&self) -> Arc<cache::block_cache::BlockCache> {
|
||||||
{
|
|
||||||
let cache = self.block_defn_cache.borrow();
|
|
||||||
if let Some((epoch, ref arc)) = *cache {
|
|
||||||
if epoch == self.block_epoch {
|
|
||||||
return Arc::clone(arc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let bg = if self.current_layout == "Model" {
|
let bg = if self.current_layout == "Model" {
|
||||||
self.bg_color
|
self.bg_color
|
||||||
} else {
|
} else {
|
||||||
self.paper_bg_color
|
self.paper_bg_color
|
||||||
};
|
};
|
||||||
|
let key = bg.map(f32::to_bits);
|
||||||
|
{
|
||||||
|
let cache = self.block_defn_cache.borrow();
|
||||||
|
if let Some((epoch, arc)) = cache.get(&key) {
|
||||||
|
if *epoch == self.block_epoch {
|
||||||
|
return Arc::clone(arc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Block definitions are cached at block-local size (annotation scale
|
// Block definitions are cached at block-local size (annotation scale
|
||||||
// 1.0). An annotative block scales as ONE unit at the INSERT level, so
|
// 1.0). An annotative block scales as ONE unit at the INSERT level, so
|
||||||
// its internal geometry / text / attributes must NOT be scaled
|
// its internal geometry / text / attributes must NOT be scaled
|
||||||
|
|
@ -1486,7 +1549,9 @@ impl Scene {
|
||||||
let built =
|
let built =
|
||||||
cache::block_cache::BlockCache::build(&self.document, 1.0, bg, &self.draw_depth_map());
|
cache::block_cache::BlockCache::build(&self.document, 1.0, bg, &self.draw_depth_map());
|
||||||
let arc = Arc::new(built);
|
let arc = Arc::new(built);
|
||||||
*self.block_defn_cache.borrow_mut() = Some((self.block_epoch, Arc::clone(&arc)));
|
let mut cache = self.block_defn_cache.borrow_mut();
|
||||||
|
cache.retain(|_, (epoch, _)| *epoch == self.block_epoch);
|
||||||
|
cache.insert(key, (self.block_epoch, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1803,6 +1868,31 @@ impl Scene {
|
||||||
.map_or(false, |t| t.elapsed().as_millis() < Self::NAV_SETTLE_MS)
|
.map_or(false, |t| t.elapsed().as_millis() < Self::NAV_SETTLE_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn record_nav_perf(&self, op: NavPerfOp, started: iced::time::Instant) {
|
||||||
|
if !crate::perf::enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (space, mode) = if self.current_layout == "Model" {
|
||||||
|
("Model", "MODEL")
|
||||||
|
} else if self.active_viewport.is_some() {
|
||||||
|
("Paper", "MSPACE")
|
||||||
|
} else {
|
||||||
|
("Paper", "PSPACE")
|
||||||
|
};
|
||||||
|
self.nav_perf_pending.set(Some(NavPerfSample {
|
||||||
|
op,
|
||||||
|
space,
|
||||||
|
mode,
|
||||||
|
started,
|
||||||
|
input_ms: started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
build_ms: 0.0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::scene) fn take_nav_perf(&self) -> Option<NavPerfSample> {
|
||||||
|
self.nav_perf_pending.take()
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the interaction-LOD hatch suppression is enabled (env
|
/// Whether the interaction-LOD hatch suppression is enabled (env
|
||||||
/// `OCS_HATCH_LOD`), read once. Default OFF: the tessellated hatch pass is
|
/// `OCS_HATCH_LOD`), read once. Default OFF: the tessellated hatch pass is
|
||||||
/// cheap enough that suppression — and its zoom flicker (#258) — is not
|
/// cheap enough that suppression — and its zoom flicker (#258) — is not
|
||||||
|
|
@ -1939,17 +2029,20 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch the active layout. Bumps `geometry_epoch` so the wire cache
|
/// Switch the active layout without pretending the document geometry
|
||||||
/// re-tessellates — `render_style`'s `adapt_to_bg` picks the model or
|
/// changed. Resident wire sets already key on block/background, so keeping
|
||||||
/// paper background depending on `current_layout`, so cached wires
|
/// `geometry_epoch` stable lets Model and Paper retain their CPU/GPU data
|
||||||
/// from the previous layout would be coloured against the wrong bg.
|
/// across tab switches. Only caches whose contents genuinely depend on the
|
||||||
/// Also runs `recolor_meshes` so ACIS mesh colour tracks the new bg.
|
/// active layout or its background are dropped.
|
||||||
pub fn set_current_layout(&mut self, name: String) {
|
pub fn set_current_layout(&mut self, name: String) {
|
||||||
if self.current_layout != name {
|
if self.current_layout != name {
|
||||||
self.current_layout = name;
|
self.current_layout = name;
|
||||||
self.sync_active_space_to_document();
|
self.sync_active_space_to_document();
|
||||||
self.recolor_meshes();
|
self.recolor_meshes();
|
||||||
self.bump_geometry();
|
*self.wire_cache.borrow_mut() = None;
|
||||||
|
*self.interaction_mesh_cache.borrow_mut() = None;
|
||||||
|
*self.mesh_pick_lookup_cache.borrow_mut() = None;
|
||||||
|
*self.insert_hatch_cache.borrow_mut() = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2034,6 +2127,17 @@ impl Scene {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normal files carry a valid direct Layout→Viewport link. This O(1)
|
||||||
|
// path is hit on every ordinary layout-tab switch.
|
||||||
|
if cur_vp.is_valid()
|
||||||
|
&& matches!(
|
||||||
|
self.document.get_entity(cur_vp),
|
||||||
|
Some(EntityType::Viewport(vp)) if vp.common.owner_handle == block_record
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Already present? Accept either the linked viewport handle or any
|
// Already present? Accept either the linked viewport handle or any
|
||||||
// `id == 1` viewport owned by the layout block.
|
// `id == 1` viewport owned by the layout block.
|
||||||
let has_sheet = self.document.entities().any(|e| {
|
let has_sheet = self.document.entities().any(|e| {
|
||||||
|
|
@ -2781,23 +2885,17 @@ impl Scene {
|
||||||
if self.current_layout == "Model" {
|
if self.current_layout == "Model" {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let layout_block = self.current_layout_block_handle();
|
let (layout_block, _, content) = self.paper_viewport_handles();
|
||||||
if layout_block.is_null() {
|
if layout_block.is_null() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
let mut result: Vec<(acadrust::Handle, String, Vec<acadrust::Handle>)> = self
|
let mut result: Vec<(acadrust::Handle, String, Vec<acadrust::Handle>)> = content
|
||||||
.document
|
.iter()
|
||||||
.entities()
|
.filter_map(|handle| {
|
||||||
.filter_map(|e| {
|
let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else {
|
||||||
if let EntityType::Viewport(vp) = e {
|
return None;
|
||||||
if self.is_content_viewport_in_layout(vp, layout_block) {
|
};
|
||||||
Some((vp.common.handle, vp.id, vp.frozen_layers.clone()))
|
Some((vp.common.handle, vp.id, vp.frozen_layers.clone()))
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -2820,20 +2918,11 @@ impl Scene {
|
||||||
if self.current_layout == "Model" {
|
if self.current_layout == "Model" {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let layout_block = self.current_layout_block_handle();
|
let (layout_block, _, content) = self.paper_viewport_handles();
|
||||||
if layout_block.is_null() {
|
if layout_block.is_null() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
self.document
|
content.len()
|
||||||
.entities()
|
|
||||||
.filter(|e| {
|
|
||||||
if let EntityType::Viewport(vp) = e {
|
|
||||||
self.is_content_viewport_in_layout(vp, layout_block)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when any entities are hidden by Isolate / Hide.
|
/// True when any entities are hidden by Isolate / Hide.
|
||||||
|
|
@ -3105,10 +3194,9 @@ impl Scene {
|
||||||
/// [`WIRE_CONTENT_GEN`] id per build (relayed via `last_model_wire_gen`)
|
/// [`WIRE_CONTENT_GEN`] id per build (relayed via `last_model_wire_gen`)
|
||||||
/// so the GPU upload gate and `render_signature` skip unchanged content.
|
/// so the GPU upload gate and `render_signature` skip unchanged content.
|
||||||
/// Whether the per-viewport annotation scale changes wire output at all
|
/// Whether the per-viewport annotation scale changes wire output at all
|
||||||
/// (PSLTSCALE on, or any annotative entity). Cached per geometry epoch —
|
/// Cached per geometry epoch; the scan short-circuits on the first
|
||||||
/// the scan short-circuits on the first annotative entity. When false, a
|
/// annotative entity. PSLTSCALE no longer participates because dash scaling
|
||||||
/// viewport's `1/vp_scale` anno override is inert and normalized away so
|
/// is applied in the wire shader from the viewport uniform.
|
||||||
/// all viewports reuse ONE resident tessellation.
|
|
||||||
fn annotation_affects_wires(&self) -> bool {
|
fn annotation_affects_wires(&self) -> bool {
|
||||||
if let Some((epoch, v)) = self.annotation_affects_wires.get() {
|
if let Some((epoch, v)) = self.annotation_affects_wires.get() {
|
||||||
if epoch == self.geometry_epoch {
|
if epoch == self.geometry_epoch {
|
||||||
|
|
@ -3128,11 +3216,10 @@ impl Scene {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let v = self.document.header.paper_space_linetype_scaling
|
let v = self
|
||||||
|| self
|
.document
|
||||||
.document
|
.entities()
|
||||||
.entities()
|
.any(|e| crate::scene::annotative::is_annotative(&self.document, e));
|
||||||
.any(|e| crate::scene::annotative::is_annotative(&self.document, e));
|
|
||||||
self.annotation_affects_wires
|
self.annotation_affects_wires
|
||||||
.set(Some((self.geometry_epoch, v)));
|
.set(Some((self.geometry_epoch, v)));
|
||||||
v
|
v
|
||||||
|
|
@ -3326,7 +3413,7 @@ impl Scene {
|
||||||
anno_scale_override: Option<f32>,
|
anno_scale_override: Option<f32>,
|
||||||
frozen_layers: Option<&HashSet<Handle>>,
|
frozen_layers: Option<&HashSet<Handle>>,
|
||||||
) -> Option<Arc<Vec<WireModel>>> {
|
) -> Option<Arc<Vec<WireModel>>> {
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
let t_patch = iced::time::Instant::now();
|
let t_patch = iced::time::Instant::now();
|
||||||
// The entry must exist, be stale, and be uniquely held so we can move
|
// The entry must exist, be stale, and be uniquely held so we can move
|
||||||
// its wires out rather than deep-clone them.
|
// its wires out rather than deep-clone them.
|
||||||
|
|
@ -3339,7 +3426,7 @@ impl Scene {
|
||||||
let strong = Arc::strong_count(&entry.wires);
|
let strong = Arc::strong_count(&entry.wires);
|
||||||
if strong != 1 {
|
if strong != 1 {
|
||||||
if perf {
|
if perf {
|
||||||
eprintln!("[perf] resident-shared strong={strong}");
|
crate::perf_record!("[perf] resident-shared strong={strong}");
|
||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -3564,7 +3651,7 @@ impl Scene {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if perf {
|
if perf {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] resident-patch {:>7.1}ms wires={} changes={}",
|
"[perf] resident-patch {:>7.1}ms wires={} changes={}",
|
||||||
t_patch.elapsed().as_secs_f64() * 1000.0,
|
t_patch.elapsed().as_secs_f64() * 1000.0,
|
||||||
arc.len(),
|
arc.len(),
|
||||||
|
|
@ -3582,9 +3669,9 @@ impl Scene {
|
||||||
fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
fn paper_sheet_wires_arc(&self) -> Arc<Vec<WireModel>> {
|
||||||
{
|
{
|
||||||
let cache = self.paper_sheet_cache.borrow();
|
let cache = self.paper_sheet_cache.borrow();
|
||||||
if let Some((epoch, gen, ref arc)) = *cache {
|
if let Some((epoch, gen, arc)) = cache.get(&self.current_layout) {
|
||||||
if epoch == self.geometry_epoch {
|
if *epoch == self.geometry_epoch {
|
||||||
self.last_model_wire_gen.set(gen);
|
self.last_model_wire_gen.set(*gen);
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3606,7 +3693,12 @@ impl Scene {
|
||||||
let arc = Arc::new(wires);
|
let arc = Arc::new(wires);
|
||||||
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||||
self.last_model_wire_gen.set(gen);
|
self.last_model_wire_gen.set(gen);
|
||||||
*self.paper_sheet_cache.borrow_mut() = Some((self.geometry_epoch, gen, Arc::clone(&arc)));
|
let mut cache = self.paper_sheet_cache.borrow_mut();
|
||||||
|
cache.retain(|_, (epoch, _, _)| *epoch == self.geometry_epoch);
|
||||||
|
cache.insert(
|
||||||
|
self.current_layout.clone(),
|
||||||
|
(self.geometry_epoch, gen, Arc::clone(&arc)),
|
||||||
|
);
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3673,7 +3765,7 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
let t_depth = iced::time::Instant::now();
|
let t_depth = iced::time::Instant::now();
|
||||||
|
|
||||||
// Replay Add/Remove into the retained block order. This avoids rescanning
|
// Replay Add/Remove into the retained block order. This avoids rescanning
|
||||||
|
|
@ -3754,7 +3846,7 @@ impl Scene {
|
||||||
cache.depths = Arc::clone(&arc);
|
cache.depths = Arc::clone(&arc);
|
||||||
*self.draw_depth_cache.borrow_mut() = Some(cache);
|
*self.draw_depth_cache.borrow_mut() = Some(cache);
|
||||||
if perf {
|
if perf {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] draw-depth-patch {:>7.1}ms entries={} changes={}",
|
"[perf] draw-depth-patch {:>7.1}ms entries={} changes={}",
|
||||||
t_depth.elapsed().as_secs_f64() * 1000.0,
|
t_depth.elapsed().as_secs_f64() * 1000.0,
|
||||||
arc.len(),
|
arc.len(),
|
||||||
|
|
@ -3837,7 +3929,7 @@ impl Scene {
|
||||||
owners,
|
owners,
|
||||||
});
|
});
|
||||||
if perf {
|
if perf {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] draw-depth {:>7.1}ms entries={}",
|
"[perf] draw-depth {:>7.1}ms entries={}",
|
||||||
t_depth.elapsed().as_secs_f64() * 1000.0,
|
t_depth.elapsed().as_secs_f64() * 1000.0,
|
||||||
arc.len(),
|
arc.len(),
|
||||||
|
|
@ -3854,15 +3946,16 @@ impl Scene {
|
||||||
// drawings. Key on a signature of `selected` instead, so hover (which
|
// drawings. Key on a signature of `selected` instead, so hover (which
|
||||||
// never changes `selected`) keeps the cache warm.
|
// never changes `selected`) keeps the cache warm.
|
||||||
let sel_sig = self.selected_set_sig();
|
let sel_sig = self.selected_set_sig();
|
||||||
|
let space = self.current_layout.clone();
|
||||||
{
|
{
|
||||||
let reuse = {
|
let reuse = {
|
||||||
let cache = self.hatch_cache.borrow();
|
let cache = self.hatch_cache.borrow();
|
||||||
match *cache {
|
match cache.get(&space) {
|
||||||
// Selection tint is baked in, so the selected set must also
|
// Selection tint is baked in, so the selected set must also
|
||||||
// match; category = a changed handle that is a hatch/solid fill.
|
// match; category = a changed handle that is a hatch/solid fill.
|
||||||
Some((cached_epoch, cached_sel, ref arc))
|
Some((cached_epoch, cached_sel, arc))
|
||||||
if cached_sel == sel_sig
|
if *cached_sel == sel_sig
|
||||||
&& self.category_cache_valid(cached_epoch, |h| {
|
&& self.category_cache_valid(*cached_epoch, |h| {
|
||||||
self.hatches.contains_key(&h)
|
self.hatches.contains_key(&h)
|
||||||
}) =>
|
}) =>
|
||||||
{
|
{
|
||||||
|
|
@ -3872,14 +3965,17 @@ impl Scene {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(arc) = reuse {
|
if let Some(arc) = reuse {
|
||||||
if let Some((ref mut e, _, _)) = *self.hatch_cache.borrow_mut() {
|
if let Some((e, _, _)) = self.hatch_cache.borrow_mut().get_mut(&space) {
|
||||||
*e = self.geometry_epoch;
|
*e = self.geometry_epoch;
|
||||||
}
|
}
|
||||||
return arc;
|
return arc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let arc = Arc::new(self.synced_hatch_models(None));
|
let arc = Arc::new(self.synced_hatch_models(None));
|
||||||
*self.hatch_cache.borrow_mut() = Some((self.geometry_epoch, sel_sig, Arc::clone(&arc)));
|
self.hatch_cache.borrow_mut().insert(
|
||||||
|
space,
|
||||||
|
(self.geometry_epoch, sel_sig, Arc::clone(&arc)),
|
||||||
|
);
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3895,14 +3991,15 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn wipeout_models_arc(&self) -> Arc<Vec<HatchModel>> {
|
pub(super) fn wipeout_models_arc(&self) -> Arc<Vec<HatchModel>> {
|
||||||
|
let space = self.current_layout.clone();
|
||||||
{
|
{
|
||||||
let reuse = {
|
let reuse = {
|
||||||
let cache = self.wipeout_cache.borrow();
|
let cache = self.wipeout_cache.borrow();
|
||||||
match *cache {
|
match cache.get(&space) {
|
||||||
Some((cached_epoch, ref arc))
|
Some((cached_epoch, arc))
|
||||||
// wipeout_models scans the whole document for Wipeout
|
// wipeout_models scans the whole document for Wipeout
|
||||||
// entities; relevance = the changed handle is a Wipeout.
|
// entities; relevance = the changed handle is a Wipeout.
|
||||||
if self.category_cache_valid(cached_epoch, |h| {
|
if self.category_cache_valid(*cached_epoch, |h| {
|
||||||
matches!(
|
matches!(
|
||||||
self.document.get_entity(h),
|
self.document.get_entity(h),
|
||||||
Some(EntityType::Wipeout(_))
|
Some(EntityType::Wipeout(_))
|
||||||
|
|
@ -3915,14 +4012,16 @@ impl Scene {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(arc) = reuse {
|
if let Some(arc) = reuse {
|
||||||
if let Some((ref mut e, _)) = *self.wipeout_cache.borrow_mut() {
|
if let Some((e, _)) = self.wipeout_cache.borrow_mut().get_mut(&space) {
|
||||||
*e = self.geometry_epoch;
|
*e = self.geometry_epoch;
|
||||||
}
|
}
|
||||||
return arc;
|
return arc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let arc = Arc::new(self.wipeout_models(None));
|
let arc = Arc::new(self.wipeout_models(None));
|
||||||
*self.wipeout_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
self.wipeout_cache
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(space, (self.geometry_epoch, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4006,16 +4105,17 @@ impl Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn meshes_arc(&self) -> Arc<Vec<MeshLodSet>> {
|
pub(super) fn meshes_arc(&self) -> Arc<Vec<MeshLodSet>> {
|
||||||
|
let space = self.current_layout.clone();
|
||||||
{
|
{
|
||||||
let reuse = {
|
let reuse = {
|
||||||
let cache = self.mesh_cache.borrow();
|
let cache = self.mesh_cache.borrow();
|
||||||
match *cache {
|
match cache.get(&space) {
|
||||||
// Top-level solids seed self.meshes; the instanced_block part
|
// Top-level solids seed self.meshes; the instanced_block part
|
||||||
// is driven by INSERTs, so an INSERT edit (e.g. a move) must
|
// is driven by INSERTs, so an INSERT edit (e.g. a move) must
|
||||||
// also invalidate. Block-definition edits route through
|
// also invalidate. Block-definition edits route through
|
||||||
// bump_geometry (a full delta) and invalidate regardless.
|
// bump_geometry (a full delta) and invalidate regardless.
|
||||||
Some((cached_epoch, ref arc))
|
Some((cached_epoch, arc))
|
||||||
if self.category_cache_valid(cached_epoch, |h| {
|
if self.category_cache_valid(*cached_epoch, |h| {
|
||||||
self.meshes.contains_key(&h)
|
self.meshes.contains_key(&h)
|
||||||
|| matches!(
|
|| matches!(
|
||||||
self.document.get_entity(h),
|
self.document.get_entity(h),
|
||||||
|
|
@ -4029,14 +4129,16 @@ impl Scene {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(arc) = reuse {
|
if let Some(arc) = reuse {
|
||||||
if let Some((ref mut e, _)) = *self.mesh_cache.borrow_mut() {
|
if let Some((e, _)) = self.mesh_cache.borrow_mut().get_mut(&space) {
|
||||||
*e = self.geometry_epoch;
|
*e = self.geometry_epoch;
|
||||||
}
|
}
|
||||||
return arc;
|
return arc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let arc = Arc::new(self.mesh_models(None));
|
let arc = Arc::new(self.mesh_models(None));
|
||||||
*self.mesh_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc)));
|
self.mesh_cache
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(space, (self.geometry_epoch, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4248,8 +4350,9 @@ impl Scene {
|
||||||
return self.hatch_models_arc();
|
return self.hatch_models_arc();
|
||||||
}
|
}
|
||||||
let sig = Self::frozen_layers_sig(frozen);
|
let sig = Self::frozen_layers_sig(frozen);
|
||||||
|
let key = (self.current_layout.clone(), sig);
|
||||||
let sel = self.selected_set_sig();
|
let sel = self.selected_set_sig();
|
||||||
if let Some((e, s, arc)) = self.frozen_hatch_cache.borrow().get(&sig) {
|
if let Some((e, s, arc)) = self.frozen_hatch_cache.borrow().get(&key) {
|
||||||
if *e == self.geometry_epoch && *s == sel {
|
if *e == self.geometry_epoch && *s == sel {
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
|
|
@ -4257,7 +4360,7 @@ impl Scene {
|
||||||
let arc = Arc::new(self.synced_hatch_models(Some(frozen)));
|
let arc = Arc::new(self.synced_hatch_models(Some(frozen)));
|
||||||
self.frozen_hatch_cache
|
self.frozen_hatch_cache
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.insert(sig, (self.geometry_epoch, sel, Arc::clone(&arc)));
|
.insert(key, (self.geometry_epoch, sel, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4270,7 +4373,8 @@ impl Scene {
|
||||||
return self.wipeout_models_arc();
|
return self.wipeout_models_arc();
|
||||||
}
|
}
|
||||||
let sig = Self::frozen_layers_sig(frozen);
|
let sig = Self::frozen_layers_sig(frozen);
|
||||||
if let Some((e, arc)) = self.frozen_wipeout_cache.borrow().get(&sig) {
|
let key = (self.current_layout.clone(), sig);
|
||||||
|
if let Some((e, arc)) = self.frozen_wipeout_cache.borrow().get(&key) {
|
||||||
if *e == self.geometry_epoch {
|
if *e == self.geometry_epoch {
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
|
|
@ -4278,7 +4382,7 @@ impl Scene {
|
||||||
let arc = Arc::new(self.wipeout_models(Some(frozen)));
|
let arc = Arc::new(self.wipeout_models(Some(frozen)));
|
||||||
self.frozen_wipeout_cache
|
self.frozen_wipeout_cache
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.insert(sig, (self.geometry_epoch, Arc::clone(&arc)));
|
.insert(key, (self.geometry_epoch, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4306,7 +4410,8 @@ impl Scene {
|
||||||
return self.meshes_arc();
|
return self.meshes_arc();
|
||||||
}
|
}
|
||||||
let sig = Self::frozen_layers_sig(frozen);
|
let sig = Self::frozen_layers_sig(frozen);
|
||||||
if let Some((e, arc)) = self.frozen_mesh_cache.borrow().get(&sig) {
|
let key = (self.current_layout.clone(), sig);
|
||||||
|
if let Some((e, arc)) = self.frozen_mesh_cache.borrow().get(&key) {
|
||||||
if *e == self.geometry_epoch {
|
if *e == self.geometry_epoch {
|
||||||
return Arc::clone(arc);
|
return Arc::clone(arc);
|
||||||
}
|
}
|
||||||
|
|
@ -4314,7 +4419,7 @@ impl Scene {
|
||||||
let arc = Arc::new(self.mesh_models(Some(frozen)));
|
let arc = Arc::new(self.mesh_models(Some(frozen)));
|
||||||
self.frozen_mesh_cache
|
self.frozen_mesh_cache
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.insert(sig, (self.geometry_epoch, Arc::clone(&arc)));
|
.insert(key, (self.geometry_epoch, Arc::clone(&arc)));
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4777,12 +4882,13 @@ impl Scene {
|
||||||
// interaction index survives pan/zoom and reaches every entity.
|
// interaction index survives pan/zoom and reaches every entity.
|
||||||
return self.entity_wires_arc();
|
return self.entity_wires_arc();
|
||||||
}
|
}
|
||||||
let layout_block = self.current_layout_block_handle();
|
|
||||||
match self.active_viewport {
|
match self.active_viewport {
|
||||||
None => self.paper_sheet_wires_arc(),
|
None => self.paper_sheet_wires_arc(),
|
||||||
Some(vp_handle) => {
|
// MSPACE editing already uses the viewport's model camera, so its
|
||||||
Arc::new(self.viewport_content_wires(layout_block, Some(vp_handle), None))
|
// interaction source must stay in model coordinates too. The old
|
||||||
}
|
// projected-Paper copy rebuilt a large Vec/Arc and interaction
|
||||||
|
// index on entry even though the resident model set already exists.
|
||||||
|
Some(vp_handle) => self.model_wires_for_viewport_arc(vp_handle, 0.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5052,7 +5158,7 @@ impl Scene {
|
||||||
allow_pending_empty: bool,
|
allow_pending_empty: bool,
|
||||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||||
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
|
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
let t0 = iced::time::Instant::now();
|
let t0 = iced::time::Instant::now();
|
||||||
let keys = base.query_wire_keys_xy(aabb);
|
let keys = base.query_wire_keys_xy(aabb);
|
||||||
let key_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
let key_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
|
@ -5076,7 +5182,7 @@ impl Scene {
|
||||||
let remap_ms = t_remap.elapsed().as_secs_f64() * 1000.0;
|
let remap_ms = t_remap.elapsed().as_secs_f64() * 1000.0;
|
||||||
let total_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
let total_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||||
if perf && total_ms >= 50.0 {
|
if perf && total_ms >= 50.0 {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} changed={:.1} gather={:.1} remap={:.1}",
|
"[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} changed={:.1} gather={:.1} remap={:.1}",
|
||||||
total_ms,
|
total_ms,
|
||||||
keys.len(),
|
keys.len(),
|
||||||
|
|
@ -5111,7 +5217,7 @@ impl Scene {
|
||||||
allow_pending_empty: bool,
|
allow_pending_empty: bool,
|
||||||
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
) -> crate::scene::pick::interaction_index::InteractionCandidates {
|
||||||
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
|
if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() {
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
let t0 = iced::time::Instant::now();
|
let t0 = iced::time::Instant::now();
|
||||||
let keys = base.query_wire_keys_screen(screen_rect, view_rot, eye, bounds);
|
let keys = base.query_wire_keys_screen(screen_rect, view_rot, eye, bounds);
|
||||||
let key_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
let key_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
|
@ -5146,7 +5252,7 @@ impl Scene {
|
||||||
let remap_ms = t_remap.elapsed().as_secs_f64() * 1000.0;
|
let remap_ms = t_remap.elapsed().as_secs_f64() * 1000.0;
|
||||||
let total_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
let total_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||||||
if perf && total_ms >= 50.0 {
|
if perf && total_ms >= 50.0 {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} changed={:.1} gather={:.1} remap={:.1}",
|
"[perf] interaction-overlay {:>7.1}ms keys={} wires={} query={:.1} changed={:.1} gather={:.1} remap={:.1}",
|
||||||
total_ms,
|
total_ms,
|
||||||
keys.len(),
|
keys.len(),
|
||||||
|
|
|
||||||
|
|
@ -434,14 +434,14 @@ impl Scene {
|
||||||
py: f32,
|
py: f32,
|
||||||
canvas: (f32, f32),
|
canvas: (f32, f32),
|
||||||
) -> Option<Handle> {
|
) -> Option<Handle> {
|
||||||
let layout_block = self.current_layout_block_handle();
|
let (_, _, handles) = self.paper_viewport_handles();
|
||||||
self.document
|
handles
|
||||||
.entities()
|
.iter()
|
||||||
.filter_map(|e| {
|
.filter_map(|handle| {
|
||||||
let EntityType::Viewport(vp) = e else {
|
let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if !self.is_content_viewport_in_layout(vp, layout_block) || !vp.status.is_on {
|
if !vp.status.is_on {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let rect = self.viewport_screen_rect(vp.common.handle, canvas)?;
|
let rect = self.viewport_screen_rect(vp.common.handle, canvas)?;
|
||||||
|
|
@ -465,14 +465,12 @@ impl Scene {
|
||||||
/// Return the handle of the first active user viewport in the current layout,
|
/// Return the handle of the first active user viewport in the current layout,
|
||||||
/// or `None` if there are none. Used by the MS command.
|
/// or `None` if there are none. Used by the MS command.
|
||||||
pub fn first_user_viewport(&self) -> Option<Handle> {
|
pub fn first_user_viewport(&self) -> Option<Handle> {
|
||||||
let layout_block = self.current_layout_block_handle();
|
let (_, _, handles) = self.paper_viewport_handles();
|
||||||
self.document.entities().find_map(|e| {
|
handles.iter().find_map(|handle| {
|
||||||
let EntityType::Viewport(vp) = e else {
|
let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if self.is_content_viewport_in_layout(vp, layout_block)
|
if vp.status.is_on {
|
||||||
&& vp.status.is_on
|
|
||||||
{
|
|
||||||
Some(vp.common.handle)
|
Some(vp.common.handle)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,71 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
|
pub(super) fn paper_viewport_handles(
|
||||||
|
&self,
|
||||||
|
) -> (Handle, Handle, Arc<Vec<Handle>>) {
|
||||||
|
{
|
||||||
|
let cache = self.paper_viewport_cache.borrow();
|
||||||
|
if let Some(cache) = cache.get(&self.current_layout) {
|
||||||
|
if cache.epoch == self.geometry_epoch && cache.layout == self.current_layout {
|
||||||
|
return (
|
||||||
|
cache.layout_block,
|
||||||
|
cache.sheet,
|
||||||
|
Arc::clone(&cache.content),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
let sheet = self.current_layout_sheet_viewport_handle();
|
||||||
|
let is_content = |handle: Handle| {
|
||||||
|
let Some(EntityType::Viewport(vp)) = self.document.get_entity(handle) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
vp.common.owner_handle == layout_block
|
||||||
|
&& if sheet.is_valid() {
|
||||||
|
handle != sheet
|
||||||
|
} else {
|
||||||
|
Self::is_content_viewport(vp)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let content = if let Some(block) = self
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|block| block.handle == layout_block)
|
||||||
|
.filter(|block| !block.entity_handles.is_empty())
|
||||||
|
{
|
||||||
|
block
|
||||||
|
.entity_handles
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|handle| is_content(*handle))
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
self.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| {
|
||||||
|
let handle = entity.common().handle;
|
||||||
|
is_content(handle).then_some(handle)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let content = Arc::new(content);
|
||||||
|
self.paper_viewport_cache.borrow_mut().insert(
|
||||||
|
self.current_layout.clone(),
|
||||||
|
PaperViewportCache {
|
||||||
|
epoch: self.geometry_epoch,
|
||||||
|
layout: self.current_layout.clone(),
|
||||||
|
layout_block,
|
||||||
|
sheet,
|
||||||
|
content: Arc::clone(&content),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(layout_block, sheet, content)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn grid_views(&self, vw: f32, vh: f32) -> Vec<(iced::Rectangle, Camera, Handle)> {
|
pub fn grid_views(&self, vw: f32, vh: f32) -> Vec<(iced::Rectangle, Camera, Handle)> {
|
||||||
self.active_viewports(vw, vh, acadrust::entities::ViewportRenderMode::Wireframe2D)
|
self.active_viewports(vw, vh, acadrust::entities::ViewportRenderMode::Wireframe2D)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -58,7 +123,7 @@ impl Scene {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
let layout_block = self.current_layout_block_handle();
|
let (_, sheet_handle, content_handles) = self.paper_viewport_handles();
|
||||||
let mut out: Vec<ViewportInstance> = Vec::new();
|
let mut out: Vec<ViewportInstance> = Vec::new();
|
||||||
// The full-canvas sheet viewport renders the paper-space entities
|
// The full-canvas sheet viewport renders the paper-space entities
|
||||||
// themselves — the layout's own view, drawn first so the floating
|
// themselves — the layout's own view, drawn first so the floating
|
||||||
|
|
@ -72,7 +137,7 @@ impl Scene {
|
||||||
sheet_cam.projection = view::camera::Projection::Orthographic;
|
sheet_cam.projection = view::camera::Projection::Orthographic;
|
||||||
let sheet_grid_on = match self
|
let sheet_grid_on = match self
|
||||||
.document
|
.document
|
||||||
.get_entity(self.current_layout_sheet_viewport_handle())
|
.get_entity(sheet_handle)
|
||||||
{
|
{
|
||||||
Some(EntityType::Viewport(vp)) => vp.status.grid_on,
|
Some(EntityType::Viewport(vp)) => vp.status.grid_on,
|
||||||
_ => false,
|
_ => false,
|
||||||
|
|
@ -92,13 +157,11 @@ impl Scene {
|
||||||
grid_on: sheet_grid_on,
|
grid_on: sheet_grid_on,
|
||||||
paper_sheet: true,
|
paper_sheet: true,
|
||||||
});
|
});
|
||||||
for e in self.document.entities() {
|
for &handle in content_handles.iter() {
|
||||||
let EntityType::Viewport(vp) = e else {
|
let Some(EntityType::Viewport(vp)) = self.document.get_entity(handle) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !self.is_content_viewport_in_layout(vp, layout_block)
|
if !vp.status.is_on {
|
||||||
|| !vp.status.is_on
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let h = vp.common.handle;
|
let h = vp.common.handle;
|
||||||
|
|
@ -122,6 +185,54 @@ impl Scene {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn paper_sheet_render_models(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
Arc<Vec<HatchModel>>,
|
||||||
|
Arc<Vec<HatchModel>>,
|
||||||
|
Arc<Vec<ImageModel>>,
|
||||||
|
) {
|
||||||
|
let selected = self.selected_set_sig();
|
||||||
|
{
|
||||||
|
let cache = self.paper_sheet_render_cache.borrow();
|
||||||
|
if let Some(cache) = cache.get(&self.current_layout) {
|
||||||
|
if cache.epoch == self.geometry_epoch
|
||||||
|
&& cache.layout == self.current_layout
|
||||||
|
&& cache.selected == selected
|
||||||
|
&& cache.paper_bg == self.paper_bg_color
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
Arc::clone(&cache.hatches),
|
||||||
|
Arc::clone(&cache.wipeouts),
|
||||||
|
Arc::clone(&cache.images),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hatches = Vec::new();
|
||||||
|
if let Some(sheet) = self.paper_sheet_fill() {
|
||||||
|
hatches.push(sheet);
|
||||||
|
}
|
||||||
|
hatches.extend(self.paper_canvas_hatches().iter().cloned());
|
||||||
|
let hatches = Arc::new(hatches);
|
||||||
|
let wipeouts = self.paper_canvas_wipeouts();
|
||||||
|
let images = self.paper_sheet_images();
|
||||||
|
self.paper_sheet_render_cache.borrow_mut().insert(
|
||||||
|
self.current_layout.clone(),
|
||||||
|
PaperSheetRenderCache {
|
||||||
|
epoch: self.geometry_epoch,
|
||||||
|
layout: self.current_layout.clone(),
|
||||||
|
selected,
|
||||||
|
paper_bg: self.paper_bg_color,
|
||||||
|
hatches: Arc::clone(&hatches),
|
||||||
|
wipeouts: Arc::clone(&wipeouts),
|
||||||
|
images: Arc::clone(&images),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(hatches, wipeouts, images)
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a paper-space Viewport entity's position/size into a pixel
|
/// Convert a paper-space Viewport entity's position/size into a pixel
|
||||||
/// `Rectangle` relative to the top-left of the canvas.
|
/// `Rectangle` relative to the top-left of the canvas.
|
||||||
///
|
///
|
||||||
|
|
@ -434,41 +545,31 @@ impl Scene {
|
||||||
) -> Arc<Vec<WireModel>> {
|
) -> Arc<Vec<WireModel>> {
|
||||||
use rustc_hash::FxHashSet as HSet;
|
use rustc_hash::FxHashSet as HSet;
|
||||||
|
|
||||||
// Only content-REAL per-viewport parameters remain: the viewport's own
|
// The viewport's frozen-layer set is the only resident-geometry input.
|
||||||
// frozen-layer set and its annotation/PSLTSCALE scale. Frustum cull and
|
// Its live zoom is camera magnification, not CANNOSCALE: tying
|
||||||
// zoom LOD are gone — every space (Model tiles, BEDIT, the sheet, and
|
// annotation geometry to view_height rebuilt the entire model on every
|
||||||
// content viewports) shares the same un-culled, LOD-free resident
|
// wheel tick whenever the drawing contained one annotative object.
|
||||||
// infrastructure, so viewports with default parameters all reuse ONE
|
// Explicit annotation-scale changes still rebuild through
|
||||||
// static-hold entry (and its stable content id) instead of re-baking
|
// `self.annotation_scale`; PSLTSCALE is a viewport GPU uniform.
|
||||||
// per viewport per zoom step.
|
let frozen = match self.document.get_entity(vp_handle) {
|
||||||
let (frozen, vp_anno_scale) = match self.document.get_entity(vp_handle) {
|
|
||||||
Some(EntityType::Viewport(vp)) => {
|
Some(EntityType::Viewport(vp)) => {
|
||||||
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
||||||
let vp_scale =
|
f
|
||||||
vp_effective_scale(vp.custom_scale, vp.view_height, vp.height);
|
|
||||||
let anno = if vp_scale > 1e-9 {
|
|
||||||
(1.0 / vp_scale) as f32
|
|
||||||
} else {
|
|
||||||
1.0_f32
|
|
||||||
};
|
|
||||||
(f, anno)
|
|
||||||
}
|
}
|
||||||
_ => (HSet::default(), 1.0_f32),
|
_ => HSet::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.resident_wires_for(
|
self.resident_wires_for(
|
||||||
self.model_space_block_handle(),
|
self.model_space_block_handle(),
|
||||||
Some(vp_anno_scale),
|
Some(self.annotation_scale),
|
||||||
Some(&frozen),
|
Some(&frozen),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resident model wires for a paper content viewport. Just the unified
|
/// Resident model wires for a paper content viewport. Just the unified
|
||||||
/// static-hold (`resident_wires_for`) keyed on the viewport's frozen set +
|
/// static-hold (`resident_wires_for`) keyed on the viewport's frozen set +
|
||||||
/// annotation scale — no per-viewport height/view cache anymore: the set is
|
/// explicit CANNOSCALE — no per-viewport height/view cache: the set is
|
||||||
/// camera-independent (no cull, no LOD), so paper zoom and MSPACE frustum
|
/// camera-independent, so paper zoom and MSPACE zoom reuse it as-is.
|
||||||
/// changes reuse it as-is, and viewports with default parameters all share
|
|
||||||
/// one entry (and one stable content id).
|
|
||||||
pub(crate) fn model_wires_for_viewport_arc(
|
pub(crate) fn model_wires_for_viewport_arc(
|
||||||
&self,
|
&self,
|
||||||
vp_handle: Handle,
|
vp_handle: Handle,
|
||||||
|
|
|
||||||
|
|
@ -748,7 +748,7 @@ impl InteractionIndex {
|
||||||
|
|
||||||
pub fn build(wires: &[WireModel]) -> Self {
|
pub fn build(wires: &[WireModel]) -> Self {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let build_started = std::time::Instant::now();
|
let build_started = std::time::Instant::now();
|
||||||
let wire_handles: Vec<Option<u64>> = wires
|
let wire_handles: Vec<Option<u64>> = wires
|
||||||
|
|
@ -930,7 +930,7 @@ impl InteractionIndex {
|
||||||
);
|
);
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if perf {
|
if perf {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] interaction-index-detail total={:.1}ms handles={:.1} collect={:.1} flatten={:.1} spatial={:.1}",
|
"[perf] interaction-index-detail total={:.1}ms handles={:.1} collect={:.1} flatten={:.1} spatial={:.1}",
|
||||||
build_started.elapsed().as_secs_f64() * 1000.0,
|
build_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
handles_elapsed.as_secs_f64() * 1000.0,
|
handles_elapsed.as_secs_f64() * 1000.0,
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,12 @@ pub struct Uniforms {
|
||||||
/// 0.0 = force opaque). Read by the wire shader so the toggle does not
|
/// 0.0 = force opaque). Read by the wire shader so the toggle does not
|
||||||
/// require a retessellate.
|
/// require a retessellate.
|
||||||
pub transparency_enable: f32,
|
pub transparency_enable: f32,
|
||||||
/// Pads the struct to 112 B (next multiple of 16) so wgpu's uniform
|
/// Per-viewport PSLTSCALE factor. Kept in the shared frame uniform so
|
||||||
/// alignment rules are satisfied.
|
/// zooming inside MSPACE changes one scalar instead of re-tessellating and
|
||||||
pub _pad: [f32; 2],
|
/// re-uploading every dashed wire.
|
||||||
|
pub linetype_scale: f32,
|
||||||
|
/// Pads the struct to the uniform alignment required by wgpu.
|
||||||
|
pub _pad: f32,
|
||||||
|
|
||||||
// ── Relative-to-eye (double-single) additions ───────────────────────────
|
// ── Relative-to-eye (double-single) additions ───────────────────────────
|
||||||
// Appended at the end so existing field offsets are unchanged; shaders that
|
// Appended at the end so existing field offsets are unchanged; shaders that
|
||||||
|
|
@ -54,7 +57,8 @@ impl Uniforms {
|
||||||
lwdisplay_enable: if lwdisplay_enable { 1.0 } else { 0.0 },
|
lwdisplay_enable: if lwdisplay_enable { 1.0 } else { 0.0 },
|
||||||
flat_shade: 0.0,
|
flat_shade: 0.0,
|
||||||
transparency_enable: 1.0,
|
transparency_enable: 1.0,
|
||||||
_pad: [0.0; 2],
|
linetype_scale: 1.0,
|
||||||
|
_pad: 0.0,
|
||||||
view_rot: camera.view_proj_rte(bounds),
|
view_rot: camera.view_proj_rte(bounds),
|
||||||
eye_high,
|
eye_high,
|
||||||
_pad_eh: 0.0,
|
_pad_eh: 0.0,
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,7 @@ impl WireArena {
|
||||||
mesh_edge: bool,
|
mesh_edge: bool,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let ranges = handle_ranges(wires)?;
|
let ranges = handle_ranges(wires)?;
|
||||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
let perf = crate::perf::enabled();
|
||||||
let total_started = std::time::Instant::now();
|
let total_started = std::time::Instant::now();
|
||||||
|
|
||||||
// Reject an oversized batch before parallel emission allocates hundreds
|
// Reject an oversized batch before parallel emission allocates hundreds
|
||||||
|
|
@ -447,7 +447,7 @@ impl WireArena {
|
||||||
let upload_ms = upload_started.elapsed().as_secs_f64() * 1000.0;
|
let upload_ms = upload_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
let const_bind_group = make_const_bg(device, const_bgl, &const_buf);
|
let const_bind_group = make_const_bg(device, const_bgl, &const_buf);
|
||||||
if perf {
|
if perf {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] arena-build-detail total={:.1}ms pack={:.1} mapped-upload={:.1} handles={} wires={} instances={} instance-bytes={} consts={}",
|
"[perf] arena-build-detail total={:.1}ms pack={:.1} mapped-upload={:.1} handles={} wires={} instances={} instance-bytes={} consts={}",
|
||||||
total_started.elapsed().as_secs_f64() * 1000.0,
|
total_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
pack_ms,
|
pack_ms,
|
||||||
|
|
@ -740,13 +740,13 @@ impl WireArena {
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if std::env::var_os("OCS_PERF").is_some() {
|
if crate::perf::enabled() {
|
||||||
let submitted: u64 = ranges
|
let submitted: u64 = ranges
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(start, end)| (end - start) as u64)
|
.map(|(start, end)| (end - start) as u64)
|
||||||
.sum();
|
.sum();
|
||||||
if submitted < self.inst_tail as u64 {
|
if submitted < self.inst_tail as u64 {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] wire-cull submitted={} resident={} ranges={}",
|
"[perf] wire-cull submitted={} resident={} ranges={}",
|
||||||
submitted,
|
submitted,
|
||||||
self.inst_tail,
|
self.inst_tail,
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,15 @@ impl Scene {
|
||||||
) -> Vec<WireModel> {
|
) -> Vec<WireModel> {
|
||||||
use acadrust::entities::Viewport;
|
use acadrust::entities::Viewport;
|
||||||
|
|
||||||
let viewports: Vec<&Viewport> = self
|
let (_, _, viewport_handles) = self.paper_viewport_handles();
|
||||||
.document
|
let viewports: Vec<&Viewport> = viewport_handles
|
||||||
.entities()
|
.iter()
|
||||||
.filter_map(|e| {
|
.filter_map(|handle| match self.document.get_entity(*handle) {
|
||||||
if let EntityType::Viewport(vp) = e {
|
Some(EntityType::Viewport(vp)) => Some(vp),
|
||||||
Some(vp)
|
_ => None,
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.filter(|vp| {
|
.filter(|vp| {
|
||||||
self.is_content_viewport_in_layout(vp, paper_block)
|
vp.common.owner_handle == paper_block
|
||||||
&& vp.status.is_on
|
&& vp.status.is_on
|
||||||
&& only_vp.map_or(true, |h| vp.common.handle == h)
|
&& only_vp.map_or(true, |h| vp.common.handle == h)
|
||||||
&& exclude_vp.map_or(true, |h| vp.common.handle != h)
|
&& exclude_vp.map_or(true, |h| vp.common.handle != h)
|
||||||
|
|
@ -328,12 +325,17 @@ impl Scene {
|
||||||
};
|
};
|
||||||
out.color = [r * 0.80, g * 0.80, b * 0.80, a * 0.85];
|
out.color = [r * 0.80, g * 0.80, b * 0.80, a * 0.85];
|
||||||
out.line_weight_px = wire.line_weight_px;
|
out.line_weight_px = wire.line_weight_px;
|
||||||
// Wire's pattern was sized for model-space coords during
|
// Wire patterns stay in their base model units. Projection
|
||||||
// tessellation; we just projected points into paper coords
|
// normally scales them with the geometry; PSLTSCALE instead
|
||||||
// (× scale), so rescale the dash pattern by the same factor
|
// keeps dash sizes constant in paper units (the on-screen path
|
||||||
// to keep dimensional consistency in the GPU shader.
|
// applies the same factor through its viewport uniform).
|
||||||
out.pattern_length = wire.pattern_length * scale;
|
let dash_scale = if self.document.header.paper_space_linetype_scaling {
|
||||||
out.pattern = wire.pattern.map(|v| v * scale);
|
1.0
|
||||||
|
} else {
|
||||||
|
scale
|
||||||
|
};
|
||||||
|
out.pattern_length = wire.pattern_length * dash_scale;
|
||||||
|
out.pattern = wire.pattern.map(|v| v * dash_scale);
|
||||||
projected.push(out);
|
projected.push(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ use std::sync::Arc;
|
||||||
use crate::scene::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
|
use crate::scene::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
|
||||||
use crate::scene::pipeline::MultiPipeline;
|
use crate::scene::pipeline::MultiPipeline;
|
||||||
use crate::scene::convert::tess_util;
|
use crate::scene::convert::tess_util;
|
||||||
use crate::scene::{HatchModel, ImageModel, MeshLodSet, Scene, Uniforms, ViewportInstance, WireModel};
|
use crate::scene::{
|
||||||
|
vp_effective_scale, HatchModel, ImageModel, MeshLodSet, NavPerfSample, Scene, Uniforms,
|
||||||
|
ViewportInstance, WireModel,
|
||||||
|
};
|
||||||
|
|
||||||
// ── Camera hover state (shader::Program::State) ───────────────────────────
|
// ── Camera hover state (shader::Program::State) ───────────────────────────
|
||||||
|
|
||||||
|
|
@ -156,6 +159,8 @@ pub struct Primitive {
|
||||||
/// `prepare` calls run before all `render` calls, so disjoint slots are
|
/// `prepare` calls run before all `render` calls, so disjoint slots are
|
||||||
/// safe.
|
/// safe.
|
||||||
pub(in crate::scene) base_slot: usize,
|
pub(in crate::scene) base_slot: usize,
|
||||||
|
/// One input-to-render sample, carried only when PERF tracing is enabled.
|
||||||
|
pub(in crate::scene) nav_perf: Option<NavPerfSample>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flags the render pipeline consumes, derived from
|
/// Flags the render pipeline consumes, derived from
|
||||||
|
|
@ -241,6 +246,7 @@ impl shader::Primitive for Primitive {
|
||||||
bounds: &Rectangle,
|
bounds: &Rectangle,
|
||||||
viewport: &Viewport,
|
viewport: &Viewport,
|
||||||
) {
|
) {
|
||||||
|
let nav_prepare_started = std::time::Instant::now();
|
||||||
let phys = viewport.physical_size();
|
let phys = viewport.physical_size();
|
||||||
let full_size = Size::new(phys.width, phys.height);
|
let full_size = Size::new(phys.width, phys.height);
|
||||||
let scale = viewport.scale_factor() as f32;
|
let scale = viewport.scale_factor() as f32;
|
||||||
|
|
@ -438,7 +444,7 @@ impl shader::Primitive for Primitive {
|
||||||
// 2D/3D sets fall through to the shared batched path below.
|
// 2D/3D sets fall through to the shared batched path below.
|
||||||
let mut arena_served = false;
|
let mut arena_served = false;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _perf = std::env::var_os("OCS_PERF").is_some();
|
let _perf = crate::perf::enabled();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _t0 = std::time::Instant::now();
|
let _t0 = std::time::Instant::now();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -455,7 +461,7 @@ impl shader::Primitive for Primitive {
|
||||||
});
|
});
|
||||||
let patch = vp.wire_patch.as_ref().map(|(_, patch)| patch);
|
let patch = vp.wire_patch.as_ref().map(|(_, patch)| patch);
|
||||||
if _perf {
|
if _perf {
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] arena-base ok={} held={} patch={:?} changes={}",
|
"[perf] arena-base ok={} held={} patch={:?} changes={}",
|
||||||
base_ok,
|
base_ok,
|
||||||
inner.wire_arena_id,
|
inner.wire_arena_id,
|
||||||
|
|
@ -725,7 +731,7 @@ impl shader::Primitive for Primitive {
|
||||||
} else {
|
} else {
|
||||||
"arena-build"
|
"arena-build"
|
||||||
};
|
};
|
||||||
eprintln!(
|
crate::perf_record!(
|
||||||
"[perf] wire {:>7.1}ms {:<18} wires={} gpu_instances={}",
|
"[perf] wire {:>7.1}ms {:<18} wires={} gpu_instances={}",
|
||||||
_t0.elapsed().as_secs_f64() * 1000.0,
|
_t0.elapsed().as_secs_f64() * 1000.0,
|
||||||
outcome,
|
outcome,
|
||||||
|
|
@ -862,6 +868,19 @@ impl shader::Primitive for Primitive {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(sample) = self.nav_perf {
|
||||||
|
crate::perf::record(format_args!(
|
||||||
|
"[perf] nav-prepare op={} space={} mode={} input={:.2}ms build={:.2}ms prepare={:.2}ms elapsed={:.2}ms viewports={}",
|
||||||
|
sample.op.label(),
|
||||||
|
sample.space,
|
||||||
|
sample.mode,
|
||||||
|
sample.input_ms,
|
||||||
|
sample.build_ms,
|
||||||
|
nav_prepare_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
sample.started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
self.viewports.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(
|
fn render(
|
||||||
|
|
@ -871,6 +890,7 @@ impl shader::Primitive for Primitive {
|
||||||
target: &iced::wgpu::TextureView,
|
target: &iced::wgpu::TextureView,
|
||||||
clip: &Rectangle<u32>,
|
clip: &Rectangle<u32>,
|
||||||
) {
|
) {
|
||||||
|
let nav_render_started = std::time::Instant::now();
|
||||||
let cw = clip.width as f32;
|
let cw = clip.width as f32;
|
||||||
let ch = clip.height as f32;
|
let ch = clip.height as f32;
|
||||||
let clip_right = clip.x + clip.width;
|
let clip_right = clip.x + clip.width;
|
||||||
|
|
@ -934,6 +954,17 @@ impl shader::Primitive for Primitive {
|
||||||
inner.viewcube.render(encoder, target, vp_clip);
|
inner.viewcube.render(encoder, target, vp_clip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(sample) = self.nav_perf {
|
||||||
|
crate::perf::record(format_args!(
|
||||||
|
"[perf] nav-render op={} space={} mode={} encode={:.2}ms elapsed={:.2}ms viewports={}",
|
||||||
|
sample.op.label(),
|
||||||
|
sample.space,
|
||||||
|
sample.mode,
|
||||||
|
nav_render_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
sample.started.elapsed().as_secs_f64() * 1000.0,
|
||||||
|
self.viewports.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1375,6 +1406,8 @@ impl Scene {
|
||||||
_hover_region: Option<usize>,
|
_hover_region: Option<usize>,
|
||||||
show_viewcube: bool,
|
show_viewcube: bool,
|
||||||
) -> Primitive {
|
) -> Primitive {
|
||||||
|
let nav_build_started = std::time::Instant::now();
|
||||||
|
let perf_nav = self.take_nav_perf();
|
||||||
// Hover comes from the scene cell driven by the app-level
|
// Hover comes from the scene cell driven by the app-level
|
||||||
// `CursorMoved` handler — the cube overlay sits above the shader
|
// `CursorMoved` handler — the cube overlay sits above the shader
|
||||||
// and would otherwise mask the move event from `Program::update`.
|
// and would otherwise mask the move event from `Program::update`.
|
||||||
|
|
@ -1396,10 +1429,19 @@ impl Scene {
|
||||||
.collect();
|
.collect();
|
||||||
// Empty viewports → blit nothing; the container background (model bg
|
// Empty viewports → blit nothing; the container background (model bg
|
||||||
// or the paper desk colour) stays visible.
|
// or the paper desk colour) stays visible.
|
||||||
|
let perf_nav = perf_nav.map(|mut sample| {
|
||||||
|
sample.build_ms = nav_build_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
sample
|
||||||
|
});
|
||||||
|
// Model panes permanently own slots 0..N. Paper starts after them so
|
||||||
|
// its sheet/content viewports never evict the Model slot's wire arena,
|
||||||
|
// textures, mesh batches and render cache during a layout-tab switch.
|
||||||
|
let base_slot = self.model_tiles.borrow().len();
|
||||||
Primitive {
|
Primitive {
|
||||||
viewports,
|
viewports,
|
||||||
bg_color,
|
bg_color,
|
||||||
base_slot: 0,
|
base_slot,
|
||||||
|
nav_perf: perf_nav,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1425,10 +1467,17 @@ impl Scene {
|
||||||
viewports: vec![],
|
viewports: vec![],
|
||||||
bg_color,
|
bg_color,
|
||||||
base_slot: tile_idx,
|
base_slot: tile_idx,
|
||||||
|
nav_perf: None,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
let active = self.active_model_tile.get();
|
let active = self.active_model_tile.get();
|
||||||
let is_active = tile_idx == active;
|
let is_active = tile_idx == active;
|
||||||
|
let nav_build_started = std::time::Instant::now();
|
||||||
|
let perf_nav = if is_active {
|
||||||
|
self.take_nav_perf()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let camera = if is_active {
|
let camera = if is_active {
|
||||||
self.camera.borrow().clone()
|
self.camera.borrow().clone()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1458,10 +1507,15 @@ impl Scene {
|
||||||
.viewport_data_for(&inst, canvas, hover_region, show_viewcube)
|
.viewport_data_for(&inst, canvas, hover_region, show_viewcube)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
|
let perf_nav = perf_nav.map(|mut sample| {
|
||||||
|
sample.build_ms = nav_build_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
|
sample
|
||||||
|
});
|
||||||
Primitive {
|
Primitive {
|
||||||
viewports,
|
viewports,
|
||||||
bg_color,
|
bg_color,
|
||||||
base_slot: tile_idx,
|
base_slot: tile_idx,
|
||||||
|
nav_perf: perf_nav,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1612,6 +1666,19 @@ impl Scene {
|
||||||
};
|
};
|
||||||
let mut uniforms =
|
let mut uniforms =
|
||||||
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display);
|
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display);
|
||||||
|
if self.document.header.paper_space_linetype_scaling
|
||||||
|
&& !inst.paper_sheet
|
||||||
|
&& inst.tile_idx.is_none()
|
||||||
|
&& inst.handle != Handle::NULL
|
||||||
|
{
|
||||||
|
if let Some(EntityType::Viewport(vp)) = self.document.get_entity(inst.handle) {
|
||||||
|
let viewport_scale =
|
||||||
|
vp_effective_scale(vp.custom_scale, vp.view_height, vp.height);
|
||||||
|
if viewport_scale.is_finite() && viewport_scale > 1e-9 {
|
||||||
|
uniforms.linetype_scale = (1.0 / viewport_scale) as f32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Crop the rotation-only RTE view-projection to the visible sub-rect.
|
// Crop the rotation-only RTE view-projection to the visible sub-rect.
|
||||||
uniforms.view_rot = crop_view_proj(uniforms.view_rot, uo, vo, us, vs);
|
uniforms.view_rot = crop_view_proj(uniforms.view_rot, uo, vo, us, vs);
|
||||||
uniforms.viewport_size = [visible_w, visible_h];
|
uniforms.viewport_size = [visible_w, visible_h];
|
||||||
|
|
@ -1655,21 +1722,18 @@ impl Scene {
|
||||||
rustc_hash::FxHashSet::default()
|
rustc_hash::FxHashSet::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let (hatches, wipeout_hatches) = if inst.paper_sheet {
|
let (hatches, wipeout_hatches, paper_images) = if inst.paper_sheet {
|
||||||
let mut v: Vec<HatchModel> = Vec::new();
|
let (hatches, wipeouts, images) = self.paper_sheet_render_models();
|
||||||
if let Some(sheet) = self.paper_sheet_fill() {
|
(hatches, wipeouts, Some(images))
|
||||||
v.push(sheet);
|
|
||||||
}
|
|
||||||
v.extend(self.paper_canvas_hatches().iter().cloned());
|
|
||||||
(Arc::new(v), self.paper_canvas_wipeouts())
|
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
self.hatch_models_for_viewport(&vp_frozen),
|
self.hatch_models_for_viewport(&vp_frozen),
|
||||||
self.wipeout_models_for_viewport(&vp_frozen),
|
self.wipeout_models_for_viewport(&vp_frozen),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let images = if inst.paper_sheet {
|
let images = if let Some(images) = paper_images {
|
||||||
self.paper_sheet_images()
|
images
|
||||||
} else {
|
} else {
|
||||||
self.images_for_viewport(&vp_frozen)
|
self.images_for_viewport(&vp_frozen)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ struct Uniforms {
|
||||||
// Transparency-display toggle: 1.0 = honour baked alpha, 0.0 = force
|
// Transparency-display toggle: 1.0 = honour baked alpha, 0.0 = force
|
||||||
// every line opaque.
|
// every line opaque.
|
||||||
transparency_enable: f32,
|
transparency_enable: f32,
|
||||||
_pad: vec2<f32>,
|
linetype_scale: f32,
|
||||||
|
_pad: f32,
|
||||||
// ── Relative-to-eye (double-single) ──────────────────────────────────
|
// ── Relative-to-eye (double-single) ──────────────────────────────────
|
||||||
// view_rot is the rotation-only view-projection; vertices subtract the eye
|
// view_rot is the rotation-only view-projection; vertices subtract the eye
|
||||||
// (eye_high + eye_low, two f32 emulating f64) before transforming, so the
|
// (eye_high + eye_low, two f32 emulating f64) before transforming, so the
|
||||||
|
|
@ -166,10 +167,13 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 {
|
||||||
// Smallest non-zero dash / gap element, in world units. Used by
|
// Smallest non-zero dash / gap element, in world units. Used by
|
||||||
// the fragment stage to decide when the pattern's finest feature
|
// the fragment stage to decide when the pattern's finest feature
|
||||||
// would render below one pixel and should collapse to a solid line.
|
// would render below one pixel and should collapse to a solid line.
|
||||||
var min_elem: f32 = in.dists.w;
|
let lt_scale = u.linetype_scale;
|
||||||
|
var min_elem: f32 = in.dists.w * lt_scale;
|
||||||
let elems = array<f32, 8>(
|
let elems = array<f32, 8>(
|
||||||
in.pat0.x, in.pat0.y, in.pat0.z, in.pat0.w,
|
in.pat0.x * lt_scale, in.pat0.y * lt_scale,
|
||||||
in.pat1.x, in.pat1.y, in.pat1.z, in.pat1.w,
|
in.pat0.z * lt_scale, in.pat0.w * lt_scale,
|
||||||
|
in.pat1.x * lt_scale, in.pat1.y * lt_scale,
|
||||||
|
in.pat1.z * lt_scale, in.pat1.w * lt_scale,
|
||||||
);
|
);
|
||||||
for (var i = 0u; i < 8u; i++) {
|
for (var i = 0u; i < 8u; i++) {
|
||||||
let e = abs(elems[i]);
|
let e = abs(elems[i]);
|
||||||
|
|
@ -186,11 +190,11 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 {
|
||||||
+ ext * hw * u.world_per_pixel;
|
+ ext * hw * u.world_per_pixel;
|
||||||
out.cap = vec2<f32>(which_end * seg_len + ext * hw, hw * side);
|
out.cap = vec2<f32>(which_end * seg_len + ext * hw, hw * side);
|
||||||
out.cap_ends = vec3<f32>(seg_len, hw_a, hw_b);
|
out.cap_ends = vec3<f32>(seg_len, hw_a, hw_b);
|
||||||
out.pattern_length = in.dists.w;
|
out.pattern_length = in.dists.w * lt_scale;
|
||||||
out.pat0 = in.pat0;
|
out.pat0 = in.pat0 * lt_scale;
|
||||||
out.pat1 = in.pat1;
|
out.pat1 = in.pat1 * lt_scale;
|
||||||
out.min_elem = min_elem;
|
out.min_elem = min_elem;
|
||||||
out.align_end = in.misc.y;
|
out.align_end = in.misc.y * lt_scale;
|
||||||
out.align_total = in.misc.z;
|
out.align_total = in.misc.z;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ struct Uniforms {
|
||||||
lwdisplay_enable: f32,
|
lwdisplay_enable: f32,
|
||||||
flat_shade: f32,
|
flat_shade: f32,
|
||||||
transparency_enable: f32,
|
transparency_enable: f32,
|
||||||
_pad: vec2<f32>,
|
linetype_scale: f32,
|
||||||
|
_pad: f32,
|
||||||
view_rot: mat4x4<f32>,
|
view_rot: mat4x4<f32>,
|
||||||
eye_high: vec3<f32>,
|
eye_high: vec3<f32>,
|
||||||
_pad_eh: f32,
|
_pad_eh: f32,
|
||||||
|
|
@ -132,10 +133,13 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
|
||||||
let ndc_offset = offset_px / (u.viewport_size * 0.5);
|
let ndc_offset = offset_px / (u.viewport_size * 0.5);
|
||||||
let final_clip = clip_pos + vec4<f32>(ndc_offset * clip_pos.w, 0.0, 0.0);
|
let final_clip = clip_pos + vec4<f32>(ndc_offset * clip_pos.w, 0.0, 0.0);
|
||||||
|
|
||||||
var min_elem: f32 = c.pattern_length;
|
let lt_scale = u.linetype_scale;
|
||||||
|
var min_elem: f32 = c.pattern_length * lt_scale;
|
||||||
let elems = array<f32, 8>(
|
let elems = array<f32, 8>(
|
||||||
c.pat0.x, c.pat0.y, c.pat0.z, c.pat0.w,
|
c.pat0.x * lt_scale, c.pat0.y * lt_scale,
|
||||||
c.pat1.x, c.pat1.y, c.pat1.z, c.pat1.w,
|
c.pat0.z * lt_scale, c.pat0.w * lt_scale,
|
||||||
|
c.pat1.x * lt_scale, c.pat1.y * lt_scale,
|
||||||
|
c.pat1.z * lt_scale, c.pat1.w * lt_scale,
|
||||||
);
|
);
|
||||||
for (var i = 0u; i < 8u; i++) {
|
for (var i = 0u; i < 8u; i++) {
|
||||||
let e = abs(elems[i]);
|
let e = abs(elems[i]);
|
||||||
|
|
@ -152,11 +156,11 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
|
||||||
+ ext * hw * u.world_per_pixel;
|
+ ext * hw * u.world_per_pixel;
|
||||||
out.cap = vec2<f32>(which_end * seg_len + ext * hw, hw * side);
|
out.cap = vec2<f32>(which_end * seg_len + ext * hw, hw * side);
|
||||||
out.cap_ends = vec3<f32>(seg_len, hw_a, hw_b);
|
out.cap_ends = vec3<f32>(seg_len, hw_a, hw_b);
|
||||||
out.pattern_length = c.pattern_length;
|
out.pattern_length = c.pattern_length * lt_scale;
|
||||||
out.pat0 = c.pat0;
|
out.pat0 = c.pat0 * lt_scale;
|
||||||
out.pat1 = c.pat1;
|
out.pat1 = c.pat1 * lt_scale;
|
||||||
out.min_elem = min_elem;
|
out.min_elem = min_elem;
|
||||||
out.align_end = c.align_end;
|
out.align_end = c.align_end * lt_scale;
|
||||||
out.align_total = c.align_total;
|
out.align_total = c.align_total;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue