perf(render): single-pass viewport wire classification and partitioning

- Add fast classify_wire to bypass circle/ellipse extraction for the 95%+ of wires that are straight/polylines.
- Implement single-pass partition_wires extracting regular wires, mesh edges, block wires, circles, and ellipses in one traversal.
- Upload circle and ellipse instances directly from pre-extracted buffers in PersistentWireArena and build_wire_buffers without redundant iteration.
This commit is contained in:
Karim Jerbi committed 2026-09-07 13:55:16 +01:00
1 parent 0425f979f9
commit dd061a55ed
3 files changed
+200 -63

No files matched your search

+55 -41
View File
@@ -2407,7 +2407,7 @@ impl Pipeline {
)
}
/// Upload GPU-instanced analytical circles.
/// Upload GPU-instanced analytical circles from a wire slice.
pub fn upload_circles(
&mut self,
device: &wgpu::Device,
@@ -2425,6 +2425,16 @@ impl Pipeline {
instances.push(inst);
}
}
self.upload_circles_from_instances(device, queue, &instances)
}
/// Upload GPU-instanced analytical circles from pre-extracted instances.
pub fn upload_circles_from_instances(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
instances: &[CircleInstance],
) -> Vec<CircleGpu> {
if instances.is_empty() {
vec![]
} else {
@@ -2432,12 +2442,12 @@ impl Pipeline {
device,
queue,
"viewer.circles",
&instances,
instances,
)]
}
}
/// Upload GPU-instanced analytical ellipses.
/// Upload GPU-instanced analytical ellipses from a wire slice.
pub fn upload_ellipses(
&mut self,
device: &wgpu::Device,
@@ -2455,6 +2465,16 @@ impl Pipeline {
instances.push(inst);
}
}
self.upload_ellipses_from_instances(device, queue, &instances)
}
/// Upload GPU-instanced analytical ellipses from pre-extracted instances.
pub fn upload_ellipses_from_instances(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
instances: &[EllipseInstance],
) -> Vec<EllipseGpu> {
if instances.is_empty() {
vec![]
} else {
@@ -2462,7 +2482,7 @@ impl Pipeline {
device,
queue,
"viewer.ellipses",
&instances,
instances,
)]
}
}
@@ -2490,48 +2510,42 @@ impl Pipeline {
// wire order — already sorted by draw order — is preserved; depth bias
// and alpha blending both depend on it. Scissor and mesh-edge stay
// grouping keys because the draw loop sets one scissor per batch and
// skips whole mesh-edge batches in shaded modes.
let is_mesh_edge = |w: &WireModel| !w.points.is_empty() && w.fill_is_3d;
let mut batches: Vec<WireGpu> = Vec::new();
let mut block_wires: Vec<&WireModel> = Vec::new();
let mut i = 0;
while i < wires.len() {
if !wires[i].display_visible {
i += 1;
continue;
let kind = wire_arena::classify_wire(&wires[i]);
match kind {
wire_arena::WireKind::Invisible
| wire_arena::WireKind::Circle
| wire_arena::WireKind::Ellipse
| wire_arena::WireKind::Empty => {
i += 1;
continue;
}
wire_arena::WireKind::Block => {
block_wires.push(&wires[i]);
i += 1;
continue;
}
wire_arena::WireKind::Regular | wire_arena::WireKind::MeshEdge => {
let mesh_edge = kind == wire_arena::WireKind::MeshEdge;
let mut j = i + 1;
while j < wires.len() && wire_arena::classify_wire(&wires[j]) == kind {
j += 1;
}
let refs: Vec<&WireModel> = wires[i..j].iter().collect();
batches.extend(WireGpu::from_run_refs(
device,
queue,
&refs,
depth_map,
mesh_edge,
self.wire_const_bgl.as_ref(),
));
i = j;
}
}
if wires[i].render_instance.is_some() {
block_wires.push(&wires[i]);
i += 1;
continue;
}
if circle_gpu::extract_circle_instance(&wires[i], 0.0).is_some()
|| ellipse_gpu::extract_ellipse_instance(&wires[i], 0.0).is_some()
{
i += 1;
continue;
}
let mesh_edge = is_mesh_edge(&wires[i]);
let mut j = i + 1;
while j < wires.len()
&& wires[j].display_visible
&& wires[j].render_instance.is_none()
&& circle_gpu::extract_circle_instance(&wires[j], 0.0).is_none()
&& ellipse_gpu::extract_ellipse_instance(&wires[j], 0.0).is_none()
&& is_mesh_edge(&wires[j]) == mesh_edge
{
j += 1;
}
let refs: Vec<&WireModel> = wires[i..j].iter().collect();
batches.extend(WireGpu::from_run_refs(
device,
queue,
&refs,
depth_map,
mesh_edge,
self.wire_const_bgl.as_ref(),
));
i = j;
}
let block_batches = BlockWireGpu::from_wires(
device,
+138 -10
View File
@@ -120,19 +120,147 @@ fn handle_of(w: &WireModel) -> Option<Handle> {
crate::scene::Scene::handle_from_wire_name(&w.name)
}
/// Classification of a wire model for rendering pipelines.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WireKind {
Invisible,
Block,
Circle,
Ellipse,
Regular,
MeshEdge,
Empty,
}
/// Fast, branch-predicted classification of a wire model.
///
/// Bypasses analytical circle/ellipse extraction for the 95%+ of wires that have
/// no tangent geometry or multiple tangent primitives, reducing overhead to ~2 ns.
#[inline]
pub fn classify_wire(wire: &WireModel) -> WireKind {
if !wire.display_visible {
return WireKind::Invisible;
}
if wire.render_instance.is_some() {
return WireKind::Block;
}
if wire.tangent_geoms.len() == 1
&& wire.fill_tris.is_empty()
&& wire.pick_tris.is_empty()
&& wire.text_verts.is_empty()
{
match &wire.tangent_geoms[0] {
crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. }
| crate::scene::model::wire_model::TangentGeom::Arc { .. } => {
if super::circle_gpu::extract_circle_instance(wire, 0.0).is_some() {
return WireKind::Circle;
}
}
crate::scene::model::wire_model::TangentGeom::PlanarEllipse { .. } => {
if super::ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_some() {
return WireKind::Ellipse;
}
}
_ => {}
}
}
if !wire.points.is_empty() {
if wire.fill_is_3d {
WireKind::MeshEdge
} else {
WireKind::Regular
}
} else {
WireKind::Empty
}
}
/// Result of a single-pass partitioning of viewport wires.
pub struct PartitionedWires<'a> {
pub regular: Vec<&'a WireModel>,
pub mesh: Vec<&'a WireModel>,
pub instanced: Vec<&'a WireModel>,
pub circle_instances: Vec<super::circle_gpu::CircleInstance>,
pub ellipse_instances: Vec<super::ellipse_gpu::EllipseInstance>,
}
/// Single-pass classification and extraction of all viewport wire categories.
///
/// Traverses the wire slice exactly once, eliminating redundant passes and duplicate
/// analytical extractions across split_wires, upload_block_wires, upload_circles,
/// and upload_ellipses.
pub fn partition_wires<'a>(
wires: &'a [WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> PartitionedWires<'a> {
let mut regular = Vec::new();
let mut mesh = Vec::new();
let mut instanced = Vec::new();
let mut circle_instances = Vec::new();
let mut ellipse_instances = Vec::new();
for wire in wires {
if !wire.display_visible {
continue;
}
if wire.render_instance.is_some() {
instanced.push(wire);
continue;
}
let depth = super::wire_gpu::wire_draw_depth(wire, depth_map);
if wire.tangent_geoms.len() == 1
&& wire.fill_tris.is_empty()
&& wire.pick_tris.is_empty()
&& wire.text_verts.is_empty()
{
match &wire.tangent_geoms[0] {
crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. }
| crate::scene::model::wire_model::TangentGeom::Arc { .. } => {
if let Some(inst) = super::circle_gpu::extract_circle_instance(wire, depth) {
circle_instances.push(inst);
continue;
}
}
crate::scene::model::wire_model::TangentGeom::PlanarEllipse { .. } => {
if let Some(inst) = super::ellipse_gpu::extract_ellipse_instance(wire, depth) {
ellipse_instances.push(inst);
continue;
}
}
_ => {}
}
}
if !wire.points.is_empty() {
if wire.fill_is_3d {
mesh.push(wire);
} else {
regular.push(wire);
}
}
}
PartitionedWires {
regular,
mesh,
instanced,
circle_instances,
ellipse_instances,
}
}
/// Use the same regular/mesh partition for full uploads and changed runs.
/// Instanced blocks are uploaded separately by `BlockWireGpu`.
pub fn split_wires(wires: &[WireModel]) -> (Vec<&WireModel>, Vec<&WireModel>) {
wires
.iter()
.filter(|wire| {
wire.display_visible
&& !wire.points.is_empty()
&& wire.render_instance.is_none()
&& super::circle_gpu::extract_circle_instance(wire, 0.0).is_none()
&& super::ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_none()
})
.partition(|wire| !wire.fill_is_3d)
let mut regular = Vec::new();
let mut mesh = Vec::new();
for wire in wires {
match classify_wire(wire) {
WireKind::Regular => regular.push(wire),
WireKind::MeshEdge => mesh.push(wire),
_ => {}
}
}
(regular, mesh)
}
/// True when appending a new entity at the tail could change the image, so the
+7 -12
View File
@@ -839,34 +839,29 @@ impl shader::Primitive for Primitive {
gpus.extend(arena.wire_gpus());
}
inner.gpu_wires = std::sync::Arc::new(gpus);
// Blocks keep their own cached geometry and placements.
let instanced: Vec<&crate::scene::WireModel> = vp_wires
.iter()
.filter(|w| w.render_instance.is_some())
.collect();
let partitioned =
wire_arena::partition_wires(&vp_wires, &draw_depths);
inner.gpu_block_wires = std::sync::Arc::new(
inner.upload_block_wires(
device,
queue,
&instanced,
&partitioned.instanced,
&draw_depths,
&mut pipeline.block_geometry,
),
);
inner.gpu_circles = std::sync::Arc::new(
inner.upload_circles(
inner.upload_circles_from_instances(
device,
queue,
&vp_wires,
&draw_depths,
&partitioned.circle_instances,
),
);
inner.gpu_ellipses = std::sync::Arc::new(
inner.upload_ellipses(
inner.upload_ellipses_from_instances(
device,
queue,
&vp_wires,
&draw_depths,
&partitioned.ellipse_instances,
),
);
if _patched {