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 <[email protected]>
This commit is contained in:
1 parent
c35b902f3a
commit
de6e27304d
7 files changed
+228
-64
No files matched your search
@@ -33,7 +33,7 @@ impl OpenCADStudio {
|
||||
dl.turn_off();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].scene.invalidate_layer_visibility(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
@@ -70,7 +70,7 @@ impl OpenCADStudio {
|
||||
dl.freeze();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].scene.invalidate_layer_visibility(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
@@ -440,7 +440,7 @@ impl OpenCADStudio {
|
||||
dl.turn_on();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].scene.invalidate_layer_visibility(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
@@ -461,7 +461,7 @@ impl OpenCADStudio {
|
||||
dl.thaw();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].scene.invalidate_layer_visibility(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
@@ -530,7 +530,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].scene.invalidate_layer_visibility(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
@@ -620,7 +620,7 @@ impl OpenCADStudio {
|
||||
dl.turn_on();
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].scene.invalidate_layer_visibility(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
|
||||
@@ -1227,9 +1227,9 @@ impl OpenCADStudio {
|
||||
"redo" => Message::Redo,
|
||||
"layer_visible" | "layer_locked" | "layer_frozen" | "layer_current" => {
|
||||
let layer = string(req, "layer")?;
|
||||
// The Layer* messages index the panel's (sortable) row list.
|
||||
let index = self.tabs[self.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.layers
|
||||
.iter()
|
||||
.position(|l| l.name == layer)
|
||||
|
||||
@@ -3338,7 +3338,7 @@ impl OpenCADStudio {
|
||||
pl.visible = on;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
|
||||
self.tabs[i].scene.invalidate_layer_visibility(&targets);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.command_line.push_output(
|
||||
@@ -3426,7 +3426,7 @@ impl OpenCADStudio {
|
||||
pl.frozen = frozen;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
|
||||
self.tabs[i].scene.invalidate_layer_visibility(&targets);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.command_line.push_output(
|
||||
|
||||
+125
-32
@@ -500,10 +500,53 @@ pub(super) struct EntityIndex {
|
||||
pub unbounded_handles: Vec<Handle>,
|
||||
}
|
||||
|
||||
/// Hash of every baked display input a memoized tessellation depends on; a
|
||||
/// memo entry is reusable only under the guard it was built with.
|
||||
fn tess_memo_guard(
|
||||
view_aabb: Option<[f32; 4]>,
|
||||
anno: f32,
|
||||
annotation_scale_handle: Option<Handle>,
|
||||
all_visible: bool,
|
||||
bg: [f32; 4],
|
||||
avp: Option<Handle>,
|
||||
) -> u64 {
|
||||
let mut g: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut mix = |x: u64| g = g.rotate_left(13) ^ x;
|
||||
// wpp removed from guard: GPU analytical rendering handles
|
||||
// circles/arcs/ellipses, Point ignores wpp, and Light (the
|
||||
// only remaining wpp consumer) is rare enough that its stale
|
||||
// glyphs don't justify clearing every memoized entity on zoom.
|
||||
if let Some(v) = view_aabb {
|
||||
for c in v {
|
||||
mix(c.to_bits() as u64);
|
||||
}
|
||||
}
|
||||
mix(anno.to_bits() as u64);
|
||||
mix(annotation_scale_handle
|
||||
.map(|handle| handle.value())
|
||||
.unwrap_or(0));
|
||||
mix(all_visible as u64);
|
||||
// Direct entities and inherited/faded block colours still bake
|
||||
// the background before Batches::finalize records its inputs.
|
||||
for channel in bg {
|
||||
mix(channel.to_bits() as u64);
|
||||
}
|
||||
mix(avp.map(|h| h.value()).unwrap_or(0));
|
||||
// SDF glyph quads bake the atlas UV of each tile, so a growth or
|
||||
// a re-bake (which rescale / rewind every UV) makes memoized text
|
||||
// address the wrong tile — garbage on screen, and a silent miss in
|
||||
// the PDF export's glyph lookup (#385 under #347's conditions).
|
||||
mix(crate::scene::text::sdf_atlas::generation());
|
||||
g
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct DependencyTargets {
|
||||
render_handles: HashSet<Handle>,
|
||||
source_handles: HashSet<Handle>,
|
||||
/// Render handles reached through a block definition (INSERTs and other
|
||||
/// block users), as opposed to entities that carry the name themselves.
|
||||
block_users: HashSet<Handle>,
|
||||
touches_block_definition: bool,
|
||||
}
|
||||
|
||||
@@ -4020,6 +4063,9 @@ impl Scene {
|
||||
let bg = self.bg_color;
|
||||
let materials: HashMap<Handle, crate::scene::model::material_model::MeshMaterial> = handles
|
||||
.iter()
|
||||
.filter(|handle| {
|
||||
self.meshes.contains_key(handle) || self.block_meshes.contains_key(handle)
|
||||
})
|
||||
.filter_map(|&handle| {
|
||||
self.document.get_entity(handle).map(|entity| {
|
||||
let color = self.render_style(entity).0;
|
||||
@@ -6706,6 +6752,12 @@ impl Scene {
|
||||
let mut new_runs: HashMap<Handle, Vec<WireModel>> = HashMap::default();
|
||||
let mut memo_updates: Vec<(Handle, Arc<Vec<WireModel>>)> = Vec::new();
|
||||
let mut visible_changed: HashSet<Handle> = HashSet::default();
|
||||
// Every geometry edit drops its handle's memo entry, so an entry that
|
||||
// survived its delta marks a visibility-only change: its wires come
|
||||
// back from the memo, provided it was built under this display context.
|
||||
let avp = style_viewport.or(self.active_viewport);
|
||||
let memo_ok = self.resident_tess_guard.get()
|
||||
== tess_memo_guard(None, anno, annotation_scale_handle, all_visible, bg, avp);
|
||||
for (h, kind) in &deltas {
|
||||
if matches!(kind, ChangeKind::Removed) {
|
||||
continue;
|
||||
@@ -6723,10 +6775,19 @@ impl Scene {
|
||||
continue;
|
||||
}
|
||||
visible_changed.insert(*h);
|
||||
let memoized = memo_ok
|
||||
.then(|| self.resident_tess_memo.borrow().get(h).cloned())
|
||||
.flatten();
|
||||
if let Some(raw) = memoized {
|
||||
let mut faded = raw.as_ref().clone();
|
||||
self.apply_refedit_fade(&mut faded, bg);
|
||||
new_runs.insert(*h, faded);
|
||||
continue;
|
||||
}
|
||||
let mut raw = tessellate_entity(
|
||||
&self.document,
|
||||
&empty_sel,
|
||||
style_viewport.or(self.active_viewport),
|
||||
avp,
|
||||
bg,
|
||||
anno,
|
||||
annotation_scale_handle,
|
||||
@@ -6777,13 +6838,20 @@ impl Scene {
|
||||
.copied()
|
||||
.unwrap_or(handle.value())
|
||||
};
|
||||
// Hashed once: a linear `order.contains` per re-shown entity made
|
||||
// turning a big layer back on quadratic.
|
||||
let ordered: HashSet<Handle> = if visible_changed.is_empty() {
|
||||
HashSet::default()
|
||||
} else {
|
||||
layout.order.iter().copied().collect()
|
||||
};
|
||||
for &(handle, kind) in &deltas {
|
||||
if matches!(kind, ChangeKind::Removed) {
|
||||
// Keep the order entry as a tombstone. Undo/Redo can then
|
||||
// restore the exact physical slot and submission position.
|
||||
continue;
|
||||
}
|
||||
if visible_changed.contains(&handle) && !layout.order.contains(&handle) {
|
||||
if visible_changed.contains(&handle) && !ordered.contains(&handle) {
|
||||
let key = effective(handle);
|
||||
let position = layout
|
||||
.order
|
||||
@@ -10748,36 +10816,8 @@ impl Scene {
|
||||
let t_build = perf.then(iced::time::Instant::now);
|
||||
let mut wires: Vec<WireModel> = if memo_active {
|
||||
// Invalidate when any baked display input changes.
|
||||
let guard = {
|
||||
let mut g: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut mix = |x: u64| g = g.rotate_left(13) ^ x;
|
||||
// wpp removed from guard: GPU analytical rendering handles
|
||||
// circles/arcs/ellipses, Point ignores wpp, and Light (the
|
||||
// only remaining wpp consumer) is rare enough that its stale
|
||||
// glyphs don't justify clearing every memoized entity on zoom.
|
||||
if let Some(v) = view_aabb {
|
||||
for c in v {
|
||||
mix(c.to_bits() as u64);
|
||||
}
|
||||
}
|
||||
mix(anno.to_bits() as u64);
|
||||
mix(annotation_scale_handle
|
||||
.map(|handle| handle.value())
|
||||
.unwrap_or(0));
|
||||
mix(all_visible as u64);
|
||||
// Direct entities and inherited/faded block colours still bake
|
||||
// the background before Batches::finalize records its inputs.
|
||||
for channel in bg {
|
||||
mix(channel.to_bits() as u64);
|
||||
}
|
||||
mix(avp.map(|h| h.value()).unwrap_or(0));
|
||||
// SDF glyph quads bake the atlas UV of each tile, so a growth or
|
||||
// a re-bake (which rescale / rewind every UV) makes memoized text
|
||||
// address the wrong tile — garbage on screen, and a silent miss in
|
||||
// the PDF export's glyph lookup (#385 under #347's conditions).
|
||||
mix(crate::scene::text::sdf_atlas::generation());
|
||||
g
|
||||
};
|
||||
let guard =
|
||||
tess_memo_guard(view_aabb, anno, annotation_scale_handle, all_visible, bg, avp);
|
||||
let (memo_cell, guard_cell) = if resident {
|
||||
(&self.resident_tess_memo, &self.resident_tess_guard)
|
||||
} else {
|
||||
@@ -11369,6 +11409,9 @@ vis_index={:.1} visible_probe={:.1}",
|
||||
target.render_handles.extend(render_handles.iter().copied());
|
||||
target.source_handles.insert(common.handle);
|
||||
target.touches_block_definition |= inside_block;
|
||||
if inside_block {
|
||||
target.block_users.extend(render_handles.iter().copied());
|
||||
}
|
||||
};
|
||||
if matches!(entity, EntityType::Point(_)) {
|
||||
extend_category(&mut index.points);
|
||||
@@ -11399,6 +11442,9 @@ vis_index={:.1} visible_probe={:.1}",
|
||||
target.render_handles.extend(render_handles.iter().copied());
|
||||
target.source_handles.insert(common.handle);
|
||||
target.touches_block_definition |= inside_block;
|
||||
if inside_block {
|
||||
target.block_users.extend(render_handles.iter().copied());
|
||||
}
|
||||
};
|
||||
let add_handle = |map: &mut HashMap<Handle, DependencyTargets>,
|
||||
handle: Option<Handle>| {
|
||||
@@ -11409,6 +11455,9 @@ vis_index={:.1} visible_probe={:.1}",
|
||||
target.render_handles.extend(render_handles.iter().copied());
|
||||
target.source_handles.insert(common.handle);
|
||||
target.touches_block_definition |= inside_block;
|
||||
if inside_block {
|
||||
target.block_users.extend(render_handles.iter().copied());
|
||||
}
|
||||
};
|
||||
add(&mut index.layers, &common.layer);
|
||||
match entity {
|
||||
@@ -11541,6 +11590,9 @@ vis_index={:.1} visible_probe={:.1}",
|
||||
combined
|
||||
.source_handles
|
||||
.extend(target.source_handles.iter().copied());
|
||||
combined
|
||||
.block_users
|
||||
.extend(target.block_users.iter().copied());
|
||||
combined.touches_block_definition |= target.touches_block_definition;
|
||||
}
|
||||
combined
|
||||
@@ -11563,6 +11615,47 @@ vis_index={:.1} visible_probe={:.1}",
|
||||
self.bump_entities_restyled(&changes);
|
||||
}
|
||||
|
||||
/// Off/freeze toggle on `names`, nothing else changed. Visibility gates an
|
||||
/// entity before it is tessellated, so entities carrying these layers keep
|
||||
/// their tessellation memo and only leave or re-enter the resident set;
|
||||
/// block users re-expand, since their definitions bake the children's
|
||||
/// visibility. Nothing is recoloured.
|
||||
pub fn invalidate_layer_visibility(&mut self, names: &[String]) {
|
||||
let targets = self.dependency_targets(DependencyKind::Layer, names);
|
||||
if targets.render_handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
if targets.touches_block_definition {
|
||||
self.block_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
let kept: Vec<_> = {
|
||||
let mut tm = self.tess_memo.borrow_mut();
|
||||
let mut rm = self.resident_tess_memo.borrow_mut();
|
||||
targets
|
||||
.render_handles
|
||||
.iter()
|
||||
.filter(|handle| !targets.block_users.contains(handle))
|
||||
.map(|&handle| (handle, tm.remove(&handle), rm.remove(&handle)))
|
||||
.collect()
|
||||
};
|
||||
let changes: Vec<(Handle, ChangeKind)> = targets
|
||||
.render_handles
|
||||
.iter()
|
||||
.map(|&handle| (handle, ChangeKind::Modified))
|
||||
.collect();
|
||||
self.bump_entities_restyled(&changes);
|
||||
let mut tm = self.tess_memo.borrow_mut();
|
||||
let mut rm = self.resident_tess_memo.borrow_mut();
|
||||
for (handle, culled, resident) in kept {
|
||||
if let Some(wires) = culled {
|
||||
tm.insert(handle, wires);
|
||||
}
|
||||
if let Some(wires) = resident {
|
||||
rm.insert(handle, wires);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_layer_dependencies(&mut self, names: &[String]) {
|
||||
let targets = self.dependency_targets(DependencyKind::Layer, names);
|
||||
self.invalidate_dependency_targets(targets);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -33,7 +33,7 @@ struct WireConst {
|
||||
align_total: f32,
|
||||
world_half_width: f32,
|
||||
is_tapered: f32,
|
||||
_pad2: f32,
|
||||
hidden: f32,
|
||||
marker_origin_high: vec4<f32>,
|
||||
marker_origin_low: vec4<f32>,
|
||||
marker_normal_scale: vec4<f32>,
|
||||
@@ -244,6 +244,12 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, c: WireCon
|
||||
out.min_elem = min_elem;
|
||||
out.align_end = c.align_end * lt_scale;
|
||||
out.align_total = c.align_total;
|
||||
// A hidden entity's arena slab stays resident; collapse it outside the
|
||||
// clip volume and take the tombstone discard.
|
||||
if c.hidden != 0.0 {
|
||||
out.clip_pos = vec4<f32>(0.0, 0.0, -2.0, 1.0);
|
||||
out.pattern_length = -1.0;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user