diff --git a/Cargo.lock b/Cargo.lock index 3196cf3c..3873b4f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "image", "open", "printpdf", + "rayon", "rfd", "truck-meshalgo", "truck-modeling", diff --git a/Cargo.toml b/Cargo.toml index 55d7071e..e7f17c73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ open = "5" printpdf = "0.9.1" flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } +rayon = "1" [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { version = "0.59", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 05d0158e..fc0f3661 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -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 -`rayon::par_iter()`. +Extracted the body of `tessellate_one` into a free function `tessellate_entity(document, selected, +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 -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 -into a Vec first then parallel-processing. +**Difficulty:** Easy. Extracting the free function was the only meaningful change. --- diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 2685d531..84167c4b 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -450,7 +450,8 @@ impl Scene { } } - let mut wires: Vec = self.document + // Collect visible entities sequentially (filter needs &self). + let visible: Vec<&EntityType> = self.document .entities() .filter(|e| { let c = e.common(); @@ -468,7 +469,16 @@ impl Scene { } 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 = visible + .into_par_iter() + .flat_map(|e| tessellate_entity(doc, sel, avp, e)) .collect(); // Apply draw order via the cached index (O(1) block lookup). @@ -526,119 +536,7 @@ impl Scene { /// Full tessellation pipeline for one entity. fn tessellate_one(&self, e: &EntityType) -> Vec { - let h = e.common().handle; - 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] + tessellate_entity(&self.document, &self.selected, self.active_viewport, e) } fn model_space_block_handle(&self) -> Handle { @@ -2535,3 +2433,109 @@ fn clip_polyline_to_rect( 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>` and is `!Send`) to +// cross thread boundaries. + +fn tessellate_entity( + document: &acadrust::CadDocument, + selected: &HashSet, + active_viewport: Option, + e: &EntityType, +) -> Vec { + 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] +} + diff --git a/src/scene/render.rs b/src/scene/render.rs index 68d07765..e7faa11a 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -3,7 +3,7 @@ use acadrust::tables::LineType; use acadrust::types::{Color as AcadColor, LineWeight}; -use acadrust::{EntityType, Handle}; +use acadrust::{CadDocument, EntityType, Handle}; use glam::Mat4; use iced::mouse; use iced::widget::shader::{self, Viewport}; @@ -157,65 +157,77 @@ impl shader::Primitive for Primitive { impl Scene { /// 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) { - let layer_name = &e.common().layer; - 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) + render_style_for(&self.document, e) } - 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") { - self.document +} + +// ── Document-only render-style helpers (no &self, safe to call from parallel contexts) ── + +/// 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 - .get(&e.common().layer) - .map(|l| l.line_type.as_str()) - .unwrap_or("Continuous") + .get(layer_name) + .map(|l| &l.color) + .unwrap_or(&AcadColor::WHITE) } 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) ──