perf: optimize large drawing workflows

This commit is contained in:
Hakan Seven 2026-07-25 02:58:46 +03:00
commit 71e0b88839
17 changed files with 1202 additions and 428 deletions

View file

@ -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"))]

View file

@ -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,
);
}
}
}

View file

@ -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()
}
}

View file

@ -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,

View file

@ -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,