perf: optimize large drawing workflows
This commit is contained in:
parent
1c7e4bb850
commit
71e0b88839
17 changed files with 1202 additions and 428 deletions
|
|
@ -479,6 +479,128 @@ pub struct InteractionHandleIndex {
|
|||
handles: SpatialSet<u64>,
|
||||
}
|
||||
|
||||
struct WireIndexEntries {
|
||||
wire: Option<Entry3<u32>>,
|
||||
segments: Vec<Entry3<SegmentRef>>,
|
||||
snap_points: Vec<Entry3<SnapPointRef>>,
|
||||
key_vertices: Vec<Entry3<KeyVertexRef>>,
|
||||
key_segments: Vec<Entry3<KeySegmentRef>>,
|
||||
fill_triangles: Vec<Entry3<TriangleRef>>,
|
||||
pick_triangles: Vec<Entry3<TriangleRef>>,
|
||||
glyphs: Vec<Entry3<GlyphRef>>,
|
||||
unbounded: bool,
|
||||
max_line_half_width_px: f32,
|
||||
}
|
||||
|
||||
fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntries {
|
||||
let mut entries = WireIndexEntries {
|
||||
wire: finite_wire_aabb3(wire).map(|aabb| Entry3 {
|
||||
aabb,
|
||||
value: wire_idx,
|
||||
}),
|
||||
segments: Vec::with_capacity(wire.points.len().saturating_sub(1)),
|
||||
snap_points: Vec::with_capacity(wire.snap_pts.len()),
|
||||
key_vertices: Vec::with_capacity(wire.key_vertices.len()),
|
||||
key_segments: Vec::with_capacity(wire.key_vertices.len().saturating_sub(1)),
|
||||
fill_triangles: Vec::with_capacity(wire.fill_tris.len() / 3),
|
||||
pick_triangles: Vec::with_capacity(wire.pick_tris.len() / 3),
|
||||
glyphs: Vec::with_capacity(wire.text_verts.len() / 6),
|
||||
unbounded: false,
|
||||
max_line_half_width_px: if wire.line_weight_px.is_finite() {
|
||||
(wire.line_weight_px * 0.5).max(0.0)
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
};
|
||||
entries.unbounded = entries.wire.is_none();
|
||||
|
||||
for start in 0..wire.points.len().saturating_sub(1) {
|
||||
let Some(aabb) = points_aabb3([
|
||||
wire_point(wire, start),
|
||||
wire_point(wire, start + 1),
|
||||
]) else {
|
||||
continue;
|
||||
};
|
||||
entries.segments.push(Entry3 {
|
||||
aabb,
|
||||
value: SegmentRef {
|
||||
wire: wire_idx,
|
||||
start: start as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (index, (point, _)) in wire.snap_pts.iter().enumerate() {
|
||||
if point.is_finite() {
|
||||
entries.snap_points.push(Entry3 {
|
||||
aabb: [point.x, point.y, point.z, point.x, point.y, point.z],
|
||||
value: SnapPointRef {
|
||||
wire: wire_idx,
|
||||
index: index as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
for (index, &point) in wire.key_vertices.iter().enumerate() {
|
||||
if point.iter().all(|value| value.is_finite()) {
|
||||
entries.key_vertices.push(Entry3 {
|
||||
aabb: [point[0], point[1], point[2], point[0], point[1], point[2]],
|
||||
value: KeyVertexRef {
|
||||
wire: wire_idx,
|
||||
index: index as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
for start in 0..wire.key_vertices.len().saturating_sub(1) {
|
||||
let Some(aabb) =
|
||||
points_aabb3([wire.key_vertices[start], wire.key_vertices[start + 1]])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
entries.key_segments.push(Entry3 {
|
||||
aabb,
|
||||
value: KeySegmentRef {
|
||||
wire: wire_idx,
|
||||
start: start as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
append_triangle_entries(
|
||||
wire_idx,
|
||||
&wire.fill_tris,
|
||||
&wire.fill_tris_low,
|
||||
&mut entries.fill_triangles,
|
||||
);
|
||||
append_triangle_entries(
|
||||
wire_idx,
|
||||
&wire.pick_tris,
|
||||
&wire.pick_tris_low,
|
||||
&mut entries.pick_triangles,
|
||||
);
|
||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||
let Some(quad) = wire.text_verts.get(start..start + 6) else {
|
||||
break;
|
||||
};
|
||||
let Some(aabb) = points_aabb3(quad.iter().map(|vertex| {
|
||||
[
|
||||
vertex.pos[0] as f64 + vertex.pos_low[0] as f64,
|
||||
vertex.pos[1] as f64 + vertex.pos_low[1] as f64,
|
||||
vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
|
||||
]
|
||||
})) else {
|
||||
continue;
|
||||
};
|
||||
entries.glyphs.push(Entry3 {
|
||||
aabb,
|
||||
value: GlyphRef {
|
||||
wire: wire_idx,
|
||||
start: start as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
impl InteractionHandleIndex {
|
||||
pub fn build(entries: impl IntoIterator<Item = (u64, [f64; 6])>) -> Self {
|
||||
Self {
|
||||
|
|
@ -554,106 +676,76 @@ impl InteractionIndex {
|
|||
let mut unbounded_wires = Vec::new();
|
||||
let mut max_line_half_width_px = 0.0f32;
|
||||
|
||||
for (wire_idx, wire) in wires.iter().enumerate() {
|
||||
if wire.line_weight_px.is_finite() {
|
||||
max_line_half_width_px =
|
||||
max_line_half_width_px.max((wire.line_weight_px * 0.5).max(0.0));
|
||||
}
|
||||
let wire_idx = wire_idx as u32;
|
||||
if let Some(aabb) = finite_wire_aabb3(wire) {
|
||||
wire_entries.push(Entry3 {
|
||||
aabb,
|
||||
value: wire_idx,
|
||||
});
|
||||
} else {
|
||||
unbounded_wires.push(wire_idx);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let per_wire: Vec<WireIndexEntries> = {
|
||||
use crate::par::prelude::*;
|
||||
wires
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.map(|(index, wire)| collect_wire_index_entries(index as u32, wire))
|
||||
.collect()
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let per_wire: Vec<WireIndexEntries> = wires
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, wire)| collect_wire_index_entries(index as u32, wire))
|
||||
.collect();
|
||||
|
||||
for start in 0..wire.points.len().saturating_sub(1) {
|
||||
let a = wire_point(wire, start);
|
||||
let b = wire_point(wire, start + 1);
|
||||
let Some(aabb) = points_aabb3([a, b]) else {
|
||||
continue;
|
||||
};
|
||||
segment_entries.push(Entry3 {
|
||||
aabb,
|
||||
value: SegmentRef {
|
||||
wire: wire_idx,
|
||||
start: start as u32,
|
||||
},
|
||||
});
|
||||
// Every per-wire worker knows its exact output sizes. Reserve the flat
|
||||
// arrays once before draining them so a dense block drawing does not
|
||||
// repeatedly copy already-flattened entries while the Vecs grow.
|
||||
wire_entries.reserve(per_wire.iter().filter(|entries| entries.wire.is_some()).count());
|
||||
unbounded_wires.reserve(per_wire.iter().filter(|entries| entries.unbounded).count());
|
||||
segment_entries.reserve(per_wire.iter().map(|entries| entries.segments.len()).sum());
|
||||
snap_point_entries.reserve(
|
||||
per_wire
|
||||
.iter()
|
||||
.map(|entries| entries.snap_points.len())
|
||||
.sum(),
|
||||
);
|
||||
key_vertex_entries.reserve(
|
||||
per_wire
|
||||
.iter()
|
||||
.map(|entries| entries.key_vertices.len())
|
||||
.sum(),
|
||||
);
|
||||
key_segment_entries.reserve(
|
||||
per_wire
|
||||
.iter()
|
||||
.map(|entries| entries.key_segments.len())
|
||||
.sum(),
|
||||
);
|
||||
fill_triangle_entries.reserve(
|
||||
per_wire
|
||||
.iter()
|
||||
.map(|entries| entries.fill_triangles.len())
|
||||
.sum(),
|
||||
);
|
||||
pick_triangle_entries.reserve(
|
||||
per_wire
|
||||
.iter()
|
||||
.map(|entries| entries.pick_triangles.len())
|
||||
.sum(),
|
||||
);
|
||||
glyph_entries.reserve(per_wire.iter().map(|entries| entries.glyphs.len()).sum());
|
||||
|
||||
for (wire_idx, mut entries) in per_wire.into_iter().enumerate() {
|
||||
max_line_half_width_px =
|
||||
max_line_half_width_px.max(entries.max_line_half_width_px);
|
||||
if let Some(entry) = entries.wire {
|
||||
wire_entries.push(entry);
|
||||
}
|
||||
for (index, (point, _)) in wire.snap_pts.iter().enumerate() {
|
||||
if !point.is_finite() {
|
||||
continue;
|
||||
}
|
||||
snap_point_entries.push(Entry3 {
|
||||
aabb: [point.x, point.y, point.z, point.x, point.y, point.z],
|
||||
value: SnapPointRef {
|
||||
wire: wire_idx,
|
||||
index: index as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
for (index, &point) in wire.key_vertices.iter().enumerate() {
|
||||
if !point.iter().all(|value| value.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
key_vertex_entries.push(Entry3 {
|
||||
aabb: [point[0], point[1], point[2], point[0], point[1], point[2]],
|
||||
value: KeyVertexRef {
|
||||
wire: wire_idx,
|
||||
index: index as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
for start in 0..wire.key_vertices.len().saturating_sub(1) {
|
||||
let Some(aabb) =
|
||||
points_aabb3([wire.key_vertices[start], wire.key_vertices[start + 1]])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
key_segment_entries.push(Entry3 {
|
||||
aabb,
|
||||
value: KeySegmentRef {
|
||||
wire: wire_idx,
|
||||
start: start as u32,
|
||||
},
|
||||
});
|
||||
}
|
||||
append_triangle_entries(
|
||||
wire_idx,
|
||||
&wire.fill_tris,
|
||||
&wire.fill_tris_low,
|
||||
&mut fill_triangle_entries,
|
||||
);
|
||||
append_triangle_entries(
|
||||
wire_idx,
|
||||
&wire.pick_tris,
|
||||
&wire.pick_tris_low,
|
||||
&mut pick_triangle_entries,
|
||||
);
|
||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||
let Some(quad) = wire.text_verts.get(start..start + 6) else {
|
||||
break;
|
||||
};
|
||||
let Some(aabb) = points_aabb3(quad.iter().map(|vertex| {
|
||||
[
|
||||
vertex.pos[0] as f64 + vertex.pos_low[0] as f64,
|
||||
vertex.pos[1] as f64 + vertex.pos_low[1] as f64,
|
||||
vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
|
||||
]
|
||||
})) else {
|
||||
continue;
|
||||
};
|
||||
glyph_entries.push(Entry3 {
|
||||
aabb,
|
||||
value: GlyphRef {
|
||||
wire: wire_idx,
|
||||
start: start as u32,
|
||||
},
|
||||
});
|
||||
if entries.unbounded {
|
||||
unbounded_wires.push(wire_idx as u32);
|
||||
}
|
||||
segment_entries.append(&mut entries.segments);
|
||||
snap_point_entries.append(&mut entries.snap_points);
|
||||
key_vertex_entries.append(&mut entries.key_vertices);
|
||||
key_segment_entries.append(&mut entries.key_segments);
|
||||
fill_triangle_entries.append(&mut entries.fill_triangles);
|
||||
pick_triangle_entries.append(&mut entries.pick_triangles);
|
||||
glyph_entries.append(&mut entries.glyphs);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
|
|||
|
|
@ -149,7 +149,9 @@ pub struct Pipeline {
|
|||
/// ref-counted, so N paper viewports (or Model tiles) drawing one identical
|
||||
/// resident set hold one copy of the GPU vertex buffers between them —
|
||||
/// their camera uniforms + scissor stay per-slot, only the geometry is
|
||||
/// deduplicated. Never mutated in place, only reassigned on content change.
|
||||
/// deduplicated. Never mutated in place; an arena-backed slot may also
|
||||
/// replace this thin draw-range list on camera changes without touching the
|
||||
/// shared resident buffer.
|
||||
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
|
||||
|
|
@ -164,6 +166,10 @@ pub struct Pipeline {
|
|||
/// The Model content id both arenas currently mirror (`u64::MAX` = none).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) wire_arena_id: u64,
|
||||
/// Last content/camera/viewport tuple used to derive visible instance
|
||||
/// ranges from the resident arena.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) wire_cull_key: (u64, u64, u32, u32),
|
||||
/// This content viewport's non-rectangular clip boundary as a triangle-fan
|
||||
/// vertex buffer in the render target's normalized device coords (`None` =
|
||||
/// rectangular / unclipped, where the viewport's own render rectangle does
|
||||
|
|
@ -1450,6 +1456,8 @@ impl Pipeline {
|
|||
wire_arena_mesh: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_arena_id: u64::MAX,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_cull_key: (u64::MAX, u64::MAX, 0, 0),
|
||||
clip_boundary: None,
|
||||
gpu_selected_wires: vec![],
|
||||
gpu_preview_wires: vec![],
|
||||
|
|
@ -2572,7 +2580,10 @@ impl Pipeline {
|
|||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, edges.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..edges.instance_count);
|
||||
pass.draw(
|
||||
0..6,
|
||||
edges.first_instance..edges.first_instance + edges.instance_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2638,7 +2649,10 @@ impl Pipeline {
|
|||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..wire.instance_count);
|
||||
pass.draw(
|
||||
0..6,
|
||||
wire.first_instance..wire.first_instance + wire.instance_count,
|
||||
);
|
||||
}
|
||||
// Live overlay wires (command preview / interim / grip drag) always
|
||||
// on top: the xray pipeline (depth_compare=Always, no depth write)
|
||||
|
|
@ -2653,7 +2667,10 @@ impl Pipeline {
|
|||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, pw.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..pw.instance_count);
|
||||
pass.draw(
|
||||
0..6,
|
||||
pw.first_instance..pw.first_instance + pw.instance_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2791,7 +2808,10 @@ impl Pipeline {
|
|||
pass.set_bind_group(1, bg.as_ref(), &[]);
|
||||
}
|
||||
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..wire.instance_count);
|
||||
pass.draw(
|
||||
0..6,
|
||||
wire.first_instance..wire.first_instance + wire.instance_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ struct Slab {
|
|||
inst_len: u32,
|
||||
const_off: u32,
|
||||
const_len: u32,
|
||||
/// World-XY bounds for plan-view draw-range culling. Unbounded whenever a
|
||||
/// source wire does not carry a trustworthy entity AABB.
|
||||
aabb: [f32; 4],
|
||||
/// Entity-level draw depth used when this slab was emitted. Individual
|
||||
/// consts may carry block-local offsets around it; structural edits shift
|
||||
/// the whole slab by the base-depth delta instead of flattening those
|
||||
|
|
@ -182,6 +185,37 @@ fn handle_ranges(wires: &[&WireModel]) -> Option<Vec<(Handle, usize, usize)>> {
|
|||
Some(out)
|
||||
}
|
||||
|
||||
fn run_aabb(wires: &[&WireModel]) -> [f32; 4] {
|
||||
let mut out = [
|
||||
f32::INFINITY,
|
||||
f32::INFINITY,
|
||||
f32::NEG_INFINITY,
|
||||
f32::NEG_INFINITY,
|
||||
];
|
||||
for wire in wires {
|
||||
let [x0, y0, x1, y1] = wire.aabb;
|
||||
if !x0.is_finite()
|
||||
|| !y0.is_finite()
|
||||
|| !x1.is_finite()
|
||||
|| !y1.is_finite()
|
||||
|| x0 > x1
|
||||
|| y0 > y1
|
||||
{
|
||||
return WireModel::UNBOUNDED_AABB;
|
||||
}
|
||||
let pad = (wire.world_width * 0.5).max(0.0);
|
||||
out[0] = out[0].min(x0 - pad);
|
||||
out[1] = out[1].min(y0 - pad);
|
||||
out[2] = out[2].max(x1 + pad);
|
||||
out[3] = out[3].max(y1 + pad);
|
||||
}
|
||||
if out[0].is_finite() {
|
||||
out
|
||||
} else {
|
||||
WireModel::UNBOUNDED_AABB
|
||||
}
|
||||
}
|
||||
|
||||
fn make_const_bg(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
|
|
@ -197,22 +231,50 @@ fn make_const_bg(
|
|||
}))
|
||||
}
|
||||
|
||||
fn alloc_inst(device: &wgpu::Device, cap: u64) -> wgpu::Buffer {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
fn alloc_inst_initialized(
|
||||
device: &wgpu::Device,
|
||||
cap: u64,
|
||||
data: &[WireInstance],
|
||||
) -> wgpu::Buffer {
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("wire_arena.ibuf"),
|
||||
size: cap * std::mem::size_of::<WireInstance>() as u64,
|
||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
mapped_at_creation: true,
|
||||
});
|
||||
if !data.is_empty() {
|
||||
let bytes = bytemuck::cast_slice(data);
|
||||
let mut mapped = buffer
|
||||
.slice(..bytes.len() as u64)
|
||||
.get_mapped_range_mut();
|
||||
mapped.copy_from_slice(bytes);
|
||||
drop(mapped);
|
||||
}
|
||||
buffer.unmap();
|
||||
buffer
|
||||
}
|
||||
|
||||
fn alloc_const(device: &wgpu::Device, cap: u64) -> wgpu::Buffer {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
fn alloc_const_initialized(
|
||||
device: &wgpu::Device,
|
||||
cap: u64,
|
||||
data: &[WireConst],
|
||||
) -> wgpu::Buffer {
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("wire_arena.cbuf"),
|
||||
size: cap * std::mem::size_of::<WireConst>() as u64,
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
mapped_at_creation: true,
|
||||
});
|
||||
if !data.is_empty() {
|
||||
let bytes = bytemuck::cast_slice(data);
|
||||
let mut mapped = buffer
|
||||
.slice(..bytes.len() as u64)
|
||||
.get_mapped_range_mut();
|
||||
mapped.copy_from_slice(bytes);
|
||||
drop(mapped);
|
||||
}
|
||||
buffer.unmap();
|
||||
buffer
|
||||
}
|
||||
|
||||
fn blank_const() -> WireConst {
|
||||
|
|
@ -230,8 +292,7 @@ fn blank_instance() -> WireInstance {
|
|||
distance_a: 0.0,
|
||||
distance_b: 0.0,
|
||||
wire_id: 0,
|
||||
world_hw_a: 0.0,
|
||||
world_hw_b: 0.0,
|
||||
taper_ratio: [0; 2],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,44 +302,128 @@ impl WireArena {
|
|||
/// path).
|
||||
pub fn build(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
_queue: &wgpu::Queue,
|
||||
wires: &[&WireModel],
|
||||
depth_map: &FxHashMap<u64, [f32; 2]>,
|
||||
const_bgl: &wgpu::BindGroupLayout,
|
||||
mesh_edge: bool,
|
||||
) -> Option<Self> {
|
||||
let ranges = handle_ranges(wires)?;
|
||||
let perf = std::env::var_os("OCS_PERF").is_some();
|
||||
let total_started = std::time::Instant::now();
|
||||
|
||||
// Reject an oversized batch before parallel emission allocates hundreds
|
||||
// of megabytes. `points.len() - 1` is an upper bound because NaN-break
|
||||
// segments are skipped by emit_wire_native.
|
||||
let max_instances: usize = wires
|
||||
.iter()
|
||||
.map(|w| w.points.len().saturating_sub(1))
|
||||
.sum();
|
||||
if max_instances as u64 > MAX_INSTANCES || wires.len() as u64 + 1 > MAX_CONSTS {
|
||||
return None;
|
||||
}
|
||||
|
||||
struct BuildPlan {
|
||||
handle: Handle,
|
||||
start: usize,
|
||||
end: usize,
|
||||
const_off: u32,
|
||||
base_depth: f32,
|
||||
}
|
||||
struct PackedSlab {
|
||||
handle: Handle,
|
||||
const_off: u32,
|
||||
base_depth: f32,
|
||||
aabb: [f32; 4],
|
||||
instances: Vec<WireInstance>,
|
||||
consts: Vec<WireConst>,
|
||||
}
|
||||
|
||||
// Assign global const slots serially so parallel workers can emit final
|
||||
// wire_id values directly. Indexed parallel collect preserves handle and
|
||||
// wire submission order.
|
||||
let mut next_const = 1u32;
|
||||
let plans: Vec<BuildPlan> = ranges
|
||||
.into_iter()
|
||||
.map(|(handle, start, end)| {
|
||||
let const_off = next_const;
|
||||
next_const += (end - start) as u32;
|
||||
let base_depth = if mesh_edge {
|
||||
0.0
|
||||
} else {
|
||||
depth_map.get(&handle.value()).map_or(0.0, |d| d[0])
|
||||
};
|
||||
BuildPlan {
|
||||
handle,
|
||||
start,
|
||||
end,
|
||||
const_off,
|
||||
base_depth,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let pack_started = std::time::Instant::now();
|
||||
use crate::par::prelude::*;
|
||||
let packed: Vec<PackedSlab> = plans
|
||||
.par_iter()
|
||||
.map(|plan| {
|
||||
let run = &wires[plan.start..plan.end];
|
||||
let capacity: usize = run
|
||||
.iter()
|
||||
.map(|w| w.points.len().saturating_sub(1))
|
||||
.sum();
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(capacity);
|
||||
let mut consts: Vec<WireConst> = Vec::with_capacity(run.len());
|
||||
for (local, &w) in run.iter().enumerate() {
|
||||
let wire_id = plan.const_off + local as u32;
|
||||
// 3D mesh outline edges are occluded by true depth and must NOT
|
||||
// take the draw-order z-bias (or hidden back edges peek through
|
||||
// the shaded fill) — matching WireGpu::from_run.
|
||||
let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
|
||||
let (mut emitted, cst) = emit_wire_native(w, wire_id, w.color, dd);
|
||||
instances.append(&mut emitted);
|
||||
consts.push(cst);
|
||||
}
|
||||
PackedSlab {
|
||||
handle: plan.handle,
|
||||
const_off: plan.const_off,
|
||||
base_depth: plan.base_depth,
|
||||
aabb: run_aabb(run),
|
||||
instances,
|
||||
consts,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let pack_ms = pack_started.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
let inst_count: usize = packed.iter().map(|slab| slab.instances.len()).sum();
|
||||
let const_count: usize = 1 + packed.iter().map(|slab| slab.consts.len()).sum::<usize>();
|
||||
if inst_count as u64 > MAX_INSTANCES || const_count as u64 > MAX_CONSTS {
|
||||
return None;
|
||||
}
|
||||
|
||||
// const slot 0 = blank tombstone target.
|
||||
let mut instances: Vec<WireInstance> = Vec::new();
|
||||
let mut consts_cpu: Vec<WireConst> = vec![blank_const()];
|
||||
let mut slabs: FxHashMap<Handle, Slab> = FxHashMap::default();
|
||||
for (h, i, j) in ranges {
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(inst_count);
|
||||
let mut consts_cpu: Vec<WireConst> = Vec::with_capacity(const_count);
|
||||
consts_cpu.push(blank_const());
|
||||
let mut slabs: FxHashMap<Handle, Slab> =
|
||||
FxHashMap::with_capacity_and_hasher(packed.len(), Default::default());
|
||||
for mut packed_slab in packed {
|
||||
let inst_off = instances.len() as u32;
|
||||
let const_off = consts_cpu.len() as u32;
|
||||
let base_depth = if mesh_edge {
|
||||
0.0
|
||||
} else {
|
||||
depth_map.get(&h.value()).map_or(0.0, |d| d[0])
|
||||
};
|
||||
for &w in &wires[i..j] {
|
||||
let wire_id = consts_cpu.len() as u32;
|
||||
// 3D mesh outline edges are occluded by true depth and must NOT
|
||||
// take the draw-order z-bias (or hidden back edges peek through
|
||||
// the shaded fill) — matching WireGpu::from_run.
|
||||
let dd = if mesh_edge { 0.0 } else { wire_draw_depth(w, depth_map) };
|
||||
let (mut insts, cst) = emit_wire_native(w, wire_id, w.color, dd);
|
||||
instances.append(&mut insts);
|
||||
consts_cpu.push(cst);
|
||||
}
|
||||
let inst_len = packed_slab.instances.len() as u32;
|
||||
let const_len = packed_slab.consts.len() as u32;
|
||||
instances.append(&mut packed_slab.instances);
|
||||
consts_cpu.append(&mut packed_slab.consts);
|
||||
slabs.insert(
|
||||
h,
|
||||
packed_slab.handle,
|
||||
Slab {
|
||||
inst_off,
|
||||
inst_len: instances.len() as u32 - inst_off,
|
||||
const_off,
|
||||
const_len: consts_cpu.len() as u32 - const_off,
|
||||
base_depth,
|
||||
inst_len,
|
||||
const_off: packed_slab.const_off,
|
||||
const_len,
|
||||
aabb: packed_slab.aabb,
|
||||
base_depth: packed_slab.base_depth,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -296,13 +441,24 @@ impl WireArena {
|
|||
let const_cap = ((const_tail as u64 * HEADROOM_NUM / HEADROOM_DEN)
|
||||
.max(MIN_CONST_CAP)
|
||||
.min(MAX_CONSTS)) as u32;
|
||||
let inst_buf = alloc_inst(device, inst_cap as u64);
|
||||
let const_buf = alloc_const(device, const_cap as u64);
|
||||
if inst_tail > 0 {
|
||||
queue.write_buffer(&inst_buf, 0, bytemuck::cast_slice(&instances));
|
||||
}
|
||||
queue.write_buffer(&const_buf, 0, bytemuck::cast_slice(&consts_cpu));
|
||||
let upload_started = std::time::Instant::now();
|
||||
let inst_buf = alloc_inst_initialized(device, inst_cap as u64, &instances);
|
||||
let const_buf = alloc_const_initialized(device, const_cap as u64, &consts_cpu);
|
||||
let upload_ms = upload_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let const_bind_group = make_const_bg(device, const_bgl, &const_buf);
|
||||
if perf {
|
||||
eprintln!(
|
||||
"[perf] arena-build-detail total={:.1}ms pack={:.1} mapped-upload={:.1} handles={} wires={} instances={} instance-bytes={} consts={}",
|
||||
total_started.elapsed().as_secs_f64() * 1000.0,
|
||||
pack_ms,
|
||||
upload_ms,
|
||||
slabs.len(),
|
||||
wires.len(),
|
||||
inst_tail,
|
||||
inst_tail as usize * std::mem::size_of::<WireInstance>(),
|
||||
const_tail,
|
||||
);
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
inst_buf,
|
||||
|
|
@ -381,6 +537,7 @@ impl WireArena {
|
|||
} else {
|
||||
depth_map.get(&h.value()).map_or(0.0, |d| d[0])
|
||||
};
|
||||
let aabb = run_aabb(run);
|
||||
|
||||
if !self.slabs.contains_key(&h)
|
||||
&& self
|
||||
|
|
@ -419,7 +576,9 @@ impl WireArena {
|
|||
const_off as u64 * csz,
|
||||
bytemuck::cast_slice(&csts),
|
||||
);
|
||||
self.slabs.get_mut(&h).unwrap().base_depth = base_depth;
|
||||
let slab = self.slabs.get_mut(&h).unwrap();
|
||||
slab.base_depth = base_depth;
|
||||
slab.aabb = aabb;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -465,6 +624,7 @@ impl WireArena {
|
|||
inst_len,
|
||||
const_off,
|
||||
const_len,
|
||||
aabb,
|
||||
base_depth,
|
||||
},
|
||||
);
|
||||
|
|
@ -509,9 +669,101 @@ impl WireArena {
|
|||
}
|
||||
vec![WireGpu {
|
||||
instance_buffer: self.inst_buf.clone(),
|
||||
first_instance: 0,
|
||||
instance_count: self.inst_tail,
|
||||
is_3d_mesh_edge: self.mesh_edge,
|
||||
const_bind_group: Some(self.const_bind_group.clone()),
|
||||
}]
|
||||
}
|
||||
|
||||
/// Draw only plan-view entity ranges that can reach the viewport. The
|
||||
/// instance buffer stays fully resident, so pan/zoom changes only this tiny
|
||||
/// list of offsets — no repack and no GPU upload. Non-plan views use the
|
||||
/// conservative full draw because a 2-D entity AABB does not contain Z.
|
||||
pub fn wire_gpus_visible(
|
||||
&self,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
clip_w: u32,
|
||||
clip_h: u32,
|
||||
) -> Vec<WireGpu> {
|
||||
if self.inst_tail == 0 {
|
||||
return vec![];
|
||||
}
|
||||
let projected_x = view_rot.transform_vector3(glam::Vec3::X);
|
||||
let projected_y = view_rot.transform_vector3(glam::Vec3::Y);
|
||||
let projected_z = view_rot.transform_vector3(glam::Vec3::Z);
|
||||
let xy_scale = projected_x
|
||||
.truncate()
|
||||
.length()
|
||||
.max(projected_y.truncate().length())
|
||||
.max(f32::MIN_POSITIVE);
|
||||
if projected_z.truncate().length() > xy_scale * 1e-5 {
|
||||
return self.wire_gpus();
|
||||
}
|
||||
|
||||
let mut ranges: Vec<(u32, u32)> = self
|
||||
.slabs
|
||||
.values()
|
||||
.filter(|slab| {
|
||||
slab.inst_len > 0
|
||||
&& !super::aabb_offscreen(slab.aabb, view_rot, eye, clip_w, clip_h)
|
||||
})
|
||||
.map(|slab| (slab.inst_off, slab.inst_off + slab.inst_len))
|
||||
.collect();
|
||||
ranges.sort_unstable_by_key(|range| range.0);
|
||||
let mut merged: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
|
||||
for (start, end) in ranges {
|
||||
if let Some((_, previous_end)) = merged.last_mut() {
|
||||
if *previous_end == start {
|
||||
*previous_end = end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
merged.push((start, end));
|
||||
}
|
||||
let mut ranges = merged;
|
||||
if ranges.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// Cap CPU draw-call overhead on pathologically interleaved draw order.
|
||||
// Merging a few separated visible spans draws their offscreen gap too,
|
||||
// but retains order and still avoids the rest of a multi-million
|
||||
// instance drawing.
|
||||
const MAX_RANGES: usize = 64;
|
||||
if ranges.len() > MAX_RANGES {
|
||||
let group = (ranges.len() + MAX_RANGES - 1) / MAX_RANGES;
|
||||
ranges = ranges
|
||||
.chunks(group)
|
||||
.map(|chunk| (chunk[0].0, chunk[chunk.len() - 1].1))
|
||||
.collect();
|
||||
}
|
||||
|
||||
if std::env::var_os("OCS_PERF").is_some() {
|
||||
let submitted: u64 = ranges
|
||||
.iter()
|
||||
.map(|(start, end)| (end - start) as u64)
|
||||
.sum();
|
||||
if submitted < self.inst_tail as u64 {
|
||||
eprintln!(
|
||||
"[perf] wire-cull submitted={} resident={} ranges={}",
|
||||
submitted,
|
||||
self.inst_tail,
|
||||
ranges.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ranges
|
||||
.into_iter()
|
||||
.map(|(start, end)| WireGpu {
|
||||
instance_buffer: self.inst_buf.clone(),
|
||||
first_instance: start,
|
||||
instance_count: end - start,
|
||||
is_3d_mesh_edge: self.mesh_edge,
|
||||
const_bind_group: Some(self.const_bind_group.clone()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,9 +65,10 @@ fn instance_buffer_mapped<T: bytemuck::Pod>(
|
|||
// even though it's constant along the wire. On native we hoist those into a
|
||||
// per-wire `WireConst` storage buffer indexed by `wire_id`, so the instance
|
||||
// keeps only the per-segment data (endpoints + arc-length distances). Cuts the
|
||||
// instance from 104 B to 60 B (~42 %) and removes the redundant per-segment
|
||||
// re-fetch of the shared constants. WebGL2 has no vertex-stage storage buffers,
|
||||
// so the wasm build below keeps the original self-contained fat instance.
|
||||
// instance from 104 B to one 64-byte cache line and removes the redundant
|
||||
// per-segment re-fetch of the shared constants. WebGL2 has no vertex-stage
|
||||
// storage buffers, so the wasm build below keeps the original self-contained
|
||||
// fat instance.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
|
|
@ -80,11 +81,11 @@ pub struct WireInstance {
|
|||
pub distance_b: f32,
|
||||
/// Index into the per-wire `WireConst` storage buffer (group 1).
|
||||
pub wire_id: u32,
|
||||
/// World-space half-width at each endpoint for a TAPERED band. `0.0` =
|
||||
/// use the per-wire constant (`WireConst.world_half_width`). The shader
|
||||
/// interpolates between the two so the band tapers across the segment.
|
||||
pub world_hw_a: f32,
|
||||
pub world_hw_b: f32,
|
||||
/// Endpoint width / the per-wire maximum width, normalized by the vertex
|
||||
/// fetch unit. `[0, 0]` means use the constant width. Ratios retain the
|
||||
/// full f32 world-width scale in `WireConst` while making every instance
|
||||
/// exactly one 64-byte cache line.
|
||||
pub taper_ratio: [u16; 2],
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
@ -99,8 +100,7 @@ impl WireInstance {
|
|||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_a) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, distance_b) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, wire_id) as u64, shader_location: 6, format: wgpu::VertexFormat::Uint32 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_a) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, world_hw_b) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32 },
|
||||
wgpu::VertexAttribute { offset: std::mem::offset_of!(WireInstance, taper_ratio) as u64, shader_location: 7, format: wgpu::VertexFormat::Unorm16x2 },
|
||||
];
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<WireInstance>() as u64,
|
||||
|
|
@ -284,6 +284,8 @@ impl WirePipelineMode {
|
|||
|
||||
pub struct WireGpu {
|
||||
pub instance_buffer: wgpu::Buffer,
|
||||
/// First instance in a shared arena buffer. Standalone buffers start at 0.
|
||||
pub first_instance: u32,
|
||||
pub instance_count: u32,
|
||||
/// `true` when the source `WireModel` also carries `fill_tris`
|
||||
/// (i.e. it is a 3D mesh face — PolyfaceMesh / PolygonMesh — whose
|
||||
|
|
@ -520,9 +522,20 @@ pub(crate) fn emit_wire_native(
|
|||
return (Vec::new(), cst);
|
||||
}
|
||||
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
||||
// Per-point half-width for a tapered band (empty ⇒ 0 ⇒ shader uses the
|
||||
// per-wire constant width).
|
||||
let tw = |i: usize| -> f32 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 };
|
||||
// Store an endpoint/max-width ratio. The shared f32 maximum keeps drawing
|
||||
// units and range out of the packed field; UNORM16 contributes only a
|
||||
// relative error below 1/65535. Preserve zero as the existing constant
|
||||
// width fallback sentinel.
|
||||
let taper_ratio = |i: usize| -> u16 {
|
||||
let width = wire.taper_widths.get(i).copied().unwrap_or(0.0);
|
||||
if width <= 0.0 || wire.world_width <= 0.0 {
|
||||
0
|
||||
} else {
|
||||
((width / wire.world_width).clamp(0.0, 1.0) * u16::MAX as f32)
|
||||
.round()
|
||||
.max(1.0) as u16
|
||||
}
|
||||
};
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
|
||||
for i in 0..seg_count {
|
||||
let a = wire.points[i];
|
||||
|
|
@ -538,8 +551,7 @@ pub(crate) fn emit_wire_native(
|
|||
distance_a: dists[i],
|
||||
distance_b: dists[i + 1],
|
||||
wire_id,
|
||||
world_hw_a: tw(i),
|
||||
world_hw_b: tw(i + 1),
|
||||
taper_ratio: [taper_ratio(i), taper_ratio(i + 1)],
|
||||
});
|
||||
}
|
||||
(instances, cst)
|
||||
|
|
@ -652,6 +664,7 @@ impl WireGpu {
|
|||
let buf = instance_buffer_mapped(device, "wire.run.ibuf", chunk);
|
||||
Self {
|
||||
instance_buffer: buf,
|
||||
first_instance: 0,
|
||||
instance_count: chunk.len() as u32,
|
||||
is_3d_mesh_edge: mesh_edge,
|
||||
const_bind_group: Some(bg.clone()),
|
||||
|
|
@ -684,6 +697,7 @@ impl WireGpu {
|
|||
let buf = instance_buffer_mapped(device, "wire.run.compat.ibuf", chunk);
|
||||
Self {
|
||||
instance_buffer: buf,
|
||||
first_instance: 0,
|
||||
instance_count: chunk.len() as u32,
|
||||
is_3d_mesh_edge: mesh_edge,
|
||||
const_bind_group: None,
|
||||
|
|
@ -740,6 +754,7 @@ impl WireGpu {
|
|||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||
Self {
|
||||
instance_buffer,
|
||||
first_instance: 0,
|
||||
instance_count: chunk.len() as u32,
|
||||
is_3d_mesh_edge: false,
|
||||
const_bind_group: Some(bg.clone()),
|
||||
|
|
@ -766,6 +781,7 @@ impl WireGpu {
|
|||
let instance_buffer = instance_buffer_mapped(device, &label, chunk);
|
||||
Self {
|
||||
instance_buffer,
|
||||
first_instance: 0,
|
||||
instance_count: chunk.len() as u32,
|
||||
is_3d_mesh_edge: false,
|
||||
const_bind_group: None,
|
||||
|
|
|
|||
|
|
@ -271,6 +271,10 @@ impl shader::Primitive for Primitive {
|
|||
inner.cached_mesh_source = None;
|
||||
inner.cached_face3d_source = None;
|
||||
inner.cached_face3d_depth_source = None;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
inner.wire_cull_key = (u64::MAX, u64::MAX, 0, 0);
|
||||
}
|
||||
inner.render_sig = u64::MAX;
|
||||
}
|
||||
// The MSAA / depth / resolve textures are always sized to the
|
||||
|
|
@ -709,6 +713,41 @@ impl shader::Primitive for Primitive {
|
|||
inner.compute_hatch_lod(queue, view_rot, eye, clip_size.width, clip_size.height);
|
||||
inner.compute_wipeout_lod(view_rot, eye, clip_size.width, clip_size.height);
|
||||
inner.compute_mesh_lod(view_rot, eye, clip_size.width, clip_size.height);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let cull_key = (
|
||||
vp.wire_content_id,
|
||||
vp.camera_generation,
|
||||
clip_size.width,
|
||||
clip_size.height,
|
||||
);
|
||||
if inner.wire_arena_id == vp.wire_content_id
|
||||
&& inner.wire_cull_key != cull_key
|
||||
{
|
||||
let mut visible = inner
|
||||
.wire_arena
|
||||
.as_ref()
|
||||
.map(|arena| {
|
||||
arena.wire_gpus_visible(
|
||||
view_rot,
|
||||
eye,
|
||||
clip_size.width,
|
||||
clip_size.height,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if let Some(arena) = inner.wire_arena_mesh.as_ref() {
|
||||
visible.extend(arena.wire_gpus_visible(
|
||||
view_rot,
|
||||
eye,
|
||||
clip_size.width,
|
||||
clip_size.height,
|
||||
));
|
||||
}
|
||||
inner.gpu_wires = std::sync::Arc::new(visible);
|
||||
inner.wire_cull_key = cull_key;
|
||||
}
|
||||
}
|
||||
if vp.show_viewcube {
|
||||
inner.viewcube.upload(
|
||||
queue,
|
||||
|
|
|
|||
Loading…
Reference in a new issue