perf: parallel wire tessellation with Rayon (Option C)
Extract tessellate_one body into a Send-compatible free function tessellate_entity, then drive it with rayon::into_par_iter() in wires_for_block(). Spreads file-open and post-edit tessellation across all CPU cores. Navigation frames are unaffected (Options A/B cache eliminates per-frame tessellation entirely). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9c8c9a4060
commit
432e35ded3
5 changed files with 202 additions and 176 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -14,6 +14,7 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"open",
|
"open",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
|
"rayon",
|
||||||
"rfd",
|
"rfd",
|
||||||
"truck-meshalgo",
|
"truck-meshalgo",
|
||||||
"truck-modeling",
|
"truck-modeling",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ open = "5"
|
||||||
printpdf = "0.9.1"
|
printpdf = "0.9.1"
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
rayon = "1"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows-sys = { version = "0.59", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
windows-sys = { version = "0.59", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||||
|
|
|
||||||
|
|
@ -79,18 +79,26 @@ re-creating thousands of buffers. Fixes mouse-movement lag in large files.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Option C — Parallel Tessellation (Rayon)
|
## Option C — Parallel Tessellation (Rayon) ✅ Implemented
|
||||||
|
|
||||||
**Status:** Planned
|
**Status:** Done
|
||||||
|
|
||||||
Replace the sequential `.flat_map(|e| self.tessellate_one(e))` chain in `wires_for_block()` with
|
Extracted the body of `tessellate_one` into a free function `tessellate_entity(document, selected,
|
||||||
`rayon::par_iter()`.
|
active_viewport, e)` that takes only `Send`-safe arguments (no `&self`, no `Rc`). Added `rayon = "1"`
|
||||||
|
to `Cargo.toml`.
|
||||||
|
|
||||||
|
`wires_for_block()` now:
|
||||||
|
1. Collects the filtered entity list into a `Vec<&EntityType>` sequentially (fast, no tessellation).
|
||||||
|
2. Calls `rayon::into_par_iter()` on that Vec, invoking `tessellate_entity` in parallel.
|
||||||
|
3. Flattens the results and applies the sort order via the O(1) cache from Option E.
|
||||||
|
|
||||||
|
`tessellate_one` is reduced to a thin shim that delegates to `tessellate_entity` — kept for any
|
||||||
|
remaining callers.
|
||||||
|
|
||||||
**Impact:** Spreads tessellation across all CPU cores. Improves file-open time and post-edit
|
**Impact:** Spreads tessellation across all CPU cores. Improves file-open time and post-edit
|
||||||
refresh. Does **not** fix per-frame navigation lag (Option A/B address that).
|
refresh. Does **not** fix per-frame navigation lag (Options A/B address that).
|
||||||
|
|
||||||
**Difficulty:** Easy. Requires making `tessellate_one` free of interior mutability, or collecting
|
**Difficulty:** Easy. Extracting the free function was the only meaningful change.
|
||||||
into a Vec first then parallel-processing.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
234
src/scene/mod.rs
234
src/scene/mod.rs
|
|
@ -450,7 +450,8 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut wires: Vec<WireModel> = self.document
|
// Collect visible entities sequentially (filter needs &self).
|
||||||
|
let visible: Vec<&EntityType> = self.document
|
||||||
.entities()
|
.entities()
|
||||||
.filter(|e| {
|
.filter(|e| {
|
||||||
let c = e.common();
|
let c = e.common();
|
||||||
|
|
@ -468,7 +469,16 @@ impl Scene {
|
||||||
}
|
}
|
||||||
self.belongs_to_visible_block(e.common().handle, c.owner_handle, block_handle)
|
self.belongs_to_visible_block(e.common().handle, c.owner_handle, block_handle)
|
||||||
})
|
})
|
||||||
.flat_map(|e| self.tessellate_one(e))
|
.collect();
|
||||||
|
|
||||||
|
// Tessellate in parallel across all available CPU cores.
|
||||||
|
use rayon::prelude::*;
|
||||||
|
let doc = &self.document;
|
||||||
|
let sel = &self.selected;
|
||||||
|
let avp = self.active_viewport;
|
||||||
|
let mut wires: Vec<WireModel> = visible
|
||||||
|
.into_par_iter()
|
||||||
|
.flat_map(|e| tessellate_entity(doc, sel, avp, e))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Apply draw order via the cached index (O(1) block lookup).
|
// Apply draw order via the cached index (O(1) block lookup).
|
||||||
|
|
@ -526,119 +536,7 @@ impl Scene {
|
||||||
|
|
||||||
/// Full tessellation pipeline for one entity.
|
/// Full tessellation pipeline for one entity.
|
||||||
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
|
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
|
||||||
let h = e.common().handle;
|
tessellate_entity(&self.document, &self.selected, self.active_viewport, e)
|
||||||
let sel = self.selected.contains(&h);
|
|
||||||
|
|
||||||
if let EntityType::Viewport(vp) = e {
|
|
||||||
let is_active = self.active_viewport == Some(h);
|
|
||||||
let is_locked = vp.status.locked;
|
|
||||||
let color = if sel && vp.id != 1 {
|
|
||||||
// Selected viewport — bright white highlight.
|
|
||||||
[1.0, 1.0, 1.0, 1.0]
|
|
||||||
} else if vp.id == 1 {
|
|
||||||
// Overall paper-space viewport — subtle grey.
|
|
||||||
[0.40, 0.40, 0.40, 1.0]
|
|
||||||
} else if is_active {
|
|
||||||
// Active (entered) viewport — bright yellow.
|
|
||||||
[1.0, 0.90, 0.20, 1.0]
|
|
||||||
} else if is_locked {
|
|
||||||
// Locked viewport — orange tint to indicate scale is frozen.
|
|
||||||
[0.90, 0.55, 0.10, 1.0]
|
|
||||||
} else {
|
|
||||||
// Normal user viewport — cyan.
|
|
||||||
[0.0, 0.75, 0.75, 1.0]
|
|
||||||
};
|
|
||||||
// Active viewport gets a dashed border to visually indicate MSPACE.
|
|
||||||
let (pattern_length, pattern) = if is_active {
|
|
||||||
(1.5_f32, [0.8, -0.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0_f32])
|
|
||||||
} else {
|
|
||||||
(0.0_f32, [0.0f32; 8])
|
|
||||||
};
|
|
||||||
return vec![tessellate::tessellate(
|
|
||||||
&self.document,
|
|
||||||
h,
|
|
||||||
e,
|
|
||||||
sel,
|
|
||||||
color,
|
|
||||||
pattern_length,
|
|
||||||
pattern,
|
|
||||||
1.5,
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
|
|
||||||
let (entity_color, pattern_length, pattern, line_weight_px, aci) = self.render_style(e);
|
|
||||||
let lt_scale = e.common().linetype_scale as f32;
|
|
||||||
let lt_name = self.resolved_linetype_name(e);
|
|
||||||
|
|
||||||
if let EntityType::Dimension(dim) = e {
|
|
||||||
let mut wires = tessellate::tessellate_dimension(
|
|
||||||
&self.document,
|
|
||||||
h,
|
|
||||||
dim,
|
|
||||||
sel,
|
|
||||||
entity_color,
|
|
||||||
line_weight_px,
|
|
||||||
);
|
|
||||||
for w in &mut wires { w.aci = aci; }
|
|
||||||
return wires;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let EntityType::Insert(ins) = e {
|
|
||||||
let is_mirrored = ins.x_scale() * ins.y_scale() < 0.0;
|
|
||||||
return ins
|
|
||||||
.explode_from_document(&self.document)
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(crate::modules::home::modify::explode::normalize_insert_entity)
|
|
||||||
.map(|sub| crate::modules::home::modify::explode::fix_mirrored_arc(sub, is_mirrored))
|
|
||||||
.flat_map(|sub| {
|
|
||||||
let (sub_color, sub_pattern_length, sub_pattern, sub_line_weight_px, sub_aci) =
|
|
||||||
self.render_style(&sub);
|
|
||||||
let mut wire = tessellate::tessellate(
|
|
||||||
&self.document,
|
|
||||||
h,
|
|
||||||
&sub,
|
|
||||||
sel,
|
|
||||||
sub_color,
|
|
||||||
sub_pattern_length,
|
|
||||||
sub_pattern,
|
|
||||||
sub_line_weight_px,
|
|
||||||
);
|
|
||||||
wire.name = h.value().to_string();
|
|
||||||
wire.aci = sub_aci;
|
|
||||||
vec![wire]
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut base = tessellate::tessellate(
|
|
||||||
&self.document,
|
|
||||||
h,
|
|
||||||
e,
|
|
||||||
sel,
|
|
||||||
entity_color,
|
|
||||||
pattern_length,
|
|
||||||
pattern,
|
|
||||||
line_weight_px,
|
|
||||||
);
|
|
||||||
base.aci = aci;
|
|
||||||
|
|
||||||
if let Some(clt) = crate::linetypes::complex_lt(lt_name) {
|
|
||||||
let wires = complex_lt::apply_along(
|
|
||||||
&base.name,
|
|
||||||
&base.points,
|
|
||||||
clt,
|
|
||||||
lt_scale.max(1e-4),
|
|
||||||
entity_color,
|
|
||||||
sel,
|
|
||||||
base.line_weight_px,
|
|
||||||
);
|
|
||||||
if !wires.is_empty() {
|
|
||||||
return wires;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vec![base]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_space_block_handle(&self) -> Handle {
|
fn model_space_block_handle(&self) -> Handle {
|
||||||
|
|
@ -2535,3 +2433,109 @@ fn clip_polyline_to_rect(
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Parallel tessellation free function ──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Takes only the `Send + Sync` data needed for tessellation so that
|
||||||
|
// `wires_for_block` can dispatch work across rayon's thread pool without
|
||||||
|
// requiring `Scene` (which contains `Rc<RefCell<...>>` and is `!Send`) to
|
||||||
|
// cross thread boundaries.
|
||||||
|
|
||||||
|
fn tessellate_entity(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
selected: &HashSet<Handle>,
|
||||||
|
active_viewport: Option<Handle>,
|
||||||
|
e: &EntityType,
|
||||||
|
) -> Vec<WireModel> {
|
||||||
|
let h = e.common().handle;
|
||||||
|
let sel = selected.contains(&h);
|
||||||
|
|
||||||
|
if let EntityType::Viewport(vp) = e {
|
||||||
|
let is_active = active_viewport == Some(h);
|
||||||
|
let is_locked = vp.status.locked;
|
||||||
|
let color = if sel && vp.id != 1 {
|
||||||
|
[1.0, 1.0, 1.0, 1.0]
|
||||||
|
} else if vp.id == 1 {
|
||||||
|
[0.40, 0.40, 0.40, 1.0]
|
||||||
|
} else if is_active {
|
||||||
|
[1.0, 0.90, 0.20, 1.0]
|
||||||
|
} else if is_locked {
|
||||||
|
[0.90, 0.55, 0.10, 1.0]
|
||||||
|
} else {
|
||||||
|
[0.0, 0.75, 0.75, 1.0]
|
||||||
|
};
|
||||||
|
let (pattern_length, pattern) = if is_active {
|
||||||
|
(1.5_f32, [0.8, -0.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0_f32])
|
||||||
|
} else {
|
||||||
|
(0.0_f32, [0.0f32; 8])
|
||||||
|
};
|
||||||
|
return vec![tessellate::tessellate(
|
||||||
|
document, h, e, sel, color, pattern_length, pattern, 1.5,
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
|
||||||
|
let (entity_color, pattern_length, pattern, line_weight_px, aci) =
|
||||||
|
render::render_style_for(document, e);
|
||||||
|
let lt_scale = e.common().linetype_scale as f32;
|
||||||
|
let lt_name = render::linetype_name_for(document, e);
|
||||||
|
|
||||||
|
if let EntityType::Dimension(dim) = e {
|
||||||
|
let mut wires = tessellate::tessellate_dimension(
|
||||||
|
document, h, dim, sel, entity_color, line_weight_px,
|
||||||
|
);
|
||||||
|
for w in &mut wires {
|
||||||
|
w.aci = aci;
|
||||||
|
}
|
||||||
|
return wires;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let EntityType::Insert(ins) = e {
|
||||||
|
let is_mirrored = ins.x_scale() * ins.y_scale() < 0.0;
|
||||||
|
return ins
|
||||||
|
.explode_from_document(document)
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(crate::modules::home::modify::explode::normalize_insert_entity)
|
||||||
|
.map(|sub| crate::modules::home::modify::explode::fix_mirrored_arc(sub, is_mirrored))
|
||||||
|
.flat_map(|sub| {
|
||||||
|
let (sub_color, sub_pattern_length, sub_pattern, sub_line_weight_px, sub_aci) =
|
||||||
|
render::render_style_for(document, &sub);
|
||||||
|
let mut wire = tessellate::tessellate(
|
||||||
|
document,
|
||||||
|
h,
|
||||||
|
&sub,
|
||||||
|
sel,
|
||||||
|
sub_color,
|
||||||
|
sub_pattern_length,
|
||||||
|
sub_pattern,
|
||||||
|
sub_line_weight_px,
|
||||||
|
);
|
||||||
|
wire.name = h.value().to_string();
|
||||||
|
wire.aci = sub_aci;
|
||||||
|
vec![wire]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut base = tessellate::tessellate(
|
||||||
|
document, h, e, sel, entity_color, pattern_length, pattern, line_weight_px,
|
||||||
|
);
|
||||||
|
base.aci = aci;
|
||||||
|
|
||||||
|
if let Some(clt) = crate::linetypes::complex_lt(lt_name) {
|
||||||
|
let wires = complex_lt::apply_along(
|
||||||
|
&base.name,
|
||||||
|
&base.points,
|
||||||
|
clt,
|
||||||
|
lt_scale.max(1e-4),
|
||||||
|
entity_color,
|
||||||
|
sel,
|
||||||
|
base.line_weight_px,
|
||||||
|
);
|
||||||
|
if !wires.is_empty() {
|
||||||
|
return wires;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![base]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
use acadrust::tables::LineType;
|
use acadrust::tables::LineType;
|
||||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{CadDocument, EntityType, Handle};
|
||||||
use glam::Mat4;
|
use glam::Mat4;
|
||||||
use iced::mouse;
|
use iced::mouse;
|
||||||
use iced::widget::shader::{self, Viewport};
|
use iced::widget::shader::{self, Viewport};
|
||||||
|
|
@ -157,65 +157,77 @@ impl shader::Primitive for Primitive {
|
||||||
impl Scene {
|
impl Scene {
|
||||||
/// Returns (entity_color, pattern_length, pattern, line_weight_px, aci).
|
/// Returns (entity_color, pattern_length, pattern, line_weight_px, aci).
|
||||||
pub(super) fn render_style(&self, e: &EntityType) -> ([f32; 4], f32, [f32; 8], f32, u8) {
|
pub(super) fn render_style(&self, e: &EntityType) -> ([f32; 4], f32, [f32; 8], f32, u8) {
|
||||||
let layer_name = &e.common().layer;
|
render_style_for(&self.document, e)
|
||||||
let (entity_color, aci) = {
|
|
||||||
let ec = &e.common().color;
|
|
||||||
let resolved = if *ec == AcadColor::ByLayer {
|
|
||||||
self.document
|
|
||||||
.layers
|
|
||||||
.get(layer_name)
|
|
||||||
.map(|l| &l.color)
|
|
||||||
.unwrap_or(&AcadColor::WHITE)
|
|
||||||
} else {
|
|
||||||
ec
|
|
||||||
};
|
|
||||||
let aci = match resolved {
|
|
||||||
AcadColor::Index(i) => *i,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
let [r, g, b, _] = tessellate::aci_to_rgba(resolved);
|
|
||||||
let alpha = 1.0 - e.common().transparency.as_percent() as f32;
|
|
||||||
([r, g, b, alpha], aci)
|
|
||||||
};
|
|
||||||
|
|
||||||
let lt_name = self.resolved_linetype_name(e);
|
|
||||||
let lt_scale = e.common().linetype_scale as f32;
|
|
||||||
let (pattern_length, pattern) =
|
|
||||||
resolve_pattern(&self.document.line_types, lt_name, lt_scale);
|
|
||||||
|
|
||||||
let line_weight_px = {
|
|
||||||
let ew = &e.common().line_weight;
|
|
||||||
let resolved = match ew {
|
|
||||||
LineWeight::ByLayer | LineWeight::ByBlock | LineWeight::Default => self
|
|
||||||
.document
|
|
||||||
.layers
|
|
||||||
.get(layer_name)
|
|
||||||
.map(|l| &l.line_weight)
|
|
||||||
.unwrap_or(&LineWeight::Default),
|
|
||||||
_ => ew,
|
|
||||||
};
|
|
||||||
const MM_TO_PX: f32 = 96.0 / 25.4;
|
|
||||||
resolved
|
|
||||||
.millimeters()
|
|
||||||
.map(|mm| (mm as f32 * MM_TO_PX).max(1.0))
|
|
||||||
.unwrap_or(1.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
(entity_color, pattern_length, pattern, line_weight_px, aci)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn resolved_linetype_name<'a>(&'a self, e: &'a EntityType) -> &'a str {
|
}
|
||||||
let elt = &e.common().linetype;
|
|
||||||
if elt.is_empty() || elt.eq_ignore_ascii_case("bylayer") {
|
// ── Document-only render-style helpers (no &self, safe to call from parallel contexts) ──
|
||||||
self.document
|
|
||||||
|
/// Resolves the effective linetype name for an entity, falling back to the
|
||||||
|
/// layer's linetype when the entity's own linetype is "ByLayer".
|
||||||
|
pub(super) fn linetype_name_for<'a>(document: &'a CadDocument, e: &'a EntityType) -> &'a str {
|
||||||
|
let elt = &e.common().linetype;
|
||||||
|
if elt.is_empty() || elt.eq_ignore_ascii_case("bylayer") {
|
||||||
|
document
|
||||||
|
.layers
|
||||||
|
.get(&e.common().layer)
|
||||||
|
.map(|l| l.line_type.as_str())
|
||||||
|
.unwrap_or("Continuous")
|
||||||
|
} else {
|
||||||
|
elt.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `(entity_color, pattern_length, pattern, line_weight_px, aci)` for
|
||||||
|
/// an entity, resolving ByLayer/ByBlock colour and linetype from the document.
|
||||||
|
pub(super) fn render_style_for(
|
||||||
|
document: &CadDocument,
|
||||||
|
e: &EntityType,
|
||||||
|
) -> ([f32; 4], f32, [f32; 8], f32, u8) {
|
||||||
|
let layer_name = &e.common().layer;
|
||||||
|
let (entity_color, aci) = {
|
||||||
|
let ec = &e.common().color;
|
||||||
|
let resolved = if *ec == AcadColor::ByLayer {
|
||||||
|
document
|
||||||
.layers
|
.layers
|
||||||
.get(&e.common().layer)
|
.get(layer_name)
|
||||||
.map(|l| l.line_type.as_str())
|
.map(|l| &l.color)
|
||||||
.unwrap_or("Continuous")
|
.unwrap_or(&AcadColor::WHITE)
|
||||||
} else {
|
} else {
|
||||||
elt.as_str()
|
ec
|
||||||
}
|
};
|
||||||
}
|
let aci = match resolved {
|
||||||
|
AcadColor::Index(i) => *i,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
let [r, g, b, _] = tessellate::aci_to_rgba(resolved);
|
||||||
|
let alpha = 1.0 - e.common().transparency.as_percent() as f32;
|
||||||
|
([r, g, b, alpha], aci)
|
||||||
|
};
|
||||||
|
|
||||||
|
let lt_name = linetype_name_for(document, e);
|
||||||
|
let lt_scale = e.common().linetype_scale as f32;
|
||||||
|
let (pattern_length, pattern) = resolve_pattern(&document.line_types, lt_name, lt_scale);
|
||||||
|
|
||||||
|
let line_weight_px = {
|
||||||
|
let ew = &e.common().line_weight;
|
||||||
|
let resolved = match ew {
|
||||||
|
LineWeight::ByLayer | LineWeight::ByBlock | LineWeight::Default => document
|
||||||
|
.layers
|
||||||
|
.get(layer_name)
|
||||||
|
.map(|l| &l.line_weight)
|
||||||
|
.unwrap_or(&LineWeight::Default),
|
||||||
|
_ => ew,
|
||||||
|
};
|
||||||
|
const MM_TO_PX: f32 = 96.0 / 25.4;
|
||||||
|
resolved
|
||||||
|
.millimeters()
|
||||||
|
.map(|mm| (mm as f32 * MM_TO_PX).max(1.0))
|
||||||
|
.unwrap_or(1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
(entity_color, pattern_length, pattern, line_weight_px, aci)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Primitive builder helpers (called by ViewportPane's shader::Program impl) ──
|
// ── Primitive builder helpers (called by ViewportPane's shader::Program impl) ──
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue