perf: build hatch/image/mesh caches off the UI thread
Parsing + DerivedCaches construction now runs entirely in a dedicated OS thread inside open_path(). FileOpened handler only assigns the pre-built caches, keeping the UI thread free during file load. - Add build_derived_caches(): parallel hatch + mesh tessellation via rayon - Extend Message::FileOpened / open_path return type with DerivedCaches - Drop populate_*_from_document() calls from FileOpened handler
This commit is contained in:
parent
45f9f96c93
commit
2da3640b45
4 changed files with 172 additions and 51 deletions
|
|
@ -212,7 +212,7 @@ pub enum DsField {
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Tick(Instant),
|
Tick(Instant),
|
||||||
OpenFile,
|
OpenFile,
|
||||||
FileOpened(Result<(String, PathBuf, CadDocument), String>),
|
FileOpened(Result<(String, PathBuf, CadDocument, crate::scene::DerivedCaches), String>),
|
||||||
SaveFile,
|
SaveFile,
|
||||||
SaveAs,
|
SaveAs,
|
||||||
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
// ── Custom Save-As dialog ─────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ impl H7CAD {
|
||||||
|
|
||||||
Message::OpenFile => Task::perform(crate::io::pick_and_open(), Message::FileOpened),
|
Message::OpenFile => Task::perform(crate::io::pick_and_open(), Message::FileOpened),
|
||||||
|
|
||||||
Message::FileOpened(Ok((name, path, doc))) => {
|
Message::FileOpened(Ok((name, path, doc, caches))) => {
|
||||||
let entity_count = doc.entities().count();
|
let entity_count = doc.entities().count();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Opened \"{name}\" — {entity_count} entities"));
|
.push_output(&format!("Opened \"{name}\" — {entity_count} entities"));
|
||||||
|
|
@ -87,10 +87,12 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tabs[i].scene.compute_and_set_world_offset();
|
// Caches were built on the background thread inside open_path().
|
||||||
self.tabs[i].scene.populate_hatches_from_document();
|
self.tabs[i].scene.world_offset = caches.world_offset;
|
||||||
self.tabs[i].scene.populate_images_from_document();
|
self.tabs[i].scene.local_extent_max = caches.local_extent_max;
|
||||||
self.tabs[i].scene.populate_meshes_from_document();
|
self.tabs[i].scene.hatches = caches.hatches;
|
||||||
|
self.tabs[i].scene.images = caches.images;
|
||||||
|
self.tabs[i].scene.meshes = caches.meshes;
|
||||||
self.tabs[i].scene.selected = std::collections::HashSet::new();
|
self.tabs[i].scene.selected = std::collections::HashSet::new();
|
||||||
self.tabs[i].scene.preview_wires = vec![];
|
self.tabs[i].scene.preview_wires = vec![];
|
||||||
self.tabs[i].scene.current_layout = "Model".to_string();
|
self.tabs[i].scene.current_layout = "Model".to_string();
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,14 @@ pub mod xref;
|
||||||
use acadrust::entities::{Dimension, EntityType};
|
use acadrust::entities::{Dimension, EntityType};
|
||||||
use acadrust::io::dwg::DwgReader;
|
use acadrust::io::dwg::DwgReader;
|
||||||
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
||||||
|
use crate::scene::DerivedCaches;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
// ── Open ──────────────────────────────────────────────────────────────────
|
// ── Open ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Show a file-open dialog and load the selected DWG or DXF file.
|
/// Show a file-open dialog and load the selected DWG or DXF file.
|
||||||
/// Returns `(filename, path, document)` or an error string.
|
/// Returns `(filename, path, document, caches)` or an error string.
|
||||||
pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument), String> {
|
pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||||
let handle = rfd::AsyncFileDialog::new()
|
let handle = rfd::AsyncFileDialog::new()
|
||||||
.set_title("Open CAD file")
|
.set_title("Open CAD file")
|
||||||
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
|
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
|
||||||
|
|
@ -40,13 +41,22 @@ pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument), String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a CAD file from a known path (used by recent files).
|
/// Load a CAD file from a known path (used by recent files).
|
||||||
pub async fn open_path(path: PathBuf) -> Result<(String, PathBuf, CadDocument), String> {
|
/// Parsing and cache building run on a dedicated OS thread so the async
|
||||||
|
/// executor stays free for rendering during the load.
|
||||||
|
pub async fn open_path(path: PathBuf) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||||
let name = path
|
let name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(|n| n.to_string_lossy().into_owned())
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
.unwrap_or_else(|| "unknown".into());
|
.unwrap_or_else(|| "unknown".into());
|
||||||
let doc = load_file(&path)?;
|
let path2 = path.clone();
|
||||||
Ok((name, path, doc))
|
let (doc, caches) = std::thread::spawn(move || -> Result<_, String> {
|
||||||
|
let doc = load_file(&path2)?;
|
||||||
|
let caches = crate::scene::build_derived_caches(&doc);
|
||||||
|
Ok((doc, caches))
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.map_err(|_| "parser thread panicked".to_string())??;
|
||||||
|
Ok((name, path, doc, caches))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
||||||
|
|
|
||||||
189
src/scene/mod.rs
189
src/scene/mod.rs
|
|
@ -60,6 +60,109 @@ use std::sync::Arc;
|
||||||
/// GPU Pipeline to skip re-uploading geometry when switching tabs.
|
/// GPU Pipeline to skip re-uploading geometry when switching tabs.
|
||||||
static GEOMETRY_EPOCH: AtomicU64 = AtomicU64::new(1);
|
static GEOMETRY_EPOCH: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
|
/// Pre-built entity caches returned by [`build_derived_caches`].
|
||||||
|
/// Produced in the file-load background task so the UI thread only assigns.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DerivedCaches {
|
||||||
|
pub world_offset: [f64; 3],
|
||||||
|
pub local_extent_max: f32,
|
||||||
|
pub hatches: HashMap<Handle, HatchModel>,
|
||||||
|
pub images: HashMap<Handle, ImageModel>,
|
||||||
|
pub meshes: HashMap<Handle, MeshModel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build hatch / image / mesh caches from a document without needing `&mut Scene`.
|
||||||
|
/// Intended to run on a background thread during file load.
|
||||||
|
pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
|
// world_offset
|
||||||
|
let h = &doc.header;
|
||||||
|
let (min, max) = (h.model_space_extents_min, h.model_space_extents_max);
|
||||||
|
let valid = min.x < max.x && min.y < max.y;
|
||||||
|
let (world_offset, local_extent_max) = if valid {
|
||||||
|
let offset = [(min.x + max.x) * 0.5, (min.y + max.y) * 0.5, (min.z + max.z) * 0.5];
|
||||||
|
let hw = ((max.x - min.x) * 0.5) as f32;
|
||||||
|
let hh = ((max.y - min.y) * 0.5) as f32;
|
||||||
|
let hz = ((max.z - min.z) * 0.5).max(1.0) as f32;
|
||||||
|
(offset, hw.max(hh).max(hz) * 10.0)
|
||||||
|
} else {
|
||||||
|
([0.0; 3], 1e9_f32)
|
||||||
|
};
|
||||||
|
|
||||||
|
// model-space block handle (same logic as Scene::model_space_block_handle)
|
||||||
|
let model_block = doc.objects.values().find_map(|obj| {
|
||||||
|
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
||||||
|
if l.name == "Model" && !l.block_record.is_null() { Some(l.block_record) } else { None }
|
||||||
|
} else { None }
|
||||||
|
}).unwrap_or_else(|| {
|
||||||
|
doc.block_records.get("*Model_Space").map(|br| br.handle).unwrap_or(Handle::NULL)
|
||||||
|
});
|
||||||
|
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
// hatches
|
||||||
|
let hatch_entries: Vec<(Handle, EntityType)> = doc.entities()
|
||||||
|
.filter_map(|e| match e {
|
||||||
|
EntityType::Hatch(h2) => Some((h2.common.handle, e.clone())),
|
||||||
|
EntityType::Solid(s) => Some((s.common.handle, e.clone())),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let hatches: HashMap<Handle, HatchModel> = hatch_entries
|
||||||
|
.into_par_iter()
|
||||||
|
.filter_map(|(handle, kind)| {
|
||||||
|
let owner = kind.common().owner_handle;
|
||||||
|
let offset = if owner == model_block { world_offset } else { [0.0; 3] };
|
||||||
|
let model = match &kind {
|
||||||
|
EntityType::Hatch(dxf) => {
|
||||||
|
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||||
|
Scene::hatch_model_from_dxf(dxf, color, offset)
|
||||||
|
}
|
||||||
|
EntityType::Solid(solid) => {
|
||||||
|
let color = tessellate::aci_to_rgba(&solid.common.color);
|
||||||
|
Some(Scene::solid_hatch_model(solid, color, offset))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
model.map(|m| (handle, m))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// images
|
||||||
|
let images: HashMap<Handle, ImageModel> = doc.entities()
|
||||||
|
.filter_map(|e| {
|
||||||
|
if let EntityType::RasterImage(img) = e {
|
||||||
|
ImageModel::from_raster_image(img).map(|m| (img.common.handle, m))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// meshes (parallel tessellation)
|
||||||
|
let mesh_entries: Vec<(Handle, EntityType)> = doc.entities()
|
||||||
|
.filter_map(|e| match e {
|
||||||
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) =>
|
||||||
|
Some((e.common().handle, e.clone())),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let meshes: HashMap<Handle, MeshModel> = mesh_entries
|
||||||
|
.into_par_iter()
|
||||||
|
.filter_map(|(handle, entity)| {
|
||||||
|
let color = tessellate::aci_to_rgba(&entity.common().color);
|
||||||
|
let model = match &entity {
|
||||||
|
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color),
|
||||||
|
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color),
|
||||||
|
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
model.map(|m| (handle, m))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
DerivedCaches { world_offset, local_extent_max, hatches, images, meshes }
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
pub camera: Rc<RefCell<Camera>>,
|
pub camera: Rc<RefCell<Camera>>,
|
||||||
pub selection: Rc<RefCell<SelectionState>>,
|
pub selection: Rc<RefCell<SelectionState>>,
|
||||||
|
|
@ -2043,6 +2146,7 @@ impl Scene {
|
||||||
self.hatches.clear();
|
self.hatches.clear();
|
||||||
|
|
||||||
let model_block = self.model_space_block_handle();
|
let model_block = self.model_space_block_handle();
|
||||||
|
let world_offset = self.world_offset;
|
||||||
|
|
||||||
let entries: Vec<(Handle, EntityType)> = self
|
let entries: Vec<(Handle, EntityType)> = self
|
||||||
.document
|
.document
|
||||||
|
|
@ -2054,26 +2158,29 @@ impl Scene {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for (handle, kind) in entries {
|
use rayon::prelude::*;
|
||||||
// Paper-space entities live in sheet coordinates — world_offset must not
|
self.hatches = entries
|
||||||
// be applied to them. Only model-space entities need the shift.
|
.into_par_iter()
|
||||||
let owner = kind.common().owner_handle;
|
.filter_map(|(handle, kind)| {
|
||||||
let offset = if owner == model_block { self.world_offset } else { [0.0; 3] };
|
// Paper-space entities live in sheet coordinates — world_offset must not
|
||||||
let model = match &kind {
|
// be applied to them. Only model-space entities need the shift.
|
||||||
EntityType::Hatch(dxf) => {
|
let owner = kind.common().owner_handle;
|
||||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
let offset = if owner == model_block { world_offset } else { [0.0; 3] };
|
||||||
Self::hatch_model_from_dxf(dxf, color, offset)
|
let model = match &kind {
|
||||||
}
|
EntityType::Hatch(dxf) => {
|
||||||
EntityType::Solid(solid) => {
|
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||||
let color = tessellate::aci_to_rgba(&solid.common.color);
|
Self::hatch_model_from_dxf(dxf, color, offset)
|
||||||
Some(Self::solid_hatch_model(solid, color, offset))
|
}
|
||||||
}
|
EntityType::Solid(solid) => {
|
||||||
_ => None,
|
let color = tessellate::aci_to_rgba(&solid.common.color);
|
||||||
};
|
Some(Self::solid_hatch_model(solid, color, offset))
|
||||||
if let Some(m) = model {
|
}
|
||||||
self.hatches.insert(handle, m);
|
_ => None,
|
||||||
}
|
};
|
||||||
}
|
model.map(|m| (handle, m))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2084,32 +2191,34 @@ impl Scene {
|
||||||
/// `Solid3D` entity is represented in the mesh cache.
|
/// `Solid3D` entity is represented in the mesh cache.
|
||||||
pub fn populate_meshes_from_document(&mut self) {
|
pub fn populate_meshes_from_document(&mut self) {
|
||||||
self.meshes.clear();
|
self.meshes.clear();
|
||||||
// Collect all ACIS-bearing entities: Solid3D, Region, Body.
|
// Collect all ACIS-bearing entities with their color resolved now,
|
||||||
let entries: Vec<(Handle, EntityType)> = self
|
// so the parallel phase only sees owned data.
|
||||||
|
let entries: Vec<(Handle, EntityType, [f32; 4])> = self
|
||||||
.document
|
.document
|
||||||
.entities()
|
.entities()
|
||||||
.filter_map(|e| match e {
|
.filter_map(|e| match e {
|
||||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) =>
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) => {
|
||||||
Some((e.common().handle, e.clone())),
|
let color = tessellate::aci_to_rgba(&e.common().color);
|
||||||
|
Some((e.common().handle, e.clone(), color))
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
for (handle, entity) in entries {
|
|
||||||
let color = if let Some(e) = self.document.get_entity(handle) {
|
use rayon::prelude::*;
|
||||||
tessellate::aci_to_rgba(&e.common().color)
|
self.meshes = entries
|
||||||
} else {
|
.into_par_iter()
|
||||||
[0.7, 0.7, 0.7, 1.0]
|
.filter_map(|(handle, entity, color)| {
|
||||||
};
|
let model = match &entity {
|
||||||
let model = match &entity {
|
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color),
|
||||||
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color),
|
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color),
|
||||||
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color),
|
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color),
|
||||||
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color),
|
_ => None,
|
||||||
_ => None,
|
};
|
||||||
};
|
model.map(|m| (handle, m))
|
||||||
if let Some(m) = model {
|
})
|
||||||
self.meshes.insert(handle, m);
|
.collect();
|
||||||
}
|
|
||||||
}
|
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue