forked from 1b-agent/opencadstudio-upstream
perf(render): in-place GPU wire arena for move/edit (phase 4, OCS_WIRE_GPU_PATCH)
On a geometry edit the resident set gets a fresh content id, so the batched wire path (build_wire_buffers) re-emits and re-uploads EVERY wire instance — the whole buffer even when one line moved. Behind the OCS_WIRE_GPU_PATCH flag (default off, native only), add a persistent per-entity instance arena: one long-lived VERTEX|COPY_DST instance buffer + its WireConst storage, with a per-entity slab table. An in-place edit (move / rotate / scale / colour — same segment count) becomes a queue.write_buffer of just that entity`s slab. Scope is deliberately IN-PLACE ONLY. draw_depth_map normalises each entity`s draw-order z-bias by the block`s entity count, so an Add or Remove re-scales EVERY entity`s z-bias — which a per-slab patch can`t honour — and a segment-count change relocates the entity. patch() returns false for any of those (and for mesh-edge / scissored sets), and the caller falls back to the identical full batched rebuild, so correctness never rides on the fast path. An adversarial review of an earlier append/tombstone design surfaced exactly these draw-order hazards; restricting to in-place removes them while keeping the common interactive-edit win. The Scene hands the changed handles to the arena via model_wire_gpu_patch, recorded by try_resident_patch for the Model block and matched by content id in prepare(); scissored paper viewports and mixed 2D/3D sets keep the batched path. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
0907d2912d
commit
38c8583d27
5 files changed
+398
-6
No files matched your search
+55
-3
@@ -121,6 +121,21 @@ struct GeometryDelta {
|
||||
/// and does a one-time full rebuild — the safe fallback, not a correctness hole.
|
||||
const GEOMETRY_JOURNAL_CAP: usize = 256;
|
||||
|
||||
/// Whether the persistent per-entity GPU wire arena (`OCS_WIRE_GPU_PATCH`) is
|
||||
/// enabled — patches one entity's instance slab on an edit instead of rebuilding
|
||||
/// the whole wire buffer. Opt-in while it is validated visually. Always off on
|
||||
/// wasm (WebGL2 has the fat self-contained instance, no arena).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn wire_gpu_patch_enabled() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static ON: OnceLock<bool> = OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var_os("OCS_WIRE_GPU_PATCH").is_some())
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) fn wire_gpu_patch_enabled() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Resolve a viewport's paper-to-model scale ratio from its two
|
||||
/// DXF-derived sources.
|
||||
///
|
||||
@@ -983,6 +998,13 @@ pub struct Scene {
|
||||
/// a full re-tessellation. Diagnostics + a hook for the oracle test to prove
|
||||
/// the fast path is actually exercised (not silently always falling back).
|
||||
resident_patch_hits: std::cell::Cell<u64>,
|
||||
/// Handoff for the GPU wire arena (`OCS_WIRE_GPU_PATCH`): when the MODEL
|
||||
/// resident set was brought up to date by an incremental patch, this records
|
||||
/// `(prev_gen, new_gen, changed handles)` so the render layer can patch just
|
||||
/// those entities' instance slabs instead of re-uploading every wire. The
|
||||
/// render layer matches `new_gen` against the viewport's content id and
|
||||
/// `prev_gen` against what the GPU currently holds; a mismatch just rebuilds.
|
||||
model_wire_gpu_patch: RefCell<Option<(u64, u64, Arc<Vec<(Handle, ChangeKind)>>)>>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
@@ -1067,6 +1089,7 @@ impl Scene {
|
||||
geometry_deltas: RefCell::new(std::collections::VecDeque::new()),
|
||||
geometry_journal_floor: std::cell::Cell::new(0),
|
||||
resident_patch_hits: std::cell::Cell::new(0),
|
||||
model_wire_gpu_patch: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2654,6 +2677,11 @@ impl Scene {
|
||||
let arc = Arc::new(wires);
|
||||
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||
self.last_model_wire_gen.set(gen);
|
||||
// A full rebuild of the Model set — the GPU arena must rebuild too, not
|
||||
// apply a stale patch, so clear any pending handoff.
|
||||
if block == self.model_space_block_handle() {
|
||||
*self.model_wire_gpu_patch.borrow_mut() = None;
|
||||
}
|
||||
let mut sets = self.resident_wire_sets.borrow_mut();
|
||||
// Evict stale entries (older epochs / abandoned keys) so switching
|
||||
// spaces or re-scaling a viewport can't accumulate dead full sets.
|
||||
@@ -2666,6 +2694,21 @@ impl Scene {
|
||||
arc
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// Model tile applies the same patch; gen-matching keeps it from applying to
|
||||
/// a viewport that rebuilt independently.
|
||||
pub(crate) fn model_wire_patch_for(
|
||||
&self,
|
||||
gen: u64,
|
||||
) -> Option<(u64, Arc<Vec<(Handle, ChangeKind)>>)> {
|
||||
match &*self.model_wire_gpu_patch.borrow() {
|
||||
Some((prev, new, changes)) if *new == gen => Some((*prev, Arc::clone(changes))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring the resident set for `key` up to the current epoch by replaying only
|
||||
/// the changed entities into the cached assembly. Returns the patched `Arc`,
|
||||
/// or `None` to fall back to a full rebuild — the safe default whenever the
|
||||
@@ -2696,9 +2739,11 @@ impl Scene {
|
||||
let deltas = self.replay_since(cached_epoch)?;
|
||||
|
||||
// Take ownership of the cached assembly (guaranteed unique above).
|
||||
let owned: Vec<WireModel> = {
|
||||
let arc = self.resident_wire_sets.borrow_mut().remove(&key)?.2;
|
||||
Arc::try_unwrap(arc).ok()?
|
||||
// `prev_gen` is the content id the GPU currently holds for this set — the
|
||||
// base the wire-arena patch replays from.
|
||||
let (prev_gen, owned) = {
|
||||
let removed = self.resident_wire_sets.borrow_mut().remove(&key)?;
|
||||
(removed.1, Arc::try_unwrap(removed.2).ok()?)
|
||||
};
|
||||
|
||||
// Group into per-entity runs. Bail if any wire isn't named with a handle
|
||||
@@ -2818,6 +2863,13 @@ impl Scene {
|
||||
.set(self.resident_patch_hits.get() + 1);
|
||||
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||
self.last_model_wire_gen.set(gen);
|
||||
// Hand the exact changed handles to the GPU wire arena so it patches just
|
||||
// those entities' slabs. Only for the Model set (the arena is model-only);
|
||||
// the render layer verifies prev_gen against what the GPU actually holds.
|
||||
if wire_gpu_patch_enabled() && block == self.model_space_block_handle() {
|
||||
*self.model_wire_gpu_patch.borrow_mut() =
|
||||
Some((prev_gen, gen, Arc::new(deltas.clone())));
|
||||
}
|
||||
let cur_epoch = self.geometry_epoch;
|
||||
let mut sets = self.resident_wire_sets.borrow_mut();
|
||||
sets.retain(|_, (e, ..)| *e == cur_epoch);
|
||||
|
||||
@@ -10,6 +10,11 @@ pub mod mesh_gpu;
|
||||
pub mod text_gpu;
|
||||
pub mod uniforms;
|
||||
pub mod viewcube;
|
||||
/// Persistent per-entity wire instance arena (native), enabled by
|
||||
/// `OCS_WIRE_GPU_PATCH` — patches one entity's slab via `write_buffer` instead
|
||||
/// of re-uploading the whole wire set on an edit.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod wire_arena;
|
||||
pub mod wire_gpu;
|
||||
|
||||
use iced::wgpu;
|
||||
@@ -44,7 +49,7 @@ pub struct Pipeline {
|
||||
/// xray pipelines). `Some` on native; `None` on web (fat instance, no
|
||||
/// storage). Passed to `WireGpu::from_run` / `from_batch` so they can build
|
||||
/// each batch's per-wire bind group.
|
||||
wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
||||
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
||||
wipeout_pipeline: wgpu::RenderPipeline,
|
||||
/// Phase 4-B — single-draw batched hatch pipeline. Per-instance
|
||||
/// data lives in storage buffers; one draw call covers every
|
||||
@@ -144,6 +149,14 @@ pub struct Pipeline {
|
||||
/// their camera uniforms + scissor stay per-slot, only the geometry is
|
||||
/// deduplicated. Never mutated in place, only reassigned on content change.
|
||||
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
|
||||
/// Persistent per-entity wire instance arena (native, `OCS_WIRE_GPU_PATCH`).
|
||||
/// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an
|
||||
/// edit patches one entity's slab in place instead of rebuilding every wire.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) wire_arena: Option<wire_arena::WireArena>,
|
||||
/// The Model content id `wire_arena` currently mirrors (`u64::MAX` = none).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) wire_arena_id: u64,
|
||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped wires. Recomputed each frame.
|
||||
wire_pixel_scissors: Vec<Option<[u32; 4]>>,
|
||||
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
|
||||
@@ -1304,6 +1317,10 @@ impl Pipeline {
|
||||
blit_uniform_buffer,
|
||||
surface_format: format,
|
||||
gpu_wires: std::sync::Arc::new(vec![]),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_arena: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_arena_id: u64::MAX,
|
||||
wire_pixel_scissors: vec![],
|
||||
gpu_selected_wires: vec![],
|
||||
gpu_preview_wires: vec![],
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// Persistent per-entity wire instance arena (native, behind OCS_WIRE_GPU_PATCH).
|
||||
//
|
||||
// The normal wire path re-emits EVERY wire into a fresh, tightly-packed instance
|
||||
// buffer whenever the resident set's content id changes — so moving one line on a
|
||||
// million-wire drawing re-uploads all million instances. This arena instead keeps
|
||||
// one persistent instance buffer (plus its shared WireConst storage) with per-
|
||||
// entity *slabs*, so an in-place edit is a `queue.write_buffer` of just that
|
||||
// entity's slab.
|
||||
//
|
||||
// Scope (v1): IN-PLACE patches only. A move / rotate / scale / colour change keeps
|
||||
// an entity's slab, its instance count, and — crucially — the block's entity count,
|
||||
// so `draw_depth_map`'s per-entity z-bias (normalised by that count) is unchanged
|
||||
// and every other slab stays correct. `patch` returns false (⇒ the caller does a
|
||||
// full batched rebuild) for anything else: an Add or Remove (which re-scales every
|
||||
// entity's z-bias), a segment-count change, or a set that isn't a single scissor-
|
||||
// free batch (mesh-edge fills, paper viewport scissor). Because a full rebuild is
|
||||
// always the fallback, correctness never rides on the fast path.
|
||||
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use super::wire_gpu::{emit_wire_native, wire_draw_depth, WireConst, WireGpu, WireInstance};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::scene::ChangeKind;
|
||||
use acadrust::Handle;
|
||||
use iced::wgpu;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
struct Slab {
|
||||
inst_off: u32,
|
||||
inst_len: u32,
|
||||
const_off: u32,
|
||||
const_len: u32,
|
||||
}
|
||||
|
||||
pub struct WireArena {
|
||||
inst_buf: wgpu::Buffer,
|
||||
inst_tail: u32,
|
||||
const_buf: wgpu::Buffer,
|
||||
const_bind_group: std::sync::Arc<wgpu::BindGroup>,
|
||||
slabs: FxHashMap<Handle, Slab>,
|
||||
}
|
||||
|
||||
fn handle_of(w: &WireModel) -> Option<Handle> {
|
||||
crate::scene::Scene::handle_from_wire_name(&w.name)
|
||||
}
|
||||
|
||||
/// True if the whole set is arena-eligible: a single batch that draws with no
|
||||
/// viewport scissor — no mesh/solid fill (which forces the multi-batch draw-order
|
||||
/// split) and no per-wire scissor (paper content viewports, which the arena's
|
||||
/// scissor-free wrapper would fail to clip). Model tiles qualify; scissored paper
|
||||
/// viewports fall back to the normal batched path.
|
||||
pub fn is_arena_eligible(wires: &[WireModel]) -> bool {
|
||||
wires
|
||||
.iter()
|
||||
.all(|w| w.fill_tris.is_empty() && w.vp_scissor.is_none())
|
||||
}
|
||||
|
||||
/// handle → wire-slot index for the selection / text-highlight overlays, built
|
||||
/// from the resident Vec (independent of the arena's slab layout, exactly like
|
||||
/// the batched path's index).
|
||||
pub fn build_handle_index(wires: &[WireModel]) -> std::sync::Arc<FxHashMap<u64, Vec<u32>>> {
|
||||
let mut index: FxHashMap<u64, Vec<u32>> = FxHashMap::default();
|
||||
index.reserve(wires.len());
|
||||
for (idx, w) in wires.iter().enumerate() {
|
||||
if let Ok(h) = w.name.parse::<u64>() {
|
||||
index.entry(h).or_default().push(idx as u32);
|
||||
}
|
||||
}
|
||||
std::sync::Arc::new(index)
|
||||
}
|
||||
|
||||
/// Group `wires` (draw-order sorted, entity-contiguous) into per-handle ranges.
|
||||
fn handle_ranges(wires: &[WireModel]) -> Option<Vec<(Handle, usize, usize)>> {
|
||||
let mut out: Vec<(Handle, usize, usize)> = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < wires.len() {
|
||||
let h = handle_of(&wires[i])?;
|
||||
let mut j = i + 1;
|
||||
while j < wires.len() && handle_of(&wires[j]) == Some(h) {
|
||||
j += 1;
|
||||
}
|
||||
out.push((h, i, j));
|
||||
i = j;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn build_const_bind_group(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
buf: &wgpu::Buffer,
|
||||
) -> std::sync::Arc<wgpu::BindGroup> {
|
||||
std::sync::Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("wire_arena.const.bg"),
|
||||
layout: bgl,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: buf.as_entire_binding(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
|
||||
fn alloc_inst(device: &wgpu::Device, cap: u32) -> wgpu::Buffer {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("wire_arena.ibuf"),
|
||||
size: (cap as u64) * std::mem::size_of::<WireInstance>() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn alloc_const(device: &wgpu::Device, cap: u32) -> wgpu::Buffer {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("wire_arena.cbuf"),
|
||||
size: (cap as u64) * std::mem::size_of::<WireConst>() as u64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
}
|
||||
|
||||
impl WireArena {
|
||||
/// Build a fresh arena from the full resident set, or `None` if it isn't
|
||||
/// single-batch (has mesh-edge fills) or a wire is unnamed — in which case
|
||||
/// the caller keeps using the normal batched path.
|
||||
pub fn build(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[WireModel],
|
||||
depth_map: &FxHashMap<u64, f32>,
|
||||
const_bgl: &wgpu::BindGroupLayout,
|
||||
) -> Option<Self> {
|
||||
if !is_arena_eligible(wires) {
|
||||
return None;
|
||||
}
|
||||
let ranges = handle_ranges(wires)?;
|
||||
|
||||
let mut instances: Vec<WireInstance> = Vec::new();
|
||||
let mut consts: Vec<WireConst> = Vec::new();
|
||||
let mut slabs: FxHashMap<Handle, Slab> = FxHashMap::default();
|
||||
for (h, i, j) in ranges {
|
||||
let inst_off = instances.len() as u32;
|
||||
let const_off = consts.len() as u32;
|
||||
for w in &wires[i..j] {
|
||||
let wire_id = consts.len() as u32;
|
||||
let dd = wire_draw_depth(w, depth_map);
|
||||
let (mut insts, cst) = emit_wire_native(w, wire_id, w.color, dd);
|
||||
instances.append(&mut insts);
|
||||
consts.push(cst);
|
||||
}
|
||||
slabs.insert(
|
||||
h,
|
||||
Slab {
|
||||
inst_off,
|
||||
inst_len: instances.len() as u32 - inst_off,
|
||||
const_off,
|
||||
const_len: consts.len() as u32 - const_off,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// In-place-only: allocate exactly the set's size (no append headroom).
|
||||
let inst_tail = instances.len() as u32;
|
||||
let inst_buf = alloc_inst(device, inst_tail.max(1));
|
||||
let const_buf = alloc_const(device, (consts.len() as u32).max(1));
|
||||
if inst_tail > 0 {
|
||||
queue.write_buffer(&inst_buf, 0, bytemuck::cast_slice(&instances));
|
||||
}
|
||||
queue.write_buffer(&const_buf, 0, bytemuck::cast_slice(&consts));
|
||||
let const_bind_group = build_const_bind_group(device, const_bgl, &const_buf);
|
||||
|
||||
Some(Self {
|
||||
inst_buf,
|
||||
inst_tail,
|
||||
const_buf,
|
||||
const_bind_group,
|
||||
slabs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply the changed handles to the arena in place. Returns false (⇒ the
|
||||
/// caller does a full batched rebuild) for any change that isn't an in-place
|
||||
/// slab overwrite — see the module header.
|
||||
pub fn patch(
|
||||
&mut self,
|
||||
queue: &wgpu::Queue,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
wires: &[WireModel],
|
||||
depth_map: &FxHashMap<u64, f32>,
|
||||
) -> bool {
|
||||
if !is_arena_eligible(wires) {
|
||||
return false;
|
||||
}
|
||||
let Some(ranges) = handle_ranges(wires) else {
|
||||
return false;
|
||||
};
|
||||
let range_of: FxHashMap<Handle, (usize, usize)> =
|
||||
ranges.into_iter().map(|(h, i, j)| (h, (i, j))).collect();
|
||||
|
||||
let inst_sz = std::mem::size_of::<WireInstance>() as u64;
|
||||
let const_sz = std::mem::size_of::<WireConst>() as u64;
|
||||
|
||||
// Only IN-PLACE updates are safe to patch: an entity keeps its slab and
|
||||
// its instance/const count (a move / rotate / scale / colour change).
|
||||
// Add / Remove / any count-changed edit alter the layout AND — because
|
||||
// draw_depth_map normalises each entity's z-bias by the block's entity
|
||||
// count — re-scale EVERY entity's draw order, which we can't patch slab-
|
||||
// by-slab. Those fall back to a full rebuild (return false), where the
|
||||
// batched path re-emits everything with the current depth map.
|
||||
for &(h, kind) in changes {
|
||||
if matches!(kind, ChangeKind::Added | ChangeKind::Removed) {
|
||||
return false;
|
||||
}
|
||||
let Some(&(i, j)) = range_of.get(&h) else {
|
||||
return false; // Modified but now absent (hidden / not in set)
|
||||
};
|
||||
let Some(&Slab {
|
||||
inst_off,
|
||||
inst_len,
|
||||
const_off,
|
||||
const_len,
|
||||
}) = self.slabs.get(&h)
|
||||
else {
|
||||
return false; // no existing slab to overwrite
|
||||
};
|
||||
let run = &wires[i..j];
|
||||
|
||||
// Emit into the entity's existing const slots (wire_id = const_off + k).
|
||||
let mut insts: Vec<WireInstance> = Vec::new();
|
||||
let mut csts: Vec<WireConst> = Vec::new();
|
||||
for w in run {
|
||||
let wire_id = const_off + csts.len() as u32;
|
||||
let dd = wire_draw_depth(w, depth_map);
|
||||
let (mut wi, c) = emit_wire_native(w, wire_id, w.color, dd);
|
||||
insts.append(&mut wi);
|
||||
csts.push(c);
|
||||
}
|
||||
if insts.len() as u32 != inst_len || csts.len() as u32 != const_len {
|
||||
return false; // segment count changed ⇒ not in place ⇒ rebuild
|
||||
}
|
||||
if inst_len > 0 {
|
||||
queue.write_buffer(
|
||||
&self.inst_buf,
|
||||
inst_off as u64 * inst_sz,
|
||||
bytemuck::cast_slice(&insts),
|
||||
);
|
||||
}
|
||||
queue.write_buffer(
|
||||
&self.const_buf,
|
||||
const_off as u64 * const_sz,
|
||||
bytemuck::cast_slice(&csts),
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// One draw batch wrapping the persistent instance buffer. `instance_count`
|
||||
/// is the whole tail (tombstones included — they draw nothing).
|
||||
pub fn wire_gpus(&self) -> Vec<WireGpu> {
|
||||
if self.inst_tail == 0 {
|
||||
return vec![];
|
||||
}
|
||||
vec![WireGpu {
|
||||
instance_buffer: self.inst_buf.clone(),
|
||||
instance_count: self.inst_tail,
|
||||
vp_scissor: None,
|
||||
is_3d_mesh_edge: false,
|
||||
const_bind_group: Some(self.const_bind_group.clone()),
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -383,7 +383,7 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve
|
||||
/// Native: 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"))]
|
||||
fn emit_wire_native(
|
||||
pub(crate) fn emit_wire_native(
|
||||
wire: &WireModel,
|
||||
wire_id: u32,
|
||||
color: [f32; 4],
|
||||
@@ -432,7 +432,10 @@ fn emit_wire_native(
|
||||
/// Looks up a wire's draw-order depth from the per-entity map using the
|
||||
/// handle encoded in its `name`. Falls back to 0.0 (transient / preview
|
||||
/// wires that carry no document handle).
|
||||
fn wire_draw_depth(wire: &WireModel, depth_map: &rustc_hash::FxHashMap<u64, f32>) -> f32 {
|
||||
pub(crate) fn wire_draw_depth(
|
||||
wire: &WireModel,
|
||||
depth_map: &rustc_hash::FxHashMap<u64, f32>,
|
||||
) -> f32 {
|
||||
wire
|
||||
.name
|
||||
.parse::<u64>()
|
||||
|
||||
@@ -105,6 +105,11 @@ pub struct ViewportData {
|
||||
/// when only the camera moved. Non-tile and preview/interim frames carry a
|
||||
/// fresh id each time → always re-upload.
|
||||
pub(in crate::scene) wire_content_id: u64,
|
||||
/// GPU wire-arena handoff (`OCS_WIRE_GPU_PATCH`): `(prev_gen, changed)` when
|
||||
/// this Model set reached `wire_content_id` by an incremental resident patch,
|
||||
/// so `prepare` can patch just those entities' slabs. `None` ⇒ full build.
|
||||
pub(in crate::scene) wire_patch:
|
||||
Option<(u64, Arc<Vec<(acadrust::Handle, crate::scene::ChangeKind)>>)>,
|
||||
/// Selected handles only (no hover) — solid meshes tint these blue.
|
||||
pub(in crate::scene) selected_handles: Arc<rustc_hash::FxHashSet<acadrust::Handle>>,
|
||||
/// Currently hovered handle — solid meshes tint it orange.
|
||||
@@ -357,6 +362,48 @@ impl shader::Primitive for Primitive {
|
||||
// independent of the `cur_key` block so a preview/interim wire change
|
||||
// still uploads even when the camera didn't move.
|
||||
if vp.wire_content_id != inner.cached_wire_id {
|
||||
// Persistent per-entity wire arena (OCS_WIRE_GPU_PATCH): patch
|
||||
// just the changed entities' instance slabs instead of rebuilding
|
||||
// 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.
|
||||
let mut arena_served = false;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if crate::scene::wire_gpu_patch_enabled()
|
||||
&& inner.wire_const_bgl.is_some()
|
||||
&& crate::scene::pipeline::wire_arena::is_arena_eligible(&vp.wires)
|
||||
{
|
||||
use crate::scene::pipeline::wire_arena;
|
||||
// In-place patch from the base the GPU holds, else rebuild.
|
||||
let patched = if let (Some(arena), Some((base, changes))) =
|
||||
(inner.wire_arena.as_mut(), vp.wire_patch.as_ref())
|
||||
{
|
||||
inner.wire_arena_id == *base
|
||||
&& !changes.is_empty()
|
||||
&& arena.patch(queue, changes, &vp.wires[..], &vp.draw_depths)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !patched {
|
||||
let bgl = inner.wire_const_bgl.as_ref().unwrap();
|
||||
inner.wire_arena = wire_arena::WireArena::build(
|
||||
device,
|
||||
queue,
|
||||
&vp.wires[..],
|
||||
&vp.draw_depths,
|
||||
bgl,
|
||||
);
|
||||
}
|
||||
if let Some(arena) = inner.wire_arena.as_ref() {
|
||||
inner.gpu_wires = std::sync::Arc::new(arena.wire_gpus());
|
||||
inner.wire_handle_index =
|
||||
wire_arena::build_handle_index(&vp.wires[..]);
|
||||
inner.wire_arena_id = vp.wire_content_id;
|
||||
arena_served = true;
|
||||
} else {
|
||||
inner.wire_arena_id = u64::MAX;
|
||||
}
|
||||
}
|
||||
// Share one copy of the resident wire buffers across every slot
|
||||
// (and every pane — one MultiPipeline backs them all) rendering
|
||||
// this content id: build on a cache miss, then hand out Arc
|
||||
@@ -364,6 +411,7 @@ impl shader::Primitive for Primitive {
|
||||
// Model tiles, upload the wire vertices once between them.
|
||||
// `.cloned()` releases the immutable cache borrow before the
|
||||
// miss branch takes a mutable one.
|
||||
if !arena_served {
|
||||
let cached = pipeline.wire_buffer_cache.get(&vp.wire_content_id).cloned();
|
||||
let built = match cached {
|
||||
Some(entry) => entry,
|
||||
@@ -386,6 +434,7 @@ impl shader::Primitive for Primitive {
|
||||
};
|
||||
inner.gpu_wires = built.0;
|
||||
inner.wire_handle_index = built.1;
|
||||
} // end !arena_served
|
||||
inner.cached_wire_id = vp.wire_content_id;
|
||||
}
|
||||
// Selection xray overlay — rebuilt when the selection changes or the
|
||||
@@ -1239,6 +1288,7 @@ impl Scene {
|
||||
geometry_epoch: self.geometry_epoch,
|
||||
camera_generation: self.camera_generation,
|
||||
wire_content_id,
|
||||
wire_patch: self.model_wire_patch_for(wire_content_id),
|
||||
selected_handles: Arc::new(self.selected.iter().copied().collect()),
|
||||
hover_handle: self.hover_highlight,
|
||||
selection_generation: self.selection_generation,
|
||||
|
||||
Reference in new issue
Block a user