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:
Hakan SevenandClaude Opus 4.8 committed 2026-07-15 06:02:25 +03:00
1 parent 0907d2912d
commit 38c8583d27
5 files changed
+398 -6

No files matched your search

+18 -1
View File
@@ -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![],