forked from 1b-agent/opencadstudio-upstream
fix(scene): unfreeze open on huge xref Inserts
A real DWG with an xref containing ~74k sub-entities froze on the first render: Insert tessellation exploded the block, style-resolved every sub serially, and one bad sub could hang the loop forever. - Insert short-circuits to an insertion marker when the referenced block has more than INSERT_SUB_LIMIT (5_000) sub-entities. Documented as temporary in RENDERING_OPTIMIZATION.md. - Re-run purge_corrupt_entities after resolve_xrefs so parser-garbage inside xref content is caught (initial purge runs before xref load). - build_derived_caches: single pass over entities collecting handles per kind; tessellation reads via doc.get_entity instead of cloning every EntityType. Image decode now parallel too. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e985fdc9c
commit
c61d648dc7
3 files changed
+119
-32
No files matched your search
@@ -272,4 +272,42 @@ Phase 4.2 Hi-Z occlusion high complexity, 3D only, last
|
||||
proportional to visible fraction.
|
||||
- **Phase 2 target:** Pan/zoom frame cost O(visible) not O(total).
|
||||
- **Phase 3 target:** Complex solid scene at far zoom → mesh triangle count < 10% of full detail.
|
||||
|
||||
---
|
||||
|
||||
## TEMPORARY: Insert sub-entity count limit (revert when better fix lands)
|
||||
|
||||
`tessellate_entity` in `src/scene/mod.rs` has a guard that short-circuits any
|
||||
Insert whose referenced block contains more than `INSERT_SUB_LIMIT` (currently
|
||||
5,000) sub-entities. The Insert is rendered as an insertion marker only — no
|
||||
explode, no sub tessellation.
|
||||
|
||||
**Why this exists:** A real-world DWG was found where an xref block contained
|
||||
~74k sub-entities. Opening the file froze the UI on the first render because
|
||||
`tessellate_entity` exploded the block and serially style-resolved every
|
||||
sub-entity (and any single bad sub could hang tessellation forever). The guard
|
||||
made the file openable.
|
||||
|
||||
**Why it should go away:** The limit hides legitimate geometry. Large xrefs are
|
||||
common in civil/infrastructure drawings — the user shouldn't have to see them
|
||||
as crosses.
|
||||
|
||||
**Proper fixes that would let us drop this guard:**
|
||||
1. Per-block tessellation cache — tessellate a block once, then per-Insert
|
||||
apply the transform to cached wire vertices. Helps when the same block is
|
||||
inserted multiple times; doesn't fully solve single-Insert huge blocks.
|
||||
2. Lazy / background Insert tessellation — show marker first, fill in detail
|
||||
on a background thread, redraw when ready.
|
||||
3. Per-sub-entity hardening — find why a single sub can hang tessellation
|
||||
(likely Spline NURBS evaluation or malformed boundary edge) and fix the
|
||||
root cause; combine with sub-count budget per frame instead of a hard cap.
|
||||
4. xref attach metadata respected — DWG carries an "unloaded" state for xref
|
||||
blocks; honour it instead of always inlining xref content on open.
|
||||
|
||||
When any of (1)–(3) lands, delete the `INSERT_SUB_LIMIT` block in
|
||||
`tessellate_entity` (search for `INSERT_SUB_LIMIT`).
|
||||
|
||||
Related companion fix in `src/app/update.rs`: after `resolve_xrefs`, a second
|
||||
`purge_corrupt_entities` pass runs against the newly inlined xref content.
|
||||
That one is not a hack — it should stay even after the Insert guard goes.
|
||||
- **Phase 4 target:** GPU-cull overhead < 0.5 ms for 1M entity scene.
|
||||
@@ -132,6 +132,17 @@ impl H7CAD {
|
||||
if let Some(base_dir) = path.parent() {
|
||||
let xrefs =
|
||||
crate::io::xref::resolve_xrefs(&mut self.tabs[i].scene.document, base_dir);
|
||||
// xref content arrives un-purged: parser-garbage entities
|
||||
// inside the referenced file can trigger infinite loops in
|
||||
// tessellation. Run the corrupt-entity guard again.
|
||||
let extra_dropped = crate::io::purge_corrupt_entities(
|
||||
&mut self.tabs[i].scene.document,
|
||||
);
|
||||
if extra_dropped > 0 {
|
||||
self.command_line.push_error(&format!(
|
||||
"Warning: {extra_dropped} corrupt xref entities dropped"
|
||||
));
|
||||
}
|
||||
for info in &xrefs {
|
||||
match info.status {
|
||||
crate::io::xref::XrefStatus::Loaded => {
|
||||
|
||||
+70
-32
@@ -120,25 +120,36 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||
|
||||
use rayon::prelude::*;
|
||||
|
||||
// Single pass over entities — collect only handles per kind. No clones.
|
||||
// Heavy tessellation runs in parallel below, reading entities via
|
||||
// `doc.get_entity(h)` (O(1) HashMap lookup).
|
||||
let mut hatch_handles: Vec<Handle> = Vec::new();
|
||||
let mut image_handles: Vec<Handle> = Vec::new();
|
||||
let mut mesh_handles: Vec<Handle> = Vec::new();
|
||||
for e in doc.entities() {
|
||||
let h = e.common().handle;
|
||||
match e {
|
||||
EntityType::Hatch(_) | EntityType::Solid(_) => hatch_handles.push(h),
|
||||
EntityType::RasterImage(_) => image_handles.push(h),
|
||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) => {
|
||||
mesh_handles.push(h)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 hatches: HashMap<Handle, HatchModel> = hatch_handles
|
||||
.par_iter()
|
||||
.filter_map(|&handle| {
|
||||
let e = doc.get_entity(handle)?;
|
||||
let owner = e.common().owner_handle;
|
||||
let offset = if owner == model_block {
|
||||
world_offset
|
||||
} else {
|
||||
[0.0; 3]
|
||||
};
|
||||
let model = match &kind {
|
||||
let model = match e {
|
||||
EntityType::Hatch(dxf) => {
|
||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||
Scene::hatch_model_from_dxf(dxf, color, offset)
|
||||
@@ -154,11 +165,11 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||
.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))
|
||||
let images: HashMap<Handle, ImageModel> = image_handles
|
||||
.par_iter()
|
||||
.filter_map(|&handle| {
|
||||
if let EntityType::RasterImage(img) = doc.get_entity(handle)? {
|
||||
ImageModel::from_raster_image(img).map(|m| (handle, m))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -166,20 +177,12 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||
.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 {
|
||||
let meshes: HashMap<Handle, MeshModel> = mesh_handles
|
||||
.par_iter()
|
||||
.filter_map(|&handle| {
|
||||
let e = doc.get_entity(handle)?;
|
||||
let color = tessellate::aci_to_rgba(&e.common().color);
|
||||
let model = match e {
|
||||
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),
|
||||
@@ -3814,6 +3817,41 @@ fn tessellate_entity(
|
||||
}
|
||||
|
||||
if let EntityType::Insert(ins) = e {
|
||||
// Bail out for huge blocks (typically xref imports): exploding +
|
||||
// tessellating tens of thousands of sub-entities freezes the UI, and
|
||||
// any single bad sub can hang tessellation forever. Draw only an
|
||||
// insertion marker so the user sees the block exists.
|
||||
const INSERT_SUB_LIMIT: usize = 5_000;
|
||||
let sub_count = document
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.map(|br| br.entity_handles.len())
|
||||
.unwrap_or(0);
|
||||
if sub_count > INSERT_SUB_LIMIT {
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let ip = glam::Vec3::new(
|
||||
(ins.insert_point.x - ox) as f32,
|
||||
(ins.insert_point.y - oy) as f32,
|
||||
(ins.insert_point.z - oz) as f32,
|
||||
);
|
||||
return vec![WireModel {
|
||||
name: h.value().to_string(),
|
||||
points: vec![],
|
||||
color: entity_color,
|
||||
selected: sel,
|
||||
aci,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![(ip, wire_model::SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}];
|
||||
}
|
||||
// Resolve the INSERT's own style so ByBlock sub-entities can inherit it.
|
||||
let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) = render::render_style_for(document, e);
|
||||
let ins_color = render::adapt_to_bg(ins_color, bg_color);
|
||||
|
||||
Reference in new issue
Block a user