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 <noreply@anthropic.com>
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);
|
||||
|
||||
Reference in new issue
Block a user