perf: keep heavy work off UI thread
This commit is contained in:
parent
12bf126646
commit
deec48ad99
75 changed files with 11703 additions and 519 deletions
223
src/scene/cache/block_cache.rs
vendored
223
src/scene/cache/block_cache.rs
vendored
|
|
@ -15,7 +15,7 @@
|
|||
// a visited set so a self-referential block produces a marker rather than
|
||||
// recursing forever.
|
||||
|
||||
use rustc_hash::FxHashMap as HashMap;
|
||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3};
|
||||
|
|
@ -153,6 +153,29 @@ pub struct BlockDefn {
|
|||
#[derive(Default, Debug)]
|
||||
pub struct BlockCache {
|
||||
defns: HashMap<String, Arc<BlockDefn>>,
|
||||
prototype_blocks: HashSet<String>,
|
||||
/// Fully expanded prototype for repeated, non-array inserts. The key omits
|
||||
/// translation but includes the linear transform and every inherited style
|
||||
/// input. A matching insert therefore reuses all nested expansion/style
|
||||
/// work and only applies its translation to the immutable prototype.
|
||||
expansion_prototypes: std::sync::Mutex<
|
||||
HashMap<ExpansionPrototypeKey, Arc<std::sync::Mutex<Option<Arc<CachedExpansion>>>>>,
|
||||
>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct ExpansionPrototypeKey {
|
||||
block_name: String,
|
||||
linear: [u64; 9],
|
||||
insert_style: Vec<u32>,
|
||||
selected: bool,
|
||||
is_xref: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CachedExpansion {
|
||||
translation: [f64; 3],
|
||||
wires: Arc<Vec<WireModel>>,
|
||||
}
|
||||
|
||||
impl BlockCache {
|
||||
|
|
@ -181,6 +204,18 @@ impl BlockCache {
|
|||
use crate::par::prelude::*;
|
||||
let mut cache = Self::new();
|
||||
let referenced = collect_referenced_blocks(doc);
|
||||
let mut reference_counts: HashMap<String, usize> = HashMap::default();
|
||||
for entity in doc.entities() {
|
||||
if let EntityType::Insert(insert) = entity {
|
||||
*reference_counts
|
||||
.entry(insert.block_name.clone())
|
||||
.or_default() += insert.instance_count();
|
||||
}
|
||||
}
|
||||
cache.prototype_blocks = reference_counts
|
||||
.into_iter()
|
||||
.filter_map(|(name, count)| (count > 1).then_some(name))
|
||||
.collect();
|
||||
// Each defn is built independently: nested INSERTs are stored as
|
||||
// by-name references (`LocalSub::Nested`), never expanded here, so a
|
||||
// block's build never depends on another block's defn. That makes the
|
||||
|
|
@ -269,7 +304,6 @@ impl BlockCache {
|
|||
/// Walk all entities + all block_record contents collecting every distinct
|
||||
/// `block_name` that appears in an Insert (transitively).
|
||||
fn collect_referenced_blocks(doc: &CadDocument) -> Vec<String> {
|
||||
use rustc_hash::FxHashSet as HashSet;
|
||||
let mut seen: HashSet<String> = HashSet::default();
|
||||
let mut queue: Vec<String> = Vec::new();
|
||||
|
||||
|
|
@ -795,6 +829,61 @@ pub fn expand_insert(
|
|||
xform = xform.then(&scale_about_p);
|
||||
}
|
||||
let name = ins_handle.value().to_string();
|
||||
let prototype_key = if view_aabb.is_none()
|
||||
&& world_per_pixel.is_none()
|
||||
&& !ins.is_array()
|
||||
&& cache.prototype_blocks.contains(&ins.block_name)
|
||||
{
|
||||
Some(expansion_prototype_key(
|
||||
ins,
|
||||
&xform,
|
||||
ins_resolved_color,
|
||||
ins_pat_len,
|
||||
ins_pat,
|
||||
ins_lw_px,
|
||||
ins_layer,
|
||||
selected,
|
||||
pslt_factor,
|
||||
is_xref,
|
||||
bg_color,
|
||||
anno_scale,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prototype_slot = prototype_key.as_ref().map(|key| {
|
||||
let mut prototypes = cache
|
||||
.expansion_prototypes
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
Arc::clone(
|
||||
prototypes
|
||||
.entry(key.clone())
|
||||
.or_insert_with(|| Arc::new(std::sync::Mutex::new(None))),
|
||||
)
|
||||
});
|
||||
let mut prototype_guard = prototype_slot
|
||||
.as_ref()
|
||||
.map(|slot| slot.lock().unwrap_or_else(|poisoned| poisoned.into_inner()));
|
||||
if let Some(cached) = prototype_guard
|
||||
.as_ref()
|
||||
.and_then(|guard| guard.as_ref())
|
||||
.cloned()
|
||||
{
|
||||
let translation = transform_translation(&xform);
|
||||
let delta = [
|
||||
translation[0] - cached.translation[0],
|
||||
translation[1] - cached.translation[1],
|
||||
translation[2] - cached.translation[2],
|
||||
];
|
||||
return Some(
|
||||
cached
|
||||
.wires
|
||||
.iter()
|
||||
.map(|wire| translated_prototype_wire(wire, &name, delta))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
let mut batches = Batches::default();
|
||||
let mut visited: Vec<String> = Vec::with_capacity(8);
|
||||
|
||||
|
|
@ -848,7 +937,135 @@ pub fn expand_insert(
|
|||
};
|
||||
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0, (0.0, 1.0));
|
||||
}
|
||||
Some(batches.finalize(&name, selected, bg_color))
|
||||
let wires = batches.finalize(&name, selected, bg_color);
|
||||
if let Some(guard) = prototype_guard.as_mut() {
|
||||
let cached = Arc::new(CachedExpansion {
|
||||
translation: transform_translation(&xform),
|
||||
wires: Arc::new(wires.clone()),
|
||||
});
|
||||
**guard = Some(cached);
|
||||
}
|
||||
Some(wires)
|
||||
}
|
||||
|
||||
fn transform_translation(transform: &Transform) -> [f64; 3] {
|
||||
[
|
||||
transform.matrix.m[0][3],
|
||||
transform.matrix.m[1][3],
|
||||
transform.matrix.m[2][3],
|
||||
]
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn expansion_prototype_key(
|
||||
ins: &acadrust::entities::Insert,
|
||||
transform: &Transform,
|
||||
ins_color: [f32; 4],
|
||||
ins_pat_len: f32,
|
||||
ins_pat: [f32; 8],
|
||||
ins_lw_px: f32,
|
||||
ins_layer: crate::scene::view::render::InheritStyle,
|
||||
selected: bool,
|
||||
pslt_factor: f32,
|
||||
is_xref: bool,
|
||||
bg_color: [f32; 4],
|
||||
anno_scale: f32,
|
||||
) -> ExpansionPrototypeKey {
|
||||
let matrix = &transform.matrix.m;
|
||||
let linear = [
|
||||
matrix[0][0].to_bits(),
|
||||
matrix[0][1].to_bits(),
|
||||
matrix[0][2].to_bits(),
|
||||
matrix[1][0].to_bits(),
|
||||
matrix[1][1].to_bits(),
|
||||
matrix[1][2].to_bits(),
|
||||
matrix[2][0].to_bits(),
|
||||
matrix[2][1].to_bits(),
|
||||
matrix[2][2].to_bits(),
|
||||
];
|
||||
let mut insert_style = Vec::with_capacity(32);
|
||||
insert_style.extend(ins_color.map(f32::to_bits));
|
||||
insert_style.push(ins_pat_len.to_bits());
|
||||
insert_style.extend(ins_pat.map(f32::to_bits));
|
||||
insert_style.push(ins_lw_px.to_bits());
|
||||
insert_style.extend(ins_layer.color.map(f32::to_bits));
|
||||
insert_style.push(ins_layer.pat_len.to_bits());
|
||||
insert_style.extend(ins_layer.pat.map(f32::to_bits));
|
||||
insert_style.push(ins_layer.lw_px.to_bits());
|
||||
insert_style.push(pslt_factor.to_bits());
|
||||
insert_style.extend(bg_color.map(f32::to_bits));
|
||||
insert_style.push(anno_scale.to_bits());
|
||||
ExpansionPrototypeKey {
|
||||
block_name: ins.block_name.clone(),
|
||||
linear,
|
||||
insert_style,
|
||||
selected,
|
||||
is_xref,
|
||||
}
|
||||
}
|
||||
|
||||
fn translated_prototype_wire(source: &WireModel, name: &str, delta: [f64; 3]) -> WireModel {
|
||||
let mut wire = source.clone();
|
||||
wire.name = name.to_string();
|
||||
translate_double_single(&mut wire.points, &mut wire.points_low, delta);
|
||||
translate_double_single(&mut wire.fill_tris, &mut wire.fill_tris_low, delta);
|
||||
translate_double_single(&mut wire.pick_tris, &mut wire.pick_tris_low, delta);
|
||||
for (point, _) in &mut wire.snap_pts {
|
||||
point.x += delta[0];
|
||||
point.y += delta[1];
|
||||
point.z += delta[2];
|
||||
}
|
||||
for point in &mut wire.key_vertices {
|
||||
point[0] += delta[0];
|
||||
point[1] += delta[1];
|
||||
point[2] += delta[2];
|
||||
}
|
||||
let delta_f32 = [delta[0] as f32, delta[1] as f32, delta[2] as f32];
|
||||
for tangent in &mut wire.tangent_geoms {
|
||||
match tangent {
|
||||
TangentGeom::Line { p1, p2 } => {
|
||||
for axis in 0..3 {
|
||||
p1[axis] += delta_f32[axis];
|
||||
p2[axis] += delta_f32[axis];
|
||||
}
|
||||
}
|
||||
TangentGeom::Circle { center, .. } => {
|
||||
for axis in 0..3 {
|
||||
center[axis] += delta_f32[axis];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !wire.text_verts.is_empty() {
|
||||
wire.text_verts =
|
||||
crate::scene::model::wire_model::map_text_verts(&wire.text_verts, |x, y, z| {
|
||||
(x + delta[0], y + delta[1], z + delta[2])
|
||||
});
|
||||
}
|
||||
if wire.aabb != WireModel::UNBOUNDED_AABB {
|
||||
wire.aabb[0] += delta_f32[0];
|
||||
wire.aabb[1] += delta_f32[1];
|
||||
wire.aabb[2] += delta_f32[0];
|
||||
wire.aabb[3] += delta_f32[1];
|
||||
}
|
||||
wire
|
||||
}
|
||||
|
||||
fn translate_double_single(points: &mut [[f32; 3]], lows: &mut Vec<[f32; 3]>, delta: [f64; 3]) {
|
||||
if points.is_empty() {
|
||||
return;
|
||||
}
|
||||
if lows.len() != points.len() {
|
||||
lows.resize(points.len(), [0.0; 3]);
|
||||
}
|
||||
for (point, low) in points.iter_mut().zip(lows.iter_mut()) {
|
||||
for axis in 0..3 {
|
||||
let value = point[axis] as f64 + low[axis] as f64 + delta[axis];
|
||||
let high = value as f32;
|
||||
point[axis] = high;
|
||||
low[axis] = (value - high as f64) as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 {
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ impl Scene {
|
|||
};
|
||||
|
||||
if !handle.is_null() {
|
||||
self.invalidate_dependency_index();
|
||||
if let Some(model) = hatch_seed {
|
||||
self.hatches.insert(handle, model);
|
||||
}
|
||||
|
|
@ -275,6 +276,7 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.invalidate_dependency_index();
|
||||
self.bump_geometry();
|
||||
true
|
||||
}
|
||||
|
|
@ -353,6 +355,7 @@ impl Scene {
|
|||
return false;
|
||||
};
|
||||
*slot = entity;
|
||||
self.invalidate_dependency_index();
|
||||
|
||||
// Drop stale derived caches for this handle, then reseed for the new
|
||||
// entity's type (which may differ from the old one).
|
||||
|
|
@ -2092,6 +2095,7 @@ impl Scene {
|
|||
/// Rebuild hatch / image / mesh caches after the document is modified
|
||||
/// outside the normal `add_entity` path (e.g. REFCLOSE SAVE).
|
||||
pub fn rebuild_derived_caches(&mut self) {
|
||||
self.invalidate_dependency_index();
|
||||
self.populate_hatches_from_document_unbumped();
|
||||
self.populate_images_from_document_unbumped();
|
||||
self.populate_meshes_impl(false, false);
|
||||
|
|
|
|||
509
src/scene/mod.rs
509
src/scene/mod.rs
|
|
@ -45,6 +45,22 @@ pub(super) struct EntityIndex {
|
|||
pub unbounded_handles: Vec<Handle>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DependencyTargets {
|
||||
render_handles: HashSet<Handle>,
|
||||
source_handles: HashSet<Handle>,
|
||||
touches_block_definition: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SceneDependencyIndex {
|
||||
layers: HashMap<String, DependencyTargets>,
|
||||
text_styles: HashMap<String, DependencyTargets>,
|
||||
dim_styles: HashMap<String, DependencyTargets>,
|
||||
object_styles: HashMap<Handle, DependencyTargets>,
|
||||
blocks: HashMap<String, HashSet<Handle>>,
|
||||
}
|
||||
|
||||
fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> {
|
||||
let mut aabb = [
|
||||
f64::INFINITY,
|
||||
|
|
@ -238,6 +254,21 @@ struct ResidentWireLayout {
|
|||
marker_start: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PreparedOpenGeometry {
|
||||
pub wires: Arc<Vec<WireModel>>,
|
||||
pub interaction_index: Option<Arc<crate::scene::pick::interaction_index::InteractionIndex>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PreparedOpenGeometry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PreparedOpenGeometry")
|
||||
.field("wires", &self.wires.len())
|
||||
.field("interaction_index", &self.interaction_index.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WireGpuPatch {
|
||||
pub(crate) changes: Arc<Vec<(Handle, ChangeKind)>>,
|
||||
|
|
@ -357,6 +388,15 @@ pub struct DerivedCaches {
|
|||
/// Reported back to the UI so the user knows when a file had parser-junk
|
||||
/// entities silently dropped.
|
||||
pub corrupt_dropped: usize,
|
||||
/// Corrupt entities dropped while resolving referenced drawings.
|
||||
pub xref_dropped: usize,
|
||||
/// XREF resolution results produced by the loader worker. Keeping these in
|
||||
/// the open bundle prevents parsing and merging references on the UI thread.
|
||||
pub xrefs: Vec<crate::io::xref::XrefInfo>,
|
||||
/// Model wire set and its spatial interaction index, prepared on the loader
|
||||
/// thread. Installing these prevents the first visible frame from paying a
|
||||
/// whole-drawing tessellation/index build while the progress overlay freezes.
|
||||
pub prepared_geometry: Option<PreparedOpenGeometry>,
|
||||
/// Background-thread open-phase timings in milliseconds (parse, purge,
|
||||
/// derived-cache build). Filled in by `open_path_with_phase`; surfaced in
|
||||
/// the open-complete breakdown log so open-time regressions are visible.
|
||||
|
|
@ -369,11 +409,30 @@ pub struct OpenTimings {
|
|||
pub parse_ms: u32,
|
||||
pub purge_ms: u32,
|
||||
pub caches_ms: u32,
|
||||
pub xref_ms: u32,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
build_derived_caches_impl(doc, None)
|
||||
}
|
||||
|
||||
/// Build open-time caches while reporting monotonic progress in 0..=10000.
|
||||
///
|
||||
/// The callback is UI-agnostic and may run from Rayon workers. Callers should
|
||||
/// keep it cheap, normally just updating atomics.
|
||||
pub fn build_derived_caches_with_progress(
|
||||
doc: &CadDocument,
|
||||
progress: &(dyn Fn(u16) + Sync),
|
||||
) -> DerivedCaches {
|
||||
build_derived_caches_impl(doc, Some(progress))
|
||||
}
|
||||
|
||||
fn build_derived_caches_impl(
|
||||
doc: &CadDocument,
|
||||
progress: Option<&(dyn Fn(u16) + Sync)>,
|
||||
) -> DerivedCaches {
|
||||
// A new drawing must not inherit the previous one's resolved images — drop
|
||||
// the memoised set so each reference re-reads / re-fetches once here (and
|
||||
// stays cached across this document's later cache rebuilds).
|
||||
|
|
@ -428,7 +487,8 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||
let mut image_handles: Vec<Handle> = Vec::new();
|
||||
let mut mesh_handles: Vec<Handle> = Vec::new();
|
||||
let mut centers: Vec<[f64; 3]> = Vec::new();
|
||||
for e in doc.entities() {
|
||||
let entity_total = doc.entity_count().max(1);
|
||||
for (index, e) in doc.entities().enumerate() {
|
||||
let h = e.common().handle;
|
||||
match e {
|
||||
EntityType::Hatch(_) | EntityType::Solid(_) => hatch_handles.push(h),
|
||||
|
|
@ -444,6 +504,14 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||
if let Some(c) = offset_centroid(e, model_block, &prep) {
|
||||
centers.push(c);
|
||||
}
|
||||
if index & 0x1fff == 0 {
|
||||
if let Some(progress) = progress {
|
||||
progress(((index as u64 * 4000) / entity_total as u64) as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(progress) = progress {
|
||||
progress(4000);
|
||||
}
|
||||
let (local_center, local_extent_max) = cluster_extent_from_centers(centers, &doc.header);
|
||||
|
||||
|
|
@ -453,6 +521,19 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||
// the per-layout adaptation kicks in later regardless).
|
||||
const LOAD_BG: [f32; 4] = [33.0 / 255.0, 40.0 / 255.0, 48.0 / 255.0, 1.0];
|
||||
|
||||
let detail_total = hatch_handles
|
||||
.len()
|
||||
.saturating_add(image_handles.len())
|
||||
.saturating_add(mesh_handles.len())
|
||||
.max(1);
|
||||
let detail_done = std::sync::atomic::AtomicUsize::new(0);
|
||||
let report_detail = |done: usize| {
|
||||
if let Some(progress) = progress {
|
||||
let value = 4000u64 + done as u64 * 6000 / detail_total as u64;
|
||||
progress(value.min(10000) as u16);
|
||||
}
|
||||
};
|
||||
|
||||
// hatches
|
||||
let hatches: HashMap<Handle, HatchModel> = hatch_handles
|
||||
.par_iter()
|
||||
|
|
@ -465,15 +546,23 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||
EntityType::Solid(solid) => Some(Scene::solid_hatch_model(solid, color)),
|
||||
_ => None,
|
||||
};
|
||||
model.map(|m| (handle, m))
|
||||
let result = model.map(|m| (handle, m));
|
||||
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if done & 0xff == 0 || done == detail_total {
|
||||
report_detail(done);
|
||||
}
|
||||
result
|
||||
})
|
||||
.collect();
|
||||
|
||||
// images
|
||||
let images: HashMap<Handle, ImageModel> = image_handles
|
||||
.par_iter()
|
||||
.filter_map(|&handle| match doc.get_entity(handle)? {
|
||||
EntityType::RasterImage(img) => ImageModel::from_raster_image(img).map(|m| (handle, m)),
|
||||
.filter_map(|&handle| {
|
||||
let result = match doc.get_entity(handle)? {
|
||||
EntityType::RasterImage(img) => {
|
||||
ImageModel::from_raster_image(img).map(|m| (handle, m))
|
||||
}
|
||||
EntityType::Ole2Frame(ole) => ImageModel::from_ole2frame(ole).map(|m| (handle, m)),
|
||||
EntityType::Underlay(u) => match doc.objects.get(&u.definition_handle) {
|
||||
Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) => {
|
||||
|
|
@ -482,6 +571,12 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if done & 0xff == 0 || done == detail_total {
|
||||
report_detail(done);
|
||||
}
|
||||
result
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
@ -511,10 +606,16 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||
let (raw, ..) = view::render::render_style_for(doc, e);
|
||||
let color = view::render::adapt_to_bg(raw, LOAD_BG);
|
||||
let top_level = layout_blocks.contains(&e.common().owner_handle);
|
||||
crate::entities::solid3d::tessellate_volume(e, color, facet_res, isolines).map(|m| {
|
||||
let result = crate::entities::solid3d::tessellate_volume(e, color, facet_res, isolines)
|
||||
.map(|m| {
|
||||
let m = if top_level { offset_mesh_lod_set(m) } else { m };
|
||||
(handle, m, top_level)
|
||||
})
|
||||
});
|
||||
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if done & 0xff == 0 || done == detail_total {
|
||||
report_detail(done);
|
||||
}
|
||||
result
|
||||
})
|
||||
.collect();
|
||||
let mut meshes: HashMap<Handle, MeshLodSet> = HashMap::default();
|
||||
|
|
@ -527,6 +628,10 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(progress) = progress {
|
||||
progress(10000);
|
||||
}
|
||||
|
||||
DerivedCaches {
|
||||
local_extent_max,
|
||||
local_center,
|
||||
|
|
@ -535,10 +640,54 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||
meshes,
|
||||
block_meshes,
|
||||
corrupt_dropped: 0,
|
||||
xref_dropped: 0,
|
||||
xrefs: Vec::new(),
|
||||
prepared_geometry: None,
|
||||
timings: OpenTimings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare the expensive first Model wire set and spatial interaction index on
|
||||
/// the loader thread. The temporary `Scene` never crosses threads (it contains
|
||||
/// `Rc`/`RefCell` state); only its Send-safe document and immutable prepared
|
||||
/// geometry are returned.
|
||||
pub fn prepare_open_geometry(
|
||||
doc: CadDocument,
|
||||
caches: &DerivedCaches,
|
||||
model_bg: [f32; 4],
|
||||
) -> (CadDocument, PreparedOpenGeometry) {
|
||||
let mut scene = Scene::new();
|
||||
scene.document = doc;
|
||||
scene.local_extent_max = caches.local_extent_max;
|
||||
scene.local_center = caches.local_center;
|
||||
scene.bg_color = model_bg;
|
||||
let cannoscale_value = scene.document.header.annotation_scale_value;
|
||||
scene.annotation_scale = if cannoscale_value > 1e-9 {
|
||||
(1.0 / cannoscale_value) as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
scene.current_layout = "Model".to_string();
|
||||
let camera = scene.camera.borrow().clone();
|
||||
let wires = scene.model_tile_wires_arc(0, &camera, 1.0, 1.0);
|
||||
let interaction_index = if scene.interaction_index_worthwhile(&wires) {
|
||||
let index =
|
||||
Arc::new(crate::scene::pick::interaction_index::InteractionIndex::build(&wires));
|
||||
index.prepare_screen();
|
||||
Some(index)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let doc = std::mem::replace(&mut scene.document, CadDocument::new());
|
||||
(
|
||||
doc,
|
||||
PreparedOpenGeometry {
|
||||
wires,
|
||||
interaction_index,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Mirrors `cache::block_cache::SANE_EXTENT` — wire coords past this magnitude
|
||||
/// are treated as corruption rather than precision-relevant geometry.
|
||||
const CLUSTER_SANE_EXTENT: f64 = 1.0e8;
|
||||
|
|
@ -1165,13 +1314,10 @@ pub struct Scene {
|
|||
/// filtered variants. Viewports sharing a frozen set share one entry (like
|
||||
/// the resident wire set). Empty for a viewport with no frozen layers (it
|
||||
/// reuses the unfiltered `*_arc` sets directly).
|
||||
frozen_hatch_cache:
|
||||
RefCell<HashMap<(String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>,
|
||||
frozen_wipeout_cache:
|
||||
RefCell<HashMap<(String, u64), (u64, Arc<Vec<HatchModel>>)>>,
|
||||
frozen_hatch_cache: 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_mesh_cache:
|
||||
RefCell<HashMap<(String, 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.
|
||||
/// Building this explodes every model-space INSERT, so without the cache a
|
||||
/// heavy block-instanced drawing re-explodes thousands of inserts on every
|
||||
|
|
@ -1258,12 +1404,16 @@ pub struct Scene {
|
|||
/// 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.
|
||||
entity_block_map_cache: RefCell<Option<(u64, HashMap<Handle, Handle>)>>,
|
||||
/// Reverse dependencies from layer/style/block definitions to the top-level
|
||||
/// entities whose resident wire runs actually change. Kept independent from
|
||||
/// `geometry_epoch`: a layer colour toggle can reuse the index, invalidate
|
||||
/// only its dependants, and avoid a whole-document scan on every toggle.
|
||||
dependency_index_cache: RefCell<Option<SceneDependencyIndex>>,
|
||||
/// Tessellated block definitions in block-local coords, keyed by render
|
||||
/// background and block epoch. Model and Paper adapt black/white colours
|
||||
/// differently; retaining both variants prevents a full block rebuild on
|
||||
/// every layout-tab switch.
|
||||
block_defn_cache:
|
||||
RefCell<HashMap<[u32; 4], (u64, Arc<cache::block_cache::BlockCache>)>>,
|
||||
block_defn_cache: RefCell<HashMap<[u32; 4], (u64, Arc<cache::block_cache::BlockCache>)>>,
|
||||
/// Spatial index + always-emit list for top-level entities
|
||||
/// (Phase 2.1). Lazily rebuilt by `entity_index()` on
|
||||
/// `geometry_epoch` change. See `EntityIndex` for what each side
|
||||
|
|
@ -1455,6 +1605,7 @@ impl Scene {
|
|||
annotation_affects_wires: std::cell::Cell::new(None),
|
||||
model_extents_cache: RefCell::new(None),
|
||||
entity_block_map_cache: RefCell::new(None),
|
||||
dependency_index_cache: RefCell::new(None),
|
||||
block_defn_cache: RefCell::new(HashMap::default()),
|
||||
entity_index_cache: RefCell::new(None),
|
||||
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||
|
|
@ -1555,6 +1706,30 @@ impl Scene {
|
|||
arc
|
||||
}
|
||||
|
||||
/// Install loader-thread geometry into this scene's Model resident cache.
|
||||
///
|
||||
/// The target scene has a different epoch from the temporary loader scene,
|
||||
/// so only immutable geometry is transferred and re-stamped here. The
|
||||
/// optional interaction index is cached against the exact same `Arc`.
|
||||
pub fn install_prepared_open_geometry(&self, prepared: PreparedOpenGeometry) {
|
||||
let block = self.model_space_block_handle();
|
||||
let key = Self::resident_wire_key(block, self.bg_color, None, None);
|
||||
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||
self.last_model_wire_gen.set(gen);
|
||||
self.resident_wire_sets.borrow_mut().insert(
|
||||
key,
|
||||
ResidentWireSet {
|
||||
epoch: self.geometry_epoch,
|
||||
gen,
|
||||
wires: Arc::clone(&prepared.wires),
|
||||
layout: None,
|
||||
},
|
||||
);
|
||||
if let Some(index) = prepared.interaction_index {
|
||||
self.cache_interaction_index(self.geometry_epoch, prepared.wires, index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Push one delta onto the journal, evicting the oldest past the cap and
|
||||
/// raising the floor so a consumer that fell behind falls back to a full
|
||||
/// rebuild. Every `geometry_epoch` bump must call this exactly once so the
|
||||
|
|
@ -1779,6 +1954,10 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
|
||||
if !changes.is_empty() {
|
||||
self.invalidate_dependency_index();
|
||||
}
|
||||
|
||||
changes
|
||||
}
|
||||
|
||||
|
|
@ -3245,30 +3424,7 @@ impl Scene {
|
|||
} else {
|
||||
self.paper_bg_color
|
||||
};
|
||||
let key = {
|
||||
let mut k: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut mix = |x: u64| k = k.rotate_left(17) ^ x.wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||
mix(block.value());
|
||||
for c in bg {
|
||||
mix(c.to_bits() as u64);
|
||||
}
|
||||
mix(anno_scale_override
|
||||
.map(|a| a.to_bits() as u64)
|
||||
.unwrap_or(u64::MAX));
|
||||
match frozen_layers {
|
||||
Some(f) => {
|
||||
// Order-independent fold of the frozen-layer set.
|
||||
let mut acc: u64 = 0;
|
||||
for h in f {
|
||||
acc ^= h.value().wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||
}
|
||||
mix(acc);
|
||||
mix(f.len() as u64);
|
||||
}
|
||||
None => mix(u64::MAX - 1),
|
||||
}
|
||||
k
|
||||
};
|
||||
let key = Self::resident_wire_key(block, bg, anno_scale_override, frozen_layers);
|
||||
{
|
||||
let sets = self.resident_wire_sets.borrow();
|
||||
if let Some(set) = sets.get(&key) {
|
||||
|
|
@ -3331,6 +3487,36 @@ impl Scene {
|
|||
arc
|
||||
}
|
||||
|
||||
fn resident_wire_key(
|
||||
block: Handle,
|
||||
bg: [f32; 4],
|
||||
anno_scale_override: Option<f32>,
|
||||
frozen_layers: Option<&HashSet<Handle>>,
|
||||
) -> u64 {
|
||||
let mut key: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut mix =
|
||||
|value: u64| key = key.rotate_left(17) ^ value.wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||
mix(block.value());
|
||||
for component in bg {
|
||||
mix(component.to_bits() as u64);
|
||||
}
|
||||
mix(anno_scale_override
|
||||
.map(|scale| scale.to_bits() as u64)
|
||||
.unwrap_or(u64::MAX));
|
||||
match frozen_layers {
|
||||
Some(frozen) => {
|
||||
let mut signature = 0u64;
|
||||
for handle in frozen {
|
||||
signature ^= handle.value().wrapping_mul(0x9E37_79B9_7F4A_7C15);
|
||||
}
|
||||
mix(signature);
|
||||
mix(frozen.len() as u64);
|
||||
}
|
||||
None => mix(u64::MAX - 1),
|
||||
}
|
||||
key
|
||||
}
|
||||
|
||||
/// The GPU wire-arena handoff for a viewport whose content id is `gen`:
|
||||
/// `(prev_gen, changed handles)` when the Model set reached `gen` via an
|
||||
/// incremental resident patch, else `None`. Read (not consumed) so every
|
||||
|
|
@ -6375,6 +6561,253 @@ impl Scene {
|
|||
})
|
||||
}
|
||||
|
||||
fn rebuild_dependency_index(&self) -> SceneDependencyIndex {
|
||||
let layout_blocks: HashSet<Handle> = self
|
||||
.document
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|object| match object {
|
||||
acadrust::objects::ObjectType::Layout(layout) if !layout.block_record.is_null() => {
|
||||
Some(layout.block_record)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let block_names: HashMap<Handle, String> = self
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.map(|record| (record.handle, record.name.to_ascii_uppercase()))
|
||||
.collect();
|
||||
let membership: HashMap<Handle, Handle> = self
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.flat_map(|record| {
|
||||
record
|
||||
.entity_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.map(move |handle| (handle, record.handle))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut roots: HashMap<String, HashSet<Handle>> = HashMap::default();
|
||||
let mut parents: HashMap<String, HashSet<String>> = HashMap::default();
|
||||
for entity in self.document.entities() {
|
||||
let EntityType::Insert(insert) = entity else {
|
||||
continue;
|
||||
};
|
||||
let target = insert.block_name.to_ascii_uppercase();
|
||||
let common = &insert.common;
|
||||
let owner = if common.owner_handle.is_null() {
|
||||
membership
|
||||
.get(&common.handle)
|
||||
.copied()
|
||||
.unwrap_or(Handle::NULL)
|
||||
} else {
|
||||
common.owner_handle
|
||||
};
|
||||
if owner.is_null() || layout_blocks.contains(&owner) {
|
||||
roots.entry(target).or_default().insert(common.handle);
|
||||
} else if let Some(parent) = block_names.get(&owner) {
|
||||
parents.entry(target).or_default().insert(parent.clone());
|
||||
}
|
||||
}
|
||||
// Propagate top-level INSERT users through nested block references.
|
||||
// Fixed-point form is cycle-safe and block graphs are normally shallow.
|
||||
let mut changed = true;
|
||||
while changed {
|
||||
changed = false;
|
||||
for (child, parent_names) in &parents {
|
||||
let inherited: Vec<Handle> = parent_names
|
||||
.iter()
|
||||
.flat_map(|parent| roots.get(parent).into_iter().flatten().copied())
|
||||
.collect();
|
||||
let entry = roots.entry(child.clone()).or_default();
|
||||
let before = entry.len();
|
||||
entry.extend(inherited);
|
||||
changed |= entry.len() != before;
|
||||
}
|
||||
}
|
||||
|
||||
let mut index = SceneDependencyIndex {
|
||||
blocks: roots.clone(),
|
||||
..SceneDependencyIndex::default()
|
||||
};
|
||||
for entity in self.document.entities() {
|
||||
let common = entity.common();
|
||||
let owner = if common.owner_handle.is_null() {
|
||||
membership
|
||||
.get(&common.handle)
|
||||
.copied()
|
||||
.unwrap_or(Handle::NULL)
|
||||
} else {
|
||||
common.owner_handle
|
||||
};
|
||||
let inside_block = !owner.is_null() && !layout_blocks.contains(&owner);
|
||||
let render_handles: HashSet<Handle> = if inside_block {
|
||||
block_names
|
||||
.get(&owner)
|
||||
.and_then(|name| roots.get(name))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
std::iter::once(common.handle).collect()
|
||||
};
|
||||
let add = |map: &mut HashMap<String, DependencyTargets>, name: &str| {
|
||||
let target = map.entry(name.to_ascii_uppercase()).or_default();
|
||||
target.render_handles.extend(render_handles.iter().copied());
|
||||
target.source_handles.insert(common.handle);
|
||||
target.touches_block_definition |= inside_block;
|
||||
};
|
||||
let add_handle = |map: &mut HashMap<Handle, DependencyTargets>,
|
||||
handle: Option<Handle>| {
|
||||
let Some(handle) = handle.filter(|handle| !handle.is_null()) else {
|
||||
return;
|
||||
};
|
||||
let target = map.entry(handle).or_default();
|
||||
target.render_handles.extend(render_handles.iter().copied());
|
||||
target.source_handles.insert(common.handle);
|
||||
target.touches_block_definition |= inside_block;
|
||||
};
|
||||
add(&mut index.layers, &common.layer);
|
||||
match entity {
|
||||
EntityType::Text(text) => add(&mut index.text_styles, &text.style),
|
||||
EntityType::MText(text) => add(&mut index.text_styles, &text.style),
|
||||
EntityType::AttributeDefinition(attribute) => {
|
||||
add(&mut index.text_styles, &attribute.text_style)
|
||||
}
|
||||
EntityType::AttributeEntity(attribute) => {
|
||||
add(&mut index.text_styles, &attribute.text_style)
|
||||
}
|
||||
EntityType::Insert(insert) => {
|
||||
for attribute in &insert.attributes {
|
||||
add(&mut index.text_styles, &attribute.text_style);
|
||||
}
|
||||
}
|
||||
EntityType::Dimension(dimension) => {
|
||||
add(&mut index.dim_styles, &dimension.base().style_name)
|
||||
}
|
||||
EntityType::Table(table) => {
|
||||
add_handle(&mut index.object_styles, table.table_style_handle)
|
||||
}
|
||||
EntityType::MultiLeader(leader) => {
|
||||
add_handle(&mut index.object_styles, leader.style_handle)
|
||||
}
|
||||
EntityType::MLine(line) => add_handle(&mut index.object_styles, line.style_handle),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
fn dependency_targets(&self, kind: &str, names: &[String]) -> DependencyTargets {
|
||||
if self.dependency_index_cache.borrow().is_none() {
|
||||
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
|
||||
}
|
||||
let cache = self.dependency_index_cache.borrow();
|
||||
let index = cache.as_ref().unwrap();
|
||||
let map = match kind {
|
||||
"layer" => &index.layers,
|
||||
"text" => &index.text_styles,
|
||||
"dim" => &index.dim_styles,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut combined = DependencyTargets::default();
|
||||
for name in names {
|
||||
let Some(target) = map.get(&name.to_ascii_uppercase()) else {
|
||||
continue;
|
||||
};
|
||||
combined
|
||||
.render_handles
|
||||
.extend(target.render_handles.iter().copied());
|
||||
combined
|
||||
.source_handles
|
||||
.extend(target.source_handles.iter().copied());
|
||||
combined.touches_block_definition |= target.touches_block_definition;
|
||||
}
|
||||
combined
|
||||
}
|
||||
|
||||
fn invalidate_dependency_targets(&mut self, targets: DependencyTargets) {
|
||||
if targets.render_handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
if targets.touches_block_definition {
|
||||
self.block_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
let sources: Vec<Handle> = targets.source_handles.iter().copied().collect();
|
||||
self.recolor_meshes_for_handles(&sources);
|
||||
let changes: Vec<(Handle, ChangeKind)> = targets
|
||||
.render_handles
|
||||
.into_iter()
|
||||
.map(|handle| (handle, ChangeKind::Modified))
|
||||
.collect();
|
||||
self.bump_entities(&changes);
|
||||
}
|
||||
|
||||
pub fn invalidate_layer_dependencies(&mut self, names: &[String]) {
|
||||
let targets = self.dependency_targets("layer", names);
|
||||
self.invalidate_dependency_targets(targets);
|
||||
}
|
||||
|
||||
pub fn invalidate_text_style_dependencies(&mut self, name: &str) {
|
||||
self.invalidate_text_style_dependencies_many(&[name.to_string()]);
|
||||
}
|
||||
|
||||
pub fn invalidate_text_style_dependencies_many(&mut self, names: &[String]) {
|
||||
let targets = self.dependency_targets("text", names);
|
||||
self.invalidate_dependency_targets(targets);
|
||||
}
|
||||
|
||||
pub fn invalidate_dim_style_dependencies(&mut self, name: &str) {
|
||||
self.invalidate_dim_style_dependencies_many(&[name.to_string()]);
|
||||
}
|
||||
|
||||
pub fn invalidate_dim_style_dependencies_many(&mut self, names: &[String]) {
|
||||
let targets = self.dependency_targets("dim", names);
|
||||
self.invalidate_dependency_targets(targets);
|
||||
}
|
||||
|
||||
pub fn invalidate_object_style_dependencies(&mut self, handles: &[Handle]) {
|
||||
if self.dependency_index_cache.borrow().is_none() {
|
||||
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
|
||||
}
|
||||
let mut combined = DependencyTargets::default();
|
||||
if let Some(index) = self.dependency_index_cache.borrow().as_ref() {
|
||||
for handle in handles {
|
||||
let Some(target) = index.object_styles.get(handle) else {
|
||||
continue;
|
||||
};
|
||||
combined
|
||||
.render_handles
|
||||
.extend(target.render_handles.iter().copied());
|
||||
combined
|
||||
.source_handles
|
||||
.extend(target.source_handles.iter().copied());
|
||||
combined.touches_block_definition |= target.touches_block_definition;
|
||||
}
|
||||
}
|
||||
self.invalidate_dependency_targets(combined);
|
||||
}
|
||||
|
||||
pub fn block_dependency_handles(&self, name: &str) -> Vec<Handle> {
|
||||
if self.dependency_index_cache.borrow().is_none() {
|
||||
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
|
||||
}
|
||||
self.dependency_index_cache
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.and_then(|index| index.blocks.get(&name.to_ascii_uppercase()))
|
||||
.map(|handles| handles.iter().copied().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate_dependency_index(&self) {
|
||||
self.dependency_index_cache.borrow_mut().take();
|
||||
}
|
||||
|
||||
/// Spatial index + always-emit list for top-level entities. Lazily
|
||||
/// rebuilt on `geometry_epoch` change.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ pub struct Pipeline {
|
|||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
|
||||
/// xray pipelines). `Some` only on the fast native path; `None` in packed
|
||||
/// xray pipelines). `Some` on native/WebGPU fast paths; `None` in packed
|
||||
/// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`.
|
||||
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
||||
wipeout_pipeline: wgpu::RenderPipeline,
|
||||
|
|
@ -338,8 +338,8 @@ impl Pipeline {
|
|||
});
|
||||
|
||||
// ── Wire pipeline ──────────────────────────────────────────────────
|
||||
// Select once per device. The fast native path hoists shared constants
|
||||
// into storage; compatibility mode keeps them in 10 packed attributes.
|
||||
// Select once per device. Native and WebGPU hoist shared constants into
|
||||
// storage when limits allow; WebGL2/compat keeps packed attributes.
|
||||
let wire_mode = wire_gpu::WirePipelineMode::select(device);
|
||||
let renderer_mode_name =
|
||||
if wire_mode.uses_storage() { "fast-storage" } else { "packed-compat" };
|
||||
|
|
@ -357,12 +357,9 @@ impl Pipeline {
|
|||
renderer_mode_name,
|
||||
device.limits().max_storage_buffers_per_shader_stage
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let wire_const_bgl = wire_mode
|
||||
.uses_storage()
|
||||
.then(|| wire_gpu::WireConst::bind_group_layout(device));
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let wire_const_bgl: Option<wgpu::BindGroupLayout> = None;
|
||||
let mut wire_bgls: Vec<&wgpu::BindGroupLayout> = vec![&frame_bgl];
|
||||
if let Some(bgl) = &wire_const_bgl {
|
||||
wire_bgls.push(bgl);
|
||||
|
|
@ -379,7 +376,6 @@ impl Pipeline {
|
|||
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("wire.shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(match wire_mode {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_gpu::WirePipelineMode::IndexedStorage => {
|
||||
include_str!("../../shaders/wire_indexed.wgsl")
|
||||
}
|
||||
|
|
@ -3157,6 +3153,14 @@ fn create_msaa_texture(
|
|||
pub struct MultiPipeline {
|
||||
pub(crate) inners: Vec<Pipeline>,
|
||||
format: wgpu::TextureFormat,
|
||||
/// Stable viewport identity → pipeline slot map. Paper viewports used to
|
||||
/// occupy slots by their current list position, so switching layouts or
|
||||
/// scrolling a sheet could assign an existing viewport to a different
|
||||
/// slot and throw away all of its GPU caches. Keep the association across
|
||||
/// tab switches and only recycle genuinely cold slots.
|
||||
pub(crate) slot_by_instance: rustc_hash::FxHashMap<u64, usize>,
|
||||
slot_last_used: Vec<u64>,
|
||||
slot_clock: u64,
|
||||
/// The resident wire batches, keyed by `wire_content_id` and shared across
|
||||
/// every slot (and every pane — one `MultiPipeline` backs all of them) that
|
||||
/// renders the same content. `prepare` builds an entry once on a cache miss
|
||||
|
|
@ -3189,8 +3193,70 @@ impl MultiPipeline {
|
|||
let n = n.max(1);
|
||||
while self.inners.len() < n {
|
||||
self.inners.push(Pipeline::new(device, queue, self.format));
|
||||
self.slot_last_used.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve stable slots for the viewport identities in one primitive.
|
||||
/// Thirty-two hot slots cover ordinary tiled/paper drawings. A cold slot
|
||||
/// is recycled only after several other prepare calls, which prevents
|
||||
/// sibling Model panes prepared in the same frame from evicting each
|
||||
/// other. If every slot is still hot, growing is safer than a visible
|
||||
/// rebuild hitch.
|
||||
pub(crate) fn resolve_slots(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
instance_ids: &[u64],
|
||||
) -> Vec<usize> {
|
||||
const SOFT_LIMIT: usize = 32;
|
||||
const HOT_WINDOW: u64 = 8;
|
||||
|
||||
self.slot_clock = self.slot_clock.wrapping_add(1).max(1);
|
||||
let now = self.slot_clock;
|
||||
let reserved: rustc_hash::FxHashSet<u64> = instance_ids.iter().copied().collect();
|
||||
let mut slots = Vec::with_capacity(instance_ids.len());
|
||||
|
||||
for &instance_id in instance_ids {
|
||||
let slot = if let Some(&slot) = self.slot_by_instance.get(&instance_id) {
|
||||
slot
|
||||
} else {
|
||||
let vacant = self
|
||||
.inners
|
||||
.iter()
|
||||
.position(|inner| inner.slot_id == u64::MAX);
|
||||
let recyclable = vacant.or_else(|| {
|
||||
(self.inners.len() >= SOFT_LIMIT)
|
||||
.then(|| {
|
||||
self.inners
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, inner)| !reserved.contains(&inner.slot_id))
|
||||
.filter(|(slot, _)| {
|
||||
now.saturating_sub(self.slot_last_used[*slot]) > HOT_WINDOW
|
||||
})
|
||||
.min_by_key(|(slot, _)| self.slot_last_used[*slot])
|
||||
.map(|(slot, _)| slot)
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
let slot = recyclable.unwrap_or_else(|| {
|
||||
let slot = self.inners.len();
|
||||
self.ensure_len(device, queue, slot + 1);
|
||||
slot
|
||||
});
|
||||
let old_id = self.inners[slot].slot_id;
|
||||
if old_id != u64::MAX {
|
||||
self.slot_by_instance.remove(&old_id);
|
||||
}
|
||||
self.slot_by_instance.insert(instance_id, slot);
|
||||
slot
|
||||
};
|
||||
self.slot_last_used[slot] = now;
|
||||
slots.push(slot);
|
||||
}
|
||||
slots
|
||||
}
|
||||
}
|
||||
|
||||
/// Send wgpu's uncaptured validation errors to stderr instead of the default
|
||||
|
|
@ -3227,6 +3293,9 @@ impl iced::widget::shader::Pipeline for MultiPipeline {
|
|||
Self {
|
||||
inners: vec![Pipeline::new(device, queue, format)],
|
||||
format,
|
||||
slot_by_instance: rustc_hash::FxHashMap::default(),
|
||||
slot_last_used: vec![0],
|
||||
slot_clock: 0,
|
||||
wire_buffer_cache: rustc_hash::FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,18 +58,17 @@ fn instance_buffer_mapped<T: bytemuck::Pod>(
|
|||
|
||||
// ── Instance layout ───────────────────────────────────────────────────────
|
||||
|
||||
// ── Native: slim per-segment instance + shared per-wire constants ───────────
|
||||
// ── WebGPU/native: slim per-segment instance + shared constants ─────────────
|
||||
//
|
||||
// Every segment of a wire used to carry the wire's color / line-weight / dash
|
||||
// pattern / draw-depth (~44 B) on each instance — re-fetched once per segment
|
||||
// even though it's constant along the wire. On native we hoist those into a
|
||||
// even though it's constant along the wire. On storage-capable adapters we hoist those into a
|
||||
// per-wire `WireConst` storage buffer indexed by `wire_id`, so the instance
|
||||
// keeps only the per-segment data (endpoints + arc-length distances). Cuts the
|
||||
// instance from 104 B to one 64-byte cache line and removes the redundant
|
||||
// per-segment re-fetch of the shared constants. WebGL2 has no vertex-stage
|
||||
// storage buffers, so the wasm build below keeps the original self-contained
|
||||
// fat instance.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct WireInstance {
|
||||
|
|
@ -87,8 +86,6 @@ pub struct WireInstance {
|
|||
/// exactly one 64-byte cache line.
|
||||
pub taper_ratio: [u16; 2],
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl WireInstance {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
// Must match `InstanceIn` in wire_indexed.wgsl.
|
||||
|
|
@ -110,11 +107,10 @@ impl WireInstance {
|
|||
}
|
||||
}
|
||||
|
||||
/// Per-wire constants shared by every segment of a wire (native only). std430
|
||||
/// Per-wire constants shared by every segment of a wire. std430
|
||||
/// layout: three vec4 then eight scalars = 80 B, matching `WireConst` in
|
||||
/// wire_indexed.wgsl. `align_end` / `align_total` carry the "A"-type endpoint
|
||||
/// alignment (see `wire_distances`); 0.0 total = no alignment.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct WireConst {
|
||||
|
|
@ -134,8 +130,6 @@ pub struct WireConst {
|
|||
pub _pad1: f32,
|
||||
pub _pad2: f32,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl WireConst {
|
||||
/// Bind-group layout for the per-wire storage buffer (group 1 of the wire /
|
||||
/// xray pipelines). Read-only storage, visible to the vertex stage.
|
||||
|
|
@ -158,8 +152,8 @@ impl WireConst {
|
|||
|
||||
// ── Packed compatibility instance (no vertex-stage storage) ────────────────
|
||||
//
|
||||
// Web always uses this layout. Native selects it at runtime for adapters whose
|
||||
// storage-buffer limits are insufficient, or when --compat-renderer is set.
|
||||
// WebGL2 and limited native adapters use this layout. WebGPU selects the
|
||||
// indexed storage path when its reported limits satisfy the same requirement.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct PackedWireInstance {
|
||||
|
|
@ -224,21 +218,15 @@ impl PackedWireInstance {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type WireInstance = PackedWireInstance;
|
||||
|
||||
/// Wire and hatch pipelines switch together: the fast path uses storage
|
||||
/// buffers; the compatibility path carries wire constants in packed vertex
|
||||
/// attributes and hatch data in a texture.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WirePipelineMode {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
IndexedStorage,
|
||||
Packed,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn select_native_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> WirePipelineMode {
|
||||
fn select_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> WirePipelineMode {
|
||||
const REQUIRED_STORAGE_BUFFERS_PER_STAGE: u32 = 5;
|
||||
if forced || max_storage_buffers_per_stage < REQUIRED_STORAGE_BUFFERS_PER_STAGE {
|
||||
WirePipelineMode::Packed
|
||||
|
|
@ -249,23 +237,15 @@ fn select_native_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> W
|
|||
|
||||
impl WirePipelineMode {
|
||||
pub fn select(device: &wgpu::Device) -> Self {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = device;
|
||||
Self::Packed
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
select_native_pipeline(
|
||||
device.limits().max_storage_buffers_per_shader_stage,
|
||||
crate::cli::gui_config().compat_renderer,
|
||||
)
|
||||
}
|
||||
let forced = crate::cli::gui_config().compat_renderer;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let forced = false;
|
||||
select_pipeline(device.limits().max_storage_buffers_per_shader_stage, forced)
|
||||
}
|
||||
|
||||
pub fn uses_storage(self) -> bool {
|
||||
match self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Self::IndexedStorage => true,
|
||||
Self::Packed => false,
|
||||
}
|
||||
|
|
@ -273,7 +253,6 @@ impl WirePipelineMode {
|
|||
|
||||
pub fn layout<'a>(self) -> wgpu::VertexBufferLayout<'a> {
|
||||
match self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Self::IndexedStorage => WireInstance::layout(),
|
||||
Self::Packed => PackedWireInstance::layout(),
|
||||
}
|
||||
|
|
@ -494,9 +473,8 @@ fn emit_wire_packed(
|
|||
instances
|
||||
}
|
||||
|
||||
/// Native: emit slim per-segment instances (positions + distances + `wire_id`)
|
||||
/// Storage path: emit slim per-segment instances (positions + distances + `wire_id`)
|
||||
/// plus the one `WireConst` record every segment of this wire shares.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn emit_wire_native(
|
||||
wire: &WireModel,
|
||||
wire_id: u32,
|
||||
|
|
@ -579,11 +557,10 @@ pub(crate) fn wire_draw_depth(
|
|||
}
|
||||
}
|
||||
|
||||
/// Build the shared per-wire `WireConst` storage buffer and its bind group
|
||||
/// (native only). All instance-buffer chunks from one build reference the same
|
||||
/// Build the shared per-wire `WireConst` storage buffer and its bind group.
|
||||
/// All instance-buffer chunks from one build reference the same
|
||||
/// buffer via their global `wire_id`, so a single bind group is cloned into
|
||||
/// each chunk.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_const_bind_group(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
|
|
@ -677,7 +654,6 @@ impl WireGpu {
|
|||
mesh_edge: bool,
|
||||
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||
) -> Vec<Self> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(const_bgl) = const_bgl {
|
||||
const MAX_INSTANCES: usize =
|
||||
268_435_456 / std::mem::size_of::<WireInstance>();
|
||||
|
|
@ -769,7 +745,6 @@ impl WireGpu {
|
|||
if total_segs == 0 {
|
||||
return vec![];
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(const_bgl) = const_bgl {
|
||||
// GPU max buffer size is 256 MB; chunk to stay within the limit.
|
||||
const MAX_INSTANCES: usize =
|
||||
|
|
|
|||
|
|
@ -391,7 +391,10 @@ impl Scene {
|
|||
// Report the exact erased handles so derived caches drop just those and
|
||||
// the resident set removes only their wires (bump_entities drops them
|
||||
// from the tessellation memos too).
|
||||
self.bump_entities(&erased);
|
||||
if !erased.is_empty() {
|
||||
self.invalidate_dependency_index();
|
||||
self.bump_entities(&erased);
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore erased Arc-backed entities without re-linking their still-present
|
||||
|
|
@ -414,6 +417,7 @@ impl Scene {
|
|||
}
|
||||
}
|
||||
if !changes.is_empty() {
|
||||
self.invalidate_dependency_index();
|
||||
self.bump_entities(&changes);
|
||||
}
|
||||
restored
|
||||
|
|
|
|||
|
|
@ -152,13 +152,6 @@ pub struct Primitive {
|
|||
pub(in crate::scene) viewports: Vec<ViewportData>,
|
||||
/// Background color used to clear each viewport's MSAA buffer.
|
||||
pub(in crate::scene) bg_color: [f32; 4],
|
||||
/// First `MultiPipeline` inner slot this primitive owns. Paper space (one
|
||||
/// shader widget, many viewports) uses 0. Per-pane Model widgets each own a
|
||||
/// distinct slot (= their tile index) so several shader widgets can share
|
||||
/// the type-keyed pipeline storage without clobbering one another — all
|
||||
/// `prepare` calls run before all `render` calls, so disjoint slots are
|
||||
/// safe.
|
||||
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>,
|
||||
}
|
||||
|
|
@ -250,10 +243,11 @@ impl shader::Primitive for Primitive {
|
|||
let phys = viewport.physical_size();
|
||||
let full_size = Size::new(phys.width, phys.height);
|
||||
let scale = viewport.scale_factor() as f32;
|
||||
pipeline.ensure_len(device, queue, self.base_slot + self.viewports.len());
|
||||
let instance_ids: Vec<u64> = self.viewports.iter().map(|vp| vp.instance_id).collect();
|
||||
let slots = pipeline.resolve_slots(device, queue, &instance_ids);
|
||||
|
||||
for (i, vp) in self.viewports.iter().enumerate() {
|
||||
let inner = &mut pipeline.inners[self.base_slot + i];
|
||||
let inner = &mut pipeline.inners[slots[i]];
|
||||
// Pipeline slots are addressed by list index, but off-canvas
|
||||
// viewports are dropped from the list — so a slot can be reused by a
|
||||
// DIFFERENT viewport across frames (e.g. the first viewport scrolls
|
||||
|
|
@ -442,7 +436,10 @@ impl shader::Primitive for Primitive {
|
|||
// the whole wire buffer. Only for the scissor-free, mesh-free
|
||||
// (single-batch) Model set; scissored paper viewports and mixed
|
||||
// 2D/3D sets fall through to the shared batched path below.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut arena_served = false;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let arena_served = false;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _perf = crate::perf::enabled();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
@ -895,9 +892,12 @@ impl shader::Primitive for Primitive {
|
|||
let ch = clip.height as f32;
|
||||
let clip_right = clip.x + clip.width;
|
||||
let clip_bottom = clip.y + clip.height;
|
||||
for (i, vp) in self.viewports.iter().enumerate() {
|
||||
let Some(inner) = pipeline.inners.get(self.base_slot + i) else {
|
||||
break;
|
||||
for vp in &self.viewports {
|
||||
let Some(slot) = pipeline.slot_by_instance.get(&vp.instance_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(inner) = pipeline.inners.get(*slot) else {
|
||||
continue;
|
||||
};
|
||||
// Where the viewport would land on the surface in absolute
|
||||
// pixels (i32 because either edge may stick off the canvas).
|
||||
|
|
@ -1433,14 +1433,9 @@ impl Scene {
|
|||
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 {
|
||||
viewports,
|
||||
bg_color,
|
||||
base_slot,
|
||||
nav_perf: perf_nav,
|
||||
}
|
||||
}
|
||||
|
|
@ -1466,7 +1461,6 @@ impl Scene {
|
|||
return Primitive {
|
||||
viewports: vec![],
|
||||
bg_color,
|
||||
base_slot: tile_idx,
|
||||
nav_perf: None,
|
||||
};
|
||||
};
|
||||
|
|
@ -1514,7 +1508,6 @@ impl Scene {
|
|||
Primitive {
|
||||
viewports,
|
||||
bg_color,
|
||||
base_slot: tile_idx,
|
||||
nav_perf: perf_nav,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue