forked from 1b-agent/opencadstudio-upstream
perf(layers): toggle layer visibility without re-tessellating
Turning a 141k-entity layer off/on took 1.5-5 s; it now takes ~0.6-0.9 s. - Off/freeze toggles go through invalidate_layer_visibility: entities on the layer keep their tessellation memo (visibility gates them before tessellation); only block users re-expand. No mesh recolor. - The resident patch reuses a surviving memo entry instead of re-tessellating, guarded by the same display-context hash as the full build. - The patch looked up `layout.order` linearly per re-shown entity, which made turning a big layer back on quadratic. - recolor_meshes_for_handles resolved materials for handles with no mesh. - Wire arena: a hidden slab keeps its instances and sets a `hidden` flag in its consts (collapsed in the vertex shader); an unchanged re-show only clears the flag. Const writes are batched into merged spans. - The `layer_visible` automation action indexed the document's layer table instead of the sortable panel rows the Layer* messages use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c35b902f3a
commit
de6e27304d
7 files changed
+228
-64
No files matched your search
@@ -63,11 +63,16 @@ struct Slab {
|
||||
/// the whole slab by the base-depth delta instead of flattening those
|
||||
/// offsets.
|
||||
base_depth: f32,
|
||||
/// `slab_content` of what the slab holds, so re-showing a hidden entity
|
||||
/// with unchanged geometry only clears its `hidden` flag. Unused (0) by
|
||||
/// the packed arena.
|
||||
content: u64,
|
||||
}
|
||||
|
||||
struct PreparedPatchRun {
|
||||
insts: Vec<WireInstance>,
|
||||
csts: Vec<WireConst>,
|
||||
content: u64,
|
||||
base_depth: f32,
|
||||
aabb: [f32; 4],
|
||||
order_sensitive: bool,
|
||||
@@ -431,6 +436,38 @@ fn blank_const() -> WireConst {
|
||||
blank
|
||||
}
|
||||
|
||||
/// Position-independent id of an emitted slab: its instances with slab-local
|
||||
/// wire ids, then its consts with the visibility flag cleared.
|
||||
fn slab_content(insts: &[WireInstance], const_off: u32, csts: &[WireConst]) -> u64 {
|
||||
use std::hash::Hasher;
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
for inst in insts {
|
||||
let mut local = *inst;
|
||||
local.wire_id -= const_off;
|
||||
hasher.write(bytemuck::bytes_of(&local));
|
||||
}
|
||||
for cst in csts {
|
||||
let mut shown = *cst;
|
||||
shown.hidden = 0.0;
|
||||
hasher.write(bytemuck::bytes_of(&shown));
|
||||
}
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
/// Merge const index ranges whose gap is small enough that one write beats two.
|
||||
fn merge_const_ranges(mut ranges: Vec<(u32, u32)>) -> Vec<(u32, u32)> {
|
||||
const GAP: u32 = 256;
|
||||
ranges.sort_unstable();
|
||||
let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
|
||||
for (start, end) in ranges {
|
||||
match merged.last_mut() {
|
||||
Some(last) if start <= last.1 + GAP => last.1 = last.1.max(end),
|
||||
_ => merged.push((start, end)),
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
/// A tombstoned instance. Const slot 0 carries the shader discard sentinel.
|
||||
fn blank_instance() -> WireInstance {
|
||||
WireInstance {
|
||||
@@ -485,6 +522,7 @@ impl WireArena {
|
||||
const_off: u32,
|
||||
base_depth: f32,
|
||||
aabb: [f32; 4],
|
||||
content: u64,
|
||||
instances: Vec<WireInstance>,
|
||||
consts: Vec<WireConst>,
|
||||
}
|
||||
@@ -540,6 +578,7 @@ impl WireArena {
|
||||
const_off: plan.const_off,
|
||||
base_depth: plan.base_depth,
|
||||
aabb: run_aabb(run),
|
||||
content: slab_content(&instances, plan.const_off, &consts),
|
||||
instances,
|
||||
consts,
|
||||
}
|
||||
@@ -574,6 +613,7 @@ impl WireArena {
|
||||
const_len,
|
||||
aabb: packed_slab.aabb,
|
||||
base_depth: packed_slab.base_depth,
|
||||
content: packed_slab.content,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -698,11 +738,13 @@ impl WireArena {
|
||||
}
|
||||
}
|
||||
|
||||
let content = slab_content(&insts, 0, &csts);
|
||||
prepared.insert(
|
||||
h,
|
||||
PreparedPatchRun {
|
||||
insts,
|
||||
csts,
|
||||
content,
|
||||
base_depth: if self.mesh_edge {
|
||||
0.0
|
||||
} else {
|
||||
@@ -714,16 +756,25 @@ impl WireArena {
|
||||
);
|
||||
}
|
||||
|
||||
// Const edits are mirrored on the CPU and uploaded once at the end in
|
||||
// merged spans: a layer toggle touches ~10^5 slabs, and a write per
|
||||
// slab cost more than the change itself.
|
||||
let mut dirty_consts: Vec<(u32, u32)> = Vec::new();
|
||||
for &(h, kind) in changes {
|
||||
let run = runs.get(&h).map(Vec::as_slice).unwrap_or(&[]);
|
||||
|
||||
// Removed / now-hidden ⇒ tombstone the slab. A handle not in THIS
|
||||
// arena's subset (it belongs to the other batch) simply isn't in its
|
||||
// slabs, so this is a no-op for it.
|
||||
// Removed / now-hidden ⇒ tombstone the slab by flagging its consts
|
||||
// hidden; the instances stay put for an unchanged re-show. A handle
|
||||
// not in THIS arena's subset (it belongs to the other batch) simply
|
||||
// isn't in its slabs, so this is a no-op for it.
|
||||
if matches!(kind, ChangeKind::Removed) || run.is_empty() {
|
||||
if let Some(slab) = self.slabs.remove(&h) {
|
||||
let blanks = vec![blank_instance(); slab.inst_len as usize];
|
||||
self.write_insts(queue, slab.inst_off, &blanks);
|
||||
let start = slab.const_off as usize;
|
||||
let end = start + slab.const_len as usize;
|
||||
for cst in &mut self.consts_cpu[start..end] {
|
||||
cst.hidden = 1.0;
|
||||
}
|
||||
dirty_consts.push((slab.const_off, slab.const_off + slab.const_len));
|
||||
self.tombstoned += slab.inst_len;
|
||||
if matches!(kind, ChangeKind::Modified) {
|
||||
self.vacant.insert(h, slab);
|
||||
@@ -738,6 +789,7 @@ impl WireArena {
|
||||
let PreparedPatchRun {
|
||||
mut insts,
|
||||
csts,
|
||||
content,
|
||||
base_depth,
|
||||
aabb,
|
||||
order_sensitive: run_order_sensitive,
|
||||
@@ -764,29 +816,27 @@ impl WireArena {
|
||||
.unwrap_or(false);
|
||||
|
||||
if in_place {
|
||||
let (inst_off, const_off) = {
|
||||
let (inst_off, const_off, same_content) = {
|
||||
let s = self.slabs.get(&h).unwrap();
|
||||
(s.inst_off, s.const_off)
|
||||
(s.inst_off, s.const_off, s.content == content)
|
||||
};
|
||||
for w in insts.iter_mut() {
|
||||
w.wire_id += const_off;
|
||||
if !same_content {
|
||||
for w in insts.iter_mut() {
|
||||
w.wire_id += const_off;
|
||||
}
|
||||
self.write_insts(queue, inst_off, &insts);
|
||||
}
|
||||
self.write_insts(queue, inst_off, &insts);
|
||||
for (k, c) in csts.iter().enumerate() {
|
||||
self.consts_cpu[const_off as usize + k] = *c;
|
||||
}
|
||||
// Push the entity's consts to the GPU too — an in-place edit is
|
||||
// NOT structural, so the whole-buffer refresh below won't run and
|
||||
// a colour change would otherwise never reach the shader.
|
||||
let csz = std::mem::size_of::<WireConst>() as u64;
|
||||
queue.write_buffer(
|
||||
&self.const_buf,
|
||||
const_off as u64 * csz,
|
||||
bytemuck::cast_slice(&csts),
|
||||
);
|
||||
// The consts reach the GPU in the merged upload below — an
|
||||
// in-place edit is NOT structural, so without it a colour change
|
||||
// or a re-show would never reach the shader.
|
||||
dirty_consts.push((const_off, const_off + const_len));
|
||||
let slab = self.slabs.get_mut(&h).unwrap();
|
||||
slab.base_depth = base_depth;
|
||||
slab.aabb = aabb;
|
||||
slab.content = content;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -830,6 +880,7 @@ impl WireArena {
|
||||
slab.const_len = const_len;
|
||||
slab.base_depth = base_depth;
|
||||
slab.aabb = aabb;
|
||||
slab.content = content;
|
||||
self.order_sensitive |= run_order_sensitive;
|
||||
continue;
|
||||
}
|
||||
@@ -887,11 +938,20 @@ impl WireArena {
|
||||
const_len,
|
||||
aabb,
|
||||
base_depth,
|
||||
content,
|
||||
},
|
||||
);
|
||||
self.order_sensitive |= run_order_sensitive;
|
||||
}
|
||||
|
||||
for (start, end) in merge_const_ranges(dirty_consts) {
|
||||
let size = std::mem::size_of::<WireConst>() as u64;
|
||||
queue.write_buffer(
|
||||
&self.const_buf,
|
||||
start as u64 * size,
|
||||
bytemuck::cast_slice(&self.consts_cpu[start as usize..end as usize]),
|
||||
);
|
||||
}
|
||||
if self.tombstoned > self.inst_tail / 2 {
|
||||
return false;
|
||||
}
|
||||
@@ -1187,6 +1247,7 @@ impl PackedWireArena {
|
||||
const_len: 0,
|
||||
aabb: packed_slab.aabb,
|
||||
base_depth: packed_slab.base_depth,
|
||||
content: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1386,6 +1447,7 @@ impl PackedWireArena {
|
||||
const_len: 0,
|
||||
aabb,
|
||||
base_depth,
|
||||
content: 0,
|
||||
},
|
||||
);
|
||||
self.order_sensitive |= run_order_sensitive;
|
||||
@@ -1588,6 +1650,7 @@ mod tests {
|
||||
const_len: 2,
|
||||
aabb: [0.0; 4],
|
||||
base_depth: 0.0,
|
||||
content: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,9 @@ pub struct WireConst {
|
||||
/// in drawing units.
|
||||
pub world_half_width: f32,
|
||||
pub is_tapered: f32,
|
||||
pub _pad2: f32,
|
||||
/// Non-zero hides every segment of the wire: arena slabs of hidden
|
||||
/// entities stay resident and are collapsed by the vertex shader.
|
||||
pub hidden: f32,
|
||||
/// Point-marker origin as a double-single pair. `marker_normal_scale.w`
|
||||
/// stores the viewport-height percentage; zero disables marker scaling.
|
||||
pub marker_origin_high: [f32; 4],
|
||||
@@ -776,7 +778,7 @@ pub(crate) fn emit_wire_native(
|
||||
align_total,
|
||||
world_half_width: wire.world_width * 0.5,
|
||||
is_tapered: if is_tapered { 1.0 } else { 0.0 },
|
||||
_pad2: 0.0,
|
||||
hidden: 0.0,
|
||||
marker_origin_high,
|
||||
marker_origin_low,
|
||||
marker_normal_scale,
|
||||
|
||||
Reference in new issue
Block a user