forked from 1b-agent/opencadstudio-upstream
feat(render): GPU analytical rendering for circles, arcs, and ellipses
- Render planar circles, circular arcs, ellipses, and elliptical arcs analytically via GPU-instanced screen-space quads and WGSL shaders. - Implement exact arc sweep clamping with round end caps matching the CAD wire shader. - Implement robust Newton-Raphson closest-point solver for ellipses with isotropic screen-space derivative anti-aliasing. - Integrate into wire pipeline and PersistentWireArena upload paths. - Remove unused mesh_volume_centroid function in doc_api.
This commit is contained in:
1 parent
143703d809
commit
0425f979f9
17 files changed
+1860
-40
No files matched your search
Vendored
+27
-1
@@ -1268,7 +1268,8 @@ fn translated_prototype_wire(
|
||||
}
|
||||
}
|
||||
TangentGeom::PlanarCircle { center, .. }
|
||||
| TangentGeom::Arc { center, .. } => {
|
||||
| TangentGeom::Arc { center, .. }
|
||||
| TangentGeom::PlanarEllipse { center, .. } => {
|
||||
for axis in 0..3 {
|
||||
center[axis] += delta[axis];
|
||||
}
|
||||
@@ -2727,6 +2728,31 @@ fn transform_tangent(
|
||||
radius: radius * ((sx + sy) * 0.5),
|
||||
})
|
||||
}
|
||||
TangentGeom::PlanarEllipse {
|
||||
center,
|
||||
major_axis,
|
||||
normal,
|
||||
minor_axis_ratio,
|
||||
start_param,
|
||||
end_param,
|
||||
} => {
|
||||
let c = t.apply(Vector3::new(center[0], center[1], center[2]));
|
||||
let m = t.apply_rotation(Vector3::new(major_axis[0], major_axis[1], major_axis[2]));
|
||||
let n = t.apply_rotation(Vector3::new(normal[0], normal[1], normal[2]));
|
||||
let n_len = n.length();
|
||||
if n_len <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let n = n / n_len;
|
||||
Some(TangentGeom::PlanarEllipse {
|
||||
center: [c.x, c.y, c.z],
|
||||
major_axis: [m.x, m.y, m.z],
|
||||
normal: [n.x, n.y, n.z],
|
||||
minor_axis_ratio: *minor_axis_ratio,
|
||||
start_param: *start_param,
|
||||
end_param: *end_param,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -707,9 +707,11 @@ pub fn tessellate(
|
||||
}
|
||||
|
||||
// ── Try the kernel path first ───────────────────────────────────────────
|
||||
// Zoom-adaptive circles scale their segment count from the current zoom level and radius.
|
||||
// Relative-PDSIZE points size their glyph from the current zoom so they
|
||||
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
||||
let te = crate::entities::point::relative_render(entity, document, world_per_pixel)
|
||||
let te = crate::entities::circle::relative_render(entity, document, world_per_pixel)
|
||||
.or_else(|| crate::entities::point::relative_render(entity, document, world_per_pixel))
|
||||
.or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel))
|
||||
.or_else(|| match entity {
|
||||
EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale(
|
||||
|
||||
+77
-9
@@ -2232,6 +2232,7 @@ impl Scene {
|
||||
self.annotation_all_visible(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
|
||||
self.last_model_wire_gen.set(gen);
|
||||
@@ -5161,9 +5162,9 @@ impl Scene {
|
||||
pub(super) fn model_tile_wires_arc(
|
||||
&self,
|
||||
_tile_idx: usize,
|
||||
_cam: &Camera,
|
||||
cam: &Camera,
|
||||
_cam_aspect: f32,
|
||||
_tile_pixel_height: f32,
|
||||
tile_pixel_height: f32,
|
||||
) -> Arc<Vec<WireModel>> {
|
||||
// In a BEDIT block editor the resident set is the edited block's own
|
||||
// (block-local) entities; otherwise model space. (#261)
|
||||
@@ -5174,7 +5175,12 @@ impl Scene {
|
||||
&self.document,
|
||||
&self.document.header.current_annotation_scale,
|
||||
);
|
||||
self.resident_wires_for(block, None, scale, None, None)
|
||||
let wpp = if tile_pixel_height > 0.0 {
|
||||
Some((2.0 * cam.ortho_size()) / tile_pixel_height)
|
||||
} else {
|
||||
self.world_per_pixel()
|
||||
};
|
||||
self.resident_wires_for(block, None, scale, None, None, wpp)
|
||||
}
|
||||
|
||||
/// Unified static-hold wire builder — the ONE tessellation path every
|
||||
@@ -5223,6 +5229,7 @@ impl Scene {
|
||||
annotation_scale_handle: Option<Handle>,
|
||||
frozen_layers: Option<&HashSet<Handle>>,
|
||||
style_viewport: Option<Handle>,
|
||||
wpp: Option<f32>,
|
||||
) -> Arc<Vec<WireModel>> {
|
||||
// Normalize an inert anno override away so distinct viewport scales
|
||||
// share one resident set when annotation can't change the wires.
|
||||
@@ -5239,6 +5246,7 @@ impl Scene {
|
||||
self.paper_bg_color
|
||||
};
|
||||
let all_visible = self.annotation_all_visible();
|
||||
let quantized_wpp = Self::quantize_wpp(wpp);
|
||||
let key = self.resident_wire_key(
|
||||
block,
|
||||
bg,
|
||||
@@ -5247,6 +5255,7 @@ impl Scene {
|
||||
all_visible,
|
||||
frozen_layers,
|
||||
style_viewport,
|
||||
quantized_wpp,
|
||||
);
|
||||
{
|
||||
let sets = self.resident_wire_sets.borrow();
|
||||
@@ -5271,17 +5280,18 @@ impl Scene {
|
||||
all_visible,
|
||||
frozen_layers,
|
||||
style_viewport,
|
||||
quantized_wpp,
|
||||
)
|
||||
{
|
||||
return arc;
|
||||
}
|
||||
// Build once: full tessellation, no cull (region = None), no zoom LOD
|
||||
// (wpp = None) — for every space, exactly like the Model static-hold.
|
||||
// Build once: full tessellation, no cull (region = None), zoom LOD
|
||||
// bounded by quantized_wpp for every space.
|
||||
let t_tess = iced::time::Instant::now();
|
||||
let mut wires = self.wires_for_block_culled(
|
||||
block,
|
||||
None,
|
||||
None,
|
||||
quantized_wpp,
|
||||
frozen_layers,
|
||||
anno_scale_override,
|
||||
annotation_scale_handle,
|
||||
@@ -5327,6 +5337,9 @@ impl Scene {
|
||||
// spaces or re-scaling a viewport can't accumulate dead full sets.
|
||||
let cur_epoch = self.geometry_epoch;
|
||||
sets.retain(|_, set| set.epoch == cur_epoch);
|
||||
if sets.len() > 16 {
|
||||
sets.clear();
|
||||
}
|
||||
sets.insert(
|
||||
key,
|
||||
ResidentWireSet {
|
||||
@@ -5348,6 +5361,7 @@ impl Scene {
|
||||
all_visible: bool,
|
||||
frozen_layers: Option<&HashSet<Handle>>,
|
||||
style_viewport: Option<Handle>,
|
||||
quantized_wpp: Option<f32>,
|
||||
) -> u64 {
|
||||
let mut key: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut mix =
|
||||
@@ -5362,6 +5376,9 @@ impl Scene {
|
||||
mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0));
|
||||
mix(all_visible as u64);
|
||||
mix(self.viewport_style_key(style_viewport));
|
||||
mix(quantized_wpp
|
||||
.map(|w| w.to_bits() as u64)
|
||||
.unwrap_or(u64::MAX));
|
||||
match frozen_layers {
|
||||
Some(frozen) => {
|
||||
let mut signature = 0u64;
|
||||
@@ -5376,6 +5393,18 @@ impl Scene {
|
||||
key
|
||||
}
|
||||
|
||||
/// Quantize world units per pixel (`wpp`) into half-octave bands (~1.414x zoom ratio).
|
||||
///
|
||||
/// This provides discrete, stable zoom bands so that panning (where wpp is constant)
|
||||
/// and small subpixel viewport drifts or mouse-wheel motions within a band do not
|
||||
/// cause re-tessellation or GPU buffer re-uploads, while zooming past a band threshold
|
||||
/// smoothly scales circle tessellation detail.
|
||||
pub fn quantize_wpp(wpp: Option<f32>) -> Option<f32> {
|
||||
let w = wpp.filter(|&w| w.is_finite() && w > 0.0)?;
|
||||
let step = (w.log2() * 2.0).floor() * 0.5;
|
||||
Some(2.0f32.powf(step))
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -5462,6 +5491,7 @@ impl Scene {
|
||||
all_visible: bool,
|
||||
frozen_layers: Option<&HashSet<Handle>>,
|
||||
style_viewport: Option<Handle>,
|
||||
quantized_wpp: Option<f32>,
|
||||
) -> Option<Arc<Vec<WireModel>>> {
|
||||
if self.viewport_style_key(style_viewport) != 0 {
|
||||
return None;
|
||||
@@ -5545,7 +5575,7 @@ impl Scene {
|
||||
e,
|
||||
Some(&blk),
|
||||
None,
|
||||
None,
|
||||
quantized_wpp,
|
||||
anno_scale_override.is_some(),
|
||||
);
|
||||
memo_updates.push((*h, Arc::new(raw.clone())));
|
||||
@@ -5827,7 +5857,7 @@ impl Scene {
|
||||
}
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
let scale = self.paper_annotation_scale_handle();
|
||||
let base = self.resident_wires_for(layout_block, None, scale, None, None);
|
||||
let base = self.resident_wires_for(layout_block, None, scale, None, None, None);
|
||||
let mut wires = (*base).clone();
|
||||
// The overall "sheet" viewport now IS the paper view itself, so its own
|
||||
// border rectangle must not be drawn as an entity on the sheet.
|
||||
@@ -9061,7 +9091,7 @@ impl Scene {
|
||||
static EN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*EN.get_or_init(|| std::env::var("OCS_NO_RESIDENT_MEMO").is_err())
|
||||
}
|
||||
let resident = base_ok && view_aabb.is_none() && wpp.is_none() && resident_memo_enabled();
|
||||
let resident = base_ok && view_aabb.is_none() && resident_memo_enabled();
|
||||
let memo_active = base_ok && (view_aabb.is_some() || resident);
|
||||
// Reported in one line below. `tess_ms` / `memo_misses` are assigned by
|
||||
// both branches; the rest exist only on the memo path and stay zero.
|
||||
@@ -10200,6 +10230,7 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}",
|
||||
scale,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let wire_points = wires.iter().flat_map(|wire| wire.key_vertices.iter().copied());
|
||||
let mesh_points = self.meshes.iter().filter_map(|(&handle, set)| {
|
||||
@@ -11213,6 +11244,7 @@ mod layout_cache_tests {
|
||||
scale,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let generation = s.last_model_wire_gen.get();
|
||||
let bounds = s.model_space_extents().unwrap();
|
||||
@@ -11226,4 +11258,40 @@ mod layout_cache_tests {
|
||||
assert!(!source.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circle_wires_scale_with_zoom_level() {
|
||||
let mut s = Scene::new();
|
||||
let mut circle = acadrust::entities::Circle::default();
|
||||
circle.radius = 100.0;
|
||||
let handle = s.add_entity(EntityType::Circle(circle));
|
||||
|
||||
// Far camera: distance is large -> wpp is large
|
||||
let mut cam_far = Camera::default();
|
||||
cam_far.distance = 1000.0;
|
||||
|
||||
// Close camera: distance is small -> wpp is small
|
||||
let mut cam_close = Camera::default();
|
||||
cam_close.distance = 1.0;
|
||||
|
||||
let wires_far = s.model_tile_wires_arc(0, &cam_far, 1.0, 1000.0);
|
||||
let circle_wire_far = wires_far
|
||||
.iter()
|
||||
.find(|w| w.name == handle.value().to_string())
|
||||
.unwrap();
|
||||
let far_pts = circle_wire_far.points.len();
|
||||
|
||||
let wires_close = s.model_tile_wires_arc(0, &cam_close, 1.0, 1000.0);
|
||||
let circle_wire_close = wires_close
|
||||
.iter()
|
||||
.find(|w| w.name == handle.value().to_string())
|
||||
.unwrap();
|
||||
let close_pts = circle_wire_close.points.len();
|
||||
|
||||
assert!(
|
||||
close_pts > far_pts,
|
||||
"Close camera points ({close_pts}) should exceed far camera points ({far_pts})"
|
||||
);
|
||||
assert!(close_pts > 500, "Close points ({close_pts}) should be > 500");
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,15 @@ pub enum TangentGeom {
|
||||
start_angle: f64,
|
||||
end_angle: f64,
|
||||
},
|
||||
/// Ellipse or elliptical arc in an arbitrary world-space plane.
|
||||
PlanarEllipse {
|
||||
center: [f64; 3],
|
||||
major_axis: [f64; 3],
|
||||
normal: [f64; 3],
|
||||
minor_axis_ratio: f64,
|
||||
start_param: f64,
|
||||
end_param: f64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Viewport-relative display transform for a point marker.
|
||||
|
||||
+10
-4
@@ -819,7 +819,7 @@ impl Scene {
|
||||
fn model_wires_for_viewport(
|
||||
&self,
|
||||
vp_handle: Handle,
|
||||
_screen_height_px: f32,
|
||||
screen_height_px: f32,
|
||||
) -> Arc<Vec<WireModel>> {
|
||||
use rustc_hash::FxHashSet as HSet;
|
||||
|
||||
@@ -829,12 +829,17 @@ impl Scene {
|
||||
// wheel tick whenever the drawing contained one annotative object.
|
||||
// Explicit viewport annotation-scale changes still rebuild the resident
|
||||
// set; PSLTSCALE is a viewport GPU uniform.
|
||||
let frozen = match self.document.get_entity(vp_handle) {
|
||||
let (frozen, wpp) = match self.document.get_entity(vp_handle) {
|
||||
Some(EntityType::Viewport(vp)) => {
|
||||
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
||||
f
|
||||
let w = if screen_height_px > 0.0 && vp.view_height > 0.0 {
|
||||
Some((vp.view_height as f32) / screen_height_px)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(f, w)
|
||||
}
|
||||
_ => HSet::default(),
|
||||
_ => (HSet::default(), None),
|
||||
};
|
||||
|
||||
let scale_handle = self.viewport_scale_handle(vp_handle);
|
||||
@@ -844,6 +849,7 @@ impl Scene {
|
||||
scale_handle,
|
||||
Some(&frozen),
|
||||
Some(vp_handle),
|
||||
wpp,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
//! GPU-instanced analytical circle buffer & pipeline layout.
|
||||
//!
|
||||
//! Renders 2D/planar circles as analytical screen/plane-aligned quads with
|
||||
//! screen-space derivative anti-aliasing. Each circle is a single instance
|
||||
//! (6 vertices for a 2-triangle quad), eliminating CPU re-tessellation on zoom
|
||||
//! and providing infinite visual smoothness.
|
||||
|
||||
use iced::wgpu;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct CircleInstance {
|
||||
pub center_high: [f32; 3],
|
||||
pub _pad0: f32,
|
||||
pub center_low: [f32; 3],
|
||||
pub radius: f32,
|
||||
pub axis_x: [f32; 3],
|
||||
pub start_angle: f32,
|
||||
pub axis_y: [f32; 3],
|
||||
pub end_angle: f32,
|
||||
pub color: [f32; 4],
|
||||
/// `[half_width, pattern_length, draw_depth, padding]`
|
||||
pub params: [f32; 4],
|
||||
pub pat0: [f32; 4],
|
||||
pub pat1: [f32; 4],
|
||||
}
|
||||
|
||||
impl CircleInstance {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
const ATTRS: &[wgpu::VertexAttribute] = &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, center_high) as u64,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, center_low) as u64,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, axis_x) as u64,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, axis_y) as u64,
|
||||
shader_location: 3,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, color) as u64,
|
||||
shader_location: 4,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, params) as u64,
|
||||
shader_location: 5,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, pat0) as u64,
|
||||
shader_location: 6,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(CircleInstance, pat1) as u64,
|
||||
shader_location: 7,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
];
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<CircleInstance>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: ATTRS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CircleGpu {
|
||||
pub instance_buffer: wgpu::Buffer,
|
||||
pub instance_count: u32,
|
||||
}
|
||||
|
||||
impl CircleGpu {
|
||||
pub fn from_instances(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
label: &str,
|
||||
instances: &[CircleInstance],
|
||||
) -> Self {
|
||||
let instance_buffer = super::gpu_upload::upload_buffer(
|
||||
device,
|
||||
queue,
|
||||
label,
|
||||
instances,
|
||||
wgpu::BufferUsages::VERTEX,
|
||||
);
|
||||
Self {
|
||||
instance_buffer,
|
||||
instance_count: instances.len() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the base and xray circle pipelines.
|
||||
pub fn create_pipelines(
|
||||
device: &wgpu::Device,
|
||||
frame_bgl: &wgpu::BindGroupLayout,
|
||||
color_format: wgpu::TextureFormat,
|
||||
sample_count: u32,
|
||||
content_stencil: &wgpu::StencilState,
|
||||
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("circle.wgsl"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/circle.wgsl").into()),
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("circle.pipeline.layout"),
|
||||
bind_group_layouts: &[Some(frame_bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let circle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("circle.pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[CircleInstance::layout()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: sample_count,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let circle_xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("circle_xray.pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[CircleInstance::layout()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: sample_count,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
(circle_pipeline, circle_xray_pipeline)
|
||||
}
|
||||
|
||||
/// Helper to extract an analytical circle or circular arc instance from a `WireModel`.
|
||||
pub fn extract_circle_instance(
|
||||
wire: &crate::scene::WireModel,
|
||||
draw_depth: f32,
|
||||
) -> Option<CircleInstance> {
|
||||
// Only thin 2D planar circles or circular arcs have exactly 1 PlanarCircle / Arc tangent geometry
|
||||
// and no 3D mesh fills or text quads.
|
||||
if wire.tangent_geoms.len() != 1
|
||||
|| !wire.fill_tris.is_empty()
|
||||
|| !wire.pick_tris.is_empty()
|
||||
|| !wire.text_verts.is_empty()
|
||||
|| wire.render_instance.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let (center, axis_x, axis_y, radius, start_angle, end_angle) = match wire.tangent_geoms[0] {
|
||||
crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||
center,
|
||||
axis_x,
|
||||
axis_y,
|
||||
radius,
|
||||
} => (center, axis_x, axis_y, radius, 0.0f32, std::f32::consts::TAU),
|
||||
crate::scene::model::wire_model::TangentGeom::Arc {
|
||||
center,
|
||||
axis_x,
|
||||
axis_y,
|
||||
radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
} => (
|
||||
center,
|
||||
axis_x,
|
||||
axis_y,
|
||||
radius,
|
||||
start_angle as f32,
|
||||
end_angle as f32,
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
if radius <= 0.0 || !radius.is_finite() || !start_angle.is_finite() || !end_angle.is_finite() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]);
|
||||
let hw = wire.line_weight_px * 0.5;
|
||||
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
||||
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
||||
|
||||
Some(CircleInstance {
|
||||
center_high: ch,
|
||||
_pad0: 0.0,
|
||||
center_low: cl,
|
||||
radius: radius as f32,
|
||||
axis_x: [axis_x[0] as f32, axis_x[1] as f32, axis_x[2] as f32],
|
||||
start_angle,
|
||||
axis_y: [axis_y[0] as f32, axis_y[1] as f32, axis_y[2] as f32],
|
||||
end_angle,
|
||||
color: wire.color,
|
||||
params: [hw, wire.pattern_length, draw_depth, 0.0],
|
||||
pat0,
|
||||
pat1,
|
||||
})
|
||||
}
|
||||
|
||||
fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) {
|
||||
let hx = x as f32;
|
||||
let lx = (x - hx as f64) as f32;
|
||||
let hy = y as f32;
|
||||
let ly = (y - hy as f64) as f32;
|
||||
let hz = z as f32;
|
||||
let lz = (z - hz as f64) as f32;
|
||||
([hx, hy, hz], [lx, ly, lz])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn circle_instance_size_matches_stride() {
|
||||
assert_eq!(std::mem::size_of::<CircleInstance>(), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circle_shader_validates_with_naga() {
|
||||
let source = include_str!("../../shaders/circle.wgsl");
|
||||
let module = naga::front::wgsl::parse_str(source).expect("circle.wgsl parses cleanly");
|
||||
let mut validator = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
);
|
||||
validator
|
||||
.validate(&module)
|
||||
.expect("circle.wgsl validates cleanly with naga");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_circle_instance_planar_success() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||
center: [100.0, 200.0, 300.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 50.0,
|
||||
});
|
||||
wire.line_weight_px = 2.0;
|
||||
wire.color = [1.0, 0.0, 0.0, 1.0];
|
||||
|
||||
let inst = extract_circle_instance(&wire, 1.5).expect("should extract planar circle");
|
||||
assert_eq!(inst.radius, 50.0);
|
||||
assert_eq!(inst.start_angle, 0.0);
|
||||
assert_eq!(inst.end_angle, std::f32::consts::TAU);
|
||||
assert_eq!(inst.center_high, [100.0, 200.0, 300.0]);
|
||||
assert_eq!(inst.color, [1.0, 0.0, 0.0, 1.0]);
|
||||
assert_eq!(inst.params[0], 1.0); // half lineweight
|
||||
assert_eq!(inst.params[2], 1.5); // draw depth
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_circle_instance_arc_success() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||
center: [10.0, 20.0, 30.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 25.0,
|
||||
start_angle: 0.5,
|
||||
end_angle: 2.5,
|
||||
});
|
||||
wire.line_weight_px = 3.0;
|
||||
wire.color = [0.0, 1.0, 0.0, 1.0];
|
||||
|
||||
let inst = extract_circle_instance(&wire, 0.5).expect("should extract circular arc");
|
||||
assert_eq!(inst.radius, 25.0);
|
||||
assert_eq!(inst.start_angle, 0.5);
|
||||
assert_eq!(inst.end_angle, 2.5);
|
||||
assert_eq!(inst.center_high, [10.0, 20.0, 30.0]);
|
||||
assert_eq!(inst.color, [0.0, 1.0, 0.0, 1.0]);
|
||||
assert_eq!(inst.params[0], 1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_circle_instance_rejects_non_planar_or_complex() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 10.0,
|
||||
});
|
||||
// 1. With fill_tris (e.g. 3D solid or hatch)
|
||||
wire.fill_tris = vec![[0.0; 3]];
|
||||
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||
|
||||
// 2. Clear fill_tris, add render_instance (block instance)
|
||||
wire.fill_tris.clear();
|
||||
wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
|
||||
source_id: 1,
|
||||
translation: [0.0; 3],
|
||||
});
|
||||
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||
|
||||
// 3. Invalid radius
|
||||
wire.render_instance = None;
|
||||
wire.tangent_geoms[0] = crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: -5.0,
|
||||
};
|
||||
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
//! GPU-instanced analytical ellipse buffer & pipeline layout.
|
||||
//!
|
||||
//! Renders 2D/planar ellipses and elliptical arcs as analytical screen/plane-aligned quads
|
||||
//! with sub-pixel screen-space derivative anti-aliasing. Each ellipse is a single instance
|
||||
//! (6 vertices for a 2-triangle quad), eliminating CPU re-tessellation on zoom and providing
|
||||
//! mathematically exact curvature at any magnification.
|
||||
|
||||
use iced::wgpu;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct EllipseInstance {
|
||||
pub center_high: [f32; 3],
|
||||
pub _pad0: f32,
|
||||
pub center_low: [f32; 3],
|
||||
pub minor_axis_ratio: f32,
|
||||
pub major_axis: [f32; 3],
|
||||
pub start_param: f32,
|
||||
pub normal: [f32; 3],
|
||||
pub end_param: f32,
|
||||
pub color: [f32; 4],
|
||||
/// `[half_width, pattern_length, draw_depth, padding]`
|
||||
pub params: [f32; 4],
|
||||
pub pat0: [f32; 4],
|
||||
pub pat1: [f32; 4],
|
||||
}
|
||||
|
||||
impl EllipseInstance {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
const ATTRS: &[wgpu::VertexAttribute] = &[
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, center_high) as u64,
|
||||
shader_location: 0,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, center_low) as u64,
|
||||
shader_location: 1,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, major_axis) as u64,
|
||||
shader_location: 2,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, normal) as u64,
|
||||
shader_location: 3,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, color) as u64,
|
||||
shader_location: 4,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, params) as u64,
|
||||
shader_location: 5,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, pat0) as u64,
|
||||
shader_location: 6,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
wgpu::VertexAttribute {
|
||||
offset: std::mem::offset_of!(EllipseInstance, pat1) as u64,
|
||||
shader_location: 7,
|
||||
format: wgpu::VertexFormat::Float32x4,
|
||||
},
|
||||
];
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<EllipseInstance>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: ATTRS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EllipseGpu {
|
||||
pub instance_buffer: wgpu::Buffer,
|
||||
pub instance_count: u32,
|
||||
}
|
||||
|
||||
impl EllipseGpu {
|
||||
pub fn from_instances(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
label: &str,
|
||||
instances: &[EllipseInstance],
|
||||
) -> Self {
|
||||
let instance_buffer = super::gpu_upload::upload_buffer(
|
||||
device,
|
||||
queue,
|
||||
label,
|
||||
instances,
|
||||
wgpu::BufferUsages::VERTEX,
|
||||
);
|
||||
Self {
|
||||
instance_buffer,
|
||||
instance_count: instances.len() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the base and xray ellipse pipelines.
|
||||
pub fn create_pipelines(
|
||||
device: &wgpu::Device,
|
||||
frame_bgl: &wgpu::BindGroupLayout,
|
||||
color_format: wgpu::TextureFormat,
|
||||
sample_count: u32,
|
||||
content_stencil: &wgpu::StencilState,
|
||||
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("ellipse.wgsl"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/ellipse.wgsl").into()),
|
||||
});
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("ellipse.pipeline.layout"),
|
||||
bind_group_layouts: &[Some(frame_bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let ellipse_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("ellipse.pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[EllipseInstance::layout()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: sample_count,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let ellipse_xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("ellipse_xray.pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[EllipseInstance::layout()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: color_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||
stencil: content_stencil.clone(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: sample_count,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
(ellipse_pipeline, ellipse_xray_pipeline)
|
||||
}
|
||||
|
||||
/// Helper to extract an analytical ellipse instance from a `WireModel`.
|
||||
pub fn extract_ellipse_instance(
|
||||
wire: &crate::scene::WireModel,
|
||||
draw_depth: f32,
|
||||
) -> Option<EllipseInstance> {
|
||||
if wire.tangent_geoms.len() != 1
|
||||
|| !wire.fill_tris.is_empty()
|
||||
|| !wire.pick_tris.is_empty()
|
||||
|| !wire.text_verts.is_empty()
|
||||
|| wire.render_instance.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||
center,
|
||||
major_axis,
|
||||
normal,
|
||||
minor_axis_ratio,
|
||||
start_param,
|
||||
end_param,
|
||||
} = wire.tangent_geoms[0] else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let major_len_sq = major_axis[0] * major_axis[0] + major_axis[1] * major_axis[1] + major_axis[2] * major_axis[2];
|
||||
let norm_len_sq = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2];
|
||||
if major_len_sq <= 1e-12 || !major_len_sq.is_finite()
|
||||
|| norm_len_sq <= 1e-12 || !norm_len_sq.is_finite()
|
||||
|| minor_axis_ratio <= 1e-6 || !minor_axis_ratio.is_finite()
|
||||
|| !start_param.is_finite() || !end_param.is_finite()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]);
|
||||
let hw = wire.line_weight_px * 0.5;
|
||||
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
||||
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
||||
|
||||
Some(EllipseInstance {
|
||||
center_high: ch,
|
||||
_pad0: 0.0,
|
||||
center_low: cl,
|
||||
minor_axis_ratio: minor_axis_ratio.min(1.0) as f32,
|
||||
major_axis: [major_axis[0] as f32, major_axis[1] as f32, major_axis[2] as f32],
|
||||
start_param: start_param as f32,
|
||||
normal: [normal[0] as f32, normal[1] as f32, normal[2] as f32],
|
||||
end_param: end_param as f32,
|
||||
color: wire.color,
|
||||
params: [hw, wire.pattern_length, draw_depth, 0.0],
|
||||
pat0,
|
||||
pat1,
|
||||
})
|
||||
}
|
||||
|
||||
fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) {
|
||||
let hx = x as f32;
|
||||
let lx = (x - hx as f64) as f32;
|
||||
let hy = y as f32;
|
||||
let ly = (y - hy as f64) as f32;
|
||||
let hz = z as f32;
|
||||
let lz = (z - hz as f64) as f32;
|
||||
([hx, hy, hz], [lx, ly, lz])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ellipse_instance_size_matches_stride() {
|
||||
assert_eq!(std::mem::size_of::<EllipseInstance>(), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ellipse_shader_validates_with_naga() {
|
||||
let source = include_str!("../../shaders/ellipse.wgsl");
|
||||
let module = naga::front::wgsl::parse_str(source).expect("ellipse.wgsl parses cleanly");
|
||||
let mut validator = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
);
|
||||
validator
|
||||
.validate(&module)
|
||||
.expect("ellipse.wgsl validates cleanly with naga");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_ellipse_instance_success() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||
center: [100.0, 200.0, 300.0],
|
||||
major_axis: [50.0, 0.0, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
minor_axis_ratio: 0.5,
|
||||
start_param: 0.0,
|
||||
end_param: std::f64::consts::TAU,
|
||||
});
|
||||
wire.line_weight_px = 2.0;
|
||||
wire.color = [1.0, 0.5, 0.0, 1.0];
|
||||
|
||||
let inst = extract_ellipse_instance(&wire, 1.0).expect("should extract ellipse");
|
||||
assert_eq!(inst.center_high, [100.0, 200.0, 300.0]);
|
||||
assert_eq!(inst.major_axis, [50.0, 0.0, 0.0]);
|
||||
assert_eq!(inst.minor_axis_ratio, 0.5);
|
||||
assert_eq!(inst.start_param, 0.0);
|
||||
assert_eq!(inst.end_param, std::f32::consts::TAU);
|
||||
assert_eq!(inst.color, [1.0, 0.5, 0.0, 1.0]);
|
||||
assert_eq!(inst.params[0], 1.0);
|
||||
assert_eq!(inst.params[2], 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_elliptical_arc_instance_success() {
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
major_axis: [10.0, 0.0, 0.0],
|
||||
normal: [0.0, 0.0, 1.0],
|
||||
minor_axis_ratio: 0.8,
|
||||
start_param: 0.5,
|
||||
end_param: 2.5,
|
||||
});
|
||||
wire.line_weight_px = 1.0;
|
||||
wire.color = [0.0, 1.0, 0.0, 1.0];
|
||||
|
||||
let inst = extract_ellipse_instance(&wire, 0.5).expect("should extract elliptical arc");
|
||||
assert_eq!(inst.minor_axis_ratio, 0.8);
|
||||
assert_eq!(inst.start_param, 0.5);
|
||||
assert_eq!(inst.end_param, 2.5);
|
||||
assert_eq!(inst.params[0], 0.5); // half-width
|
||||
assert_eq!(inst.params[2], 0.5); // draw-depth
|
||||
}
|
||||
|
||||
fn rust_closest_point_ellipse(p_in: [f32; 2], ab_in: [f32; 2]) -> [f32; 2] {
|
||||
let px = p_in[0].abs();
|
||||
let py = p_in[1].abs();
|
||||
let a = ab_in[0];
|
||||
let b = ab_in[1];
|
||||
if (a - b).abs() < 1e-6 {
|
||||
let len = (px * px + py * py).sqrt();
|
||||
let q = if len > 1e-6 {
|
||||
[px / len * a, py / len * a]
|
||||
} else {
|
||||
[a, 0.0]
|
||||
};
|
||||
return [
|
||||
if p_in[0] >= 0.0 { q[0] } else { -q[0] },
|
||||
if p_in[1] >= 0.0 { q[1] } else { -q[1] },
|
||||
];
|
||||
}
|
||||
|
||||
// Initial estimate from eccentric anomaly of radial direction
|
||||
let mut t = (py * a).atan2(px * b);
|
||||
let k = a * a - b * b;
|
||||
|
||||
// 3 iterations of Newton-Raphson
|
||||
for _ in 0..3 {
|
||||
let (st, ct) = t.sin_cos();
|
||||
let f = k * st * ct - px * a * st + py * b * ct;
|
||||
let f_prime = k * (ct * ct - st * st) - px * a * ct - py * b * st;
|
||||
if f_prime.abs() > 1e-7 {
|
||||
t -= f / f_prime;
|
||||
}
|
||||
}
|
||||
t = t.clamp(0.0, std::f32::consts::FRAC_PI_2);
|
||||
let q = [a * t.cos(), b * t.sin()];
|
||||
[
|
||||
if p_in[0] >= 0.0 { q[0] } else { -q[0] },
|
||||
if p_in[1] >= 0.0 { q[1] } else { -q[1] },
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closest_point_ellipse_accuracy() {
|
||||
let a = 100.0f32;
|
||||
let b = 40.0f32;
|
||||
// Test points directly on the ellipse: closest point should be itself!
|
||||
for i in 0..36 {
|
||||
let angle = (i as f32) * std::f32::consts::TAU / 36.0;
|
||||
let px = a * angle.cos();
|
||||
let py = b * angle.sin();
|
||||
let q = rust_closest_point_ellipse([px, py], [a, b]);
|
||||
let dist = ((q[0] - px).powi(2) + (q[1] - py).powi(2)).sqrt();
|
||||
assert!(
|
||||
dist < 0.2,
|
||||
"Point on ellipse at angle {angle} failed: px={px}, py={py}, q={:?}, dist={dist}",
|
||||
q
|
||||
);
|
||||
// Verify q is on the ellipse (x/a)^2 + (y/b)^2 == 1
|
||||
let eq = (q[0] / a).powi(2) + (q[1] / b).powi(2);
|
||||
assert!((eq - 1.0).abs() < 0.01, "eq={eq} for angle {angle}");
|
||||
}
|
||||
|
||||
// Test points offset from ellipse (inside and outside)
|
||||
for i in 0..36 {
|
||||
let angle = (i as f32) * std::f32::consts::TAU / 36.0;
|
||||
for offset in [-5.0f32, -1.0, 1.0, 5.0] {
|
||||
let norm = [angle.cos() / a, angle.sin() / b];
|
||||
let norm_len = (norm[0] * norm[0] + norm[1] * norm[1]).sqrt();
|
||||
let un = [norm[0] / norm_len, norm[1] / norm_len];
|
||||
let px = a * angle.cos() + offset * un[0];
|
||||
let py = b * angle.sin() + offset * un[1];
|
||||
let q = rust_closest_point_ellipse([px, py], [a, b]);
|
||||
let eq = (q[0] / a).powi(2) + (q[1] / b).powi(2);
|
||||
assert!(
|
||||
(eq - 1.0).abs() < 0.05,
|
||||
"Offset point at angle {angle}, offset {offset}: q={:?}, eq={eq}",
|
||||
q
|
||||
);
|
||||
let measured_dist = ((q[0] - px).powi(2) + (q[1] - py).powi(2)).sqrt();
|
||||
assert!(
|
||||
(measured_dist - offset.abs()).abs() < 0.2,
|
||||
"Distance error at angle {angle}, offset {offset}: measured={measured_dist}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+231
-2
@@ -1,6 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod gpu_tests;
|
||||
mod device_capabilities;
|
||||
pub mod circle_gpu;
|
||||
pub mod ellipse_gpu;
|
||||
pub mod face3d_gpu;
|
||||
pub mod gpu_budget;
|
||||
pub mod gpu_upload;
|
||||
@@ -20,6 +22,8 @@ pub mod wire_gpu;
|
||||
use iced::wgpu;
|
||||
use iced::{Rectangle, Size};
|
||||
|
||||
pub use circle_gpu::{CircleGpu, CircleInstance};
|
||||
pub use ellipse_gpu::{EllipseGpu, EllipseInstance};
|
||||
pub use face3d_gpu::Face3DGpu;
|
||||
pub use wipeout_gpu::WipeoutGpu;
|
||||
pub use image_gpu::ImageGpu;
|
||||
@@ -79,6 +83,8 @@ pub struct Pipeline {
|
||||
shadow_pipeline: wgpu::RenderPipeline,
|
||||
shadow_plain_pipeline: wgpu::RenderPipeline,
|
||||
wire_pipeline: wgpu::RenderPipeline,
|
||||
circle_pipeline: wgpu::RenderPipeline,
|
||||
ellipse_pipeline: wgpu::RenderPipeline,
|
||||
block_wire_pipeline: wgpu::RenderPipeline,
|
||||
/// Stamps clip-boundary polygons into the stencil buffer (viewports + XCLIP).
|
||||
clip_mask_pipeline: wgpu::RenderPipeline,
|
||||
@@ -89,6 +95,8 @@ pub struct Pipeline {
|
||||
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
||||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||
circle_xray_pipeline: wgpu::RenderPipeline,
|
||||
ellipse_xray_pipeline: wgpu::RenderPipeline,
|
||||
block_wire_xray_pipeline: wgpu::RenderPipeline,
|
||||
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
|
||||
/// xray pipelines). `Some` on any storage-capable device; `None` in packed
|
||||
@@ -225,6 +233,8 @@ pub struct Pipeline {
|
||||
/// replace this thin draw-range list on camera changes without touching the
|
||||
/// shared resident buffer.
|
||||
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
|
||||
pub(crate) gpu_circles: std::sync::Arc<Vec<CircleGpu>>,
|
||||
pub(crate) gpu_ellipses: std::sync::Arc<Vec<EllipseGpu>>,
|
||||
pub(crate) gpu_block_wires: std::sync::Arc<Vec<BlockWireGpu>>,
|
||||
/// Persistent per-entity wire instance arena (capability-selected format).
|
||||
/// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an
|
||||
@@ -256,6 +266,8 @@ pub struct Pipeline {
|
||||
clip_boundary: Option<(wgpu::Buffer, u32)>,
|
||||
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
|
||||
gpu_selected_wires: Vec<WireGpu>,
|
||||
gpu_selected_circles: Vec<CircleGpu>,
|
||||
gpu_selected_ellipses: Vec<EllipseGpu>,
|
||||
gpu_selected_block_wires: Vec<BlockWireGpu>,
|
||||
/// Command-preview / interim / grip-drag overlay wires. Re-uploaded every
|
||||
/// frame they are present (small), drawn on top of the base wire pass — so
|
||||
@@ -2083,6 +2095,22 @@ impl Pipeline {
|
||||
&content_stencil,
|
||||
);
|
||||
|
||||
let (circle_pipeline, circle_xray_pipeline) = circle_gpu::create_pipelines(
|
||||
device,
|
||||
&frame_bgl,
|
||||
format,
|
||||
MSAA_SAMPLES,
|
||||
&content_stencil,
|
||||
);
|
||||
|
||||
let (ellipse_pipeline, ellipse_xray_pipeline) = ellipse_gpu::create_pipelines(
|
||||
device,
|
||||
&frame_bgl,
|
||||
format,
|
||||
MSAA_SAMPLES,
|
||||
&content_stencil,
|
||||
);
|
||||
|
||||
let viewcube = ViewCubePipeline::new(device, queue, format);
|
||||
|
||||
let init_size = Size::new(1, 1);
|
||||
@@ -2223,6 +2251,10 @@ impl Pipeline {
|
||||
wire_black_pipeline,
|
||||
block_wire_black_pipeline,
|
||||
wire_xray_pipeline,
|
||||
circle_pipeline,
|
||||
circle_xray_pipeline,
|
||||
ellipse_pipeline,
|
||||
ellipse_xray_pipeline,
|
||||
block_wire_xray_pipeline,
|
||||
wire_const_bgl,
|
||||
block_wire_const_bgl,
|
||||
@@ -2295,6 +2327,8 @@ impl Pipeline {
|
||||
blit_uniform_buffer,
|
||||
surface_format: format,
|
||||
gpu_wires: std::sync::Arc::new(vec![]),
|
||||
gpu_circles: std::sync::Arc::new(vec![]),
|
||||
gpu_ellipses: std::sync::Arc::new(vec![]),
|
||||
gpu_block_wires: std::sync::Arc::new(vec![]),
|
||||
wire_arena: None,
|
||||
wire_arena_mesh: None,
|
||||
@@ -2308,6 +2342,8 @@ impl Pipeline {
|
||||
silhouette_key: (usize::MAX, u64::MAX, [u32::MAX; 3], false),
|
||||
clip_boundary: None,
|
||||
gpu_selected_wires: vec![],
|
||||
gpu_selected_circles: vec![],
|
||||
gpu_selected_ellipses: vec![],
|
||||
gpu_selected_block_wires: vec![],
|
||||
gpu_preview_wires: vec![],
|
||||
gpu_wipeouts: vec![],
|
||||
@@ -2371,6 +2407,66 @@ impl Pipeline {
|
||||
)
|
||||
}
|
||||
|
||||
/// Upload GPU-instanced analytical circles.
|
||||
pub fn upload_circles(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
) -> Vec<CircleGpu> {
|
||||
let mut instances: Vec<CircleInstance> = Vec::new();
|
||||
for wire in wires {
|
||||
if !wire.display_visible {
|
||||
continue;
|
||||
}
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(inst) = circle_gpu::extract_circle_instance(wire, depth) {
|
||||
instances.push(inst);
|
||||
}
|
||||
}
|
||||
if instances.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![CircleGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"viewer.circles",
|
||||
&instances,
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload GPU-instanced analytical ellipses.
|
||||
pub fn upload_ellipses(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
wires: &[WireModel],
|
||||
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||
) -> Vec<EllipseGpu> {
|
||||
let mut instances: Vec<EllipseInstance> = Vec::new();
|
||||
for wire in wires {
|
||||
if !wire.display_visible {
|
||||
continue;
|
||||
}
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(inst) = ellipse_gpu::extract_ellipse_instance(wire, depth) {
|
||||
instances.push(inst);
|
||||
}
|
||||
}
|
||||
if instances.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![EllipseGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"viewer.ellipses",
|
||||
&instances,
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Build resident batches and a handle index shared across viewport slots.
|
||||
pub fn build_wire_buffers(
|
||||
&mut self,
|
||||
@@ -2383,6 +2479,8 @@ impl Pipeline {
|
||||
std::sync::Arc<Vec<WireGpu>>,
|
||||
std::sync::Arc<Vec<BlockWireGpu>>,
|
||||
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
|
||||
std::sync::Arc<Vec<CircleGpu>>,
|
||||
std::sync::Arc<Vec<EllipseGpu>>,
|
||||
) {
|
||||
// Batch the wire pass: instead of one GPU buffer + one draw call per
|
||||
// WireModel (tens of thousands on a large drawing), merge maximal runs
|
||||
@@ -2407,11 +2505,19 @@ impl Pipeline {
|
||||
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;
|
||||
@@ -2462,10 +2568,14 @@ impl Pipeline {
|
||||
wires.len(),
|
||||
);
|
||||
}
|
||||
let circle_batches = self.upload_circles(device, queue, wires, depth_map);
|
||||
let ellipse_batches = self.upload_ellipses(device, queue, wires, depth_map);
|
||||
(
|
||||
std::sync::Arc::new(batches),
|
||||
std::sync::Arc::new(block_batches),
|
||||
std::sync::Arc::new(index),
|
||||
std::sync::Arc::new(circle_batches),
|
||||
std::sync::Arc::new(ellipse_batches),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2492,6 +2602,8 @@ impl Pipeline {
|
||||
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
|
||||
self.gpu_selected_wires = vec![];
|
||||
self.gpu_selected_block_wires = vec![];
|
||||
self.gpu_selected_circles = vec![];
|
||||
self.gpu_selected_ellipses = vec![];
|
||||
return;
|
||||
}
|
||||
// Gather borrowed highlighted wires via the prebuilt index —
|
||||
@@ -2527,15 +2639,79 @@ impl Pipeline {
|
||||
hover_wires.push(wire);
|
||||
}
|
||||
}
|
||||
let mut selected_circles: Vec<CircleInstance> = Vec::new();
|
||||
for &wire in &selected_wires {
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(mut inst) = circle_gpu::extract_circle_instance(wire, depth) {
|
||||
if let Some(tint) = selected_tint {
|
||||
inst.color = tint;
|
||||
}
|
||||
selected_circles.push(inst);
|
||||
}
|
||||
}
|
||||
for &wire in &hover_wires {
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(mut inst) = circle_gpu::extract_circle_instance(wire, depth) {
|
||||
inst.color = WireModel::HOVER;
|
||||
selected_circles.push(inst);
|
||||
}
|
||||
}
|
||||
self.gpu_selected_circles = if selected_circles.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![CircleGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"viewer.selected_circles",
|
||||
&selected_circles,
|
||||
)]
|
||||
};
|
||||
|
||||
let mut selected_ellipses: Vec<EllipseInstance> = Vec::new();
|
||||
for &wire in &selected_wires {
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(mut inst) = ellipse_gpu::extract_ellipse_instance(wire, depth) {
|
||||
if let Some(tint) = selected_tint {
|
||||
inst.color = tint;
|
||||
}
|
||||
selected_ellipses.push(inst);
|
||||
}
|
||||
}
|
||||
for &wire in &hover_wires {
|
||||
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||
if let Some(mut inst) = ellipse_gpu::extract_ellipse_instance(wire, depth) {
|
||||
inst.color = WireModel::HOVER;
|
||||
selected_ellipses.push(inst);
|
||||
}
|
||||
}
|
||||
self.gpu_selected_ellipses = if selected_ellipses.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![EllipseGpu::from_instances(
|
||||
device,
|
||||
queue,
|
||||
"viewer.selected_ellipses",
|
||||
&selected_ellipses,
|
||||
)]
|
||||
};
|
||||
|
||||
let selected_regular: Vec<&WireModel> = selected_wires
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|wire| wire.render_instance.is_none())
|
||||
.filter(|wire| {
|
||||
wire.render_instance.is_none()
|
||||
&& circle_gpu::extract_circle_instance(wire, 0.0).is_none()
|
||||
&& ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_none()
|
||||
})
|
||||
.collect();
|
||||
let hover_regular: Vec<&WireModel> = hover_wires
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|wire| wire.render_instance.is_none())
|
||||
.filter(|wire| {
|
||||
wire.render_instance.is_none()
|
||||
&& circle_gpu::extract_circle_instance(wire, 0.0).is_none()
|
||||
&& ellipse_gpu::extract_ellipse_instance(wire, 0.0).is_none()
|
||||
})
|
||||
.collect();
|
||||
let selected_blocks: Vec<&WireModel> = selected_wires
|
||||
.iter()
|
||||
@@ -4373,6 +4549,26 @@ impl Pipeline {
|
||||
}
|
||||
bind_and_draw_block_wire(&mut pass, wire);
|
||||
}
|
||||
// Analytical GPU circles (instanced screen-space quads)
|
||||
if self.gpu_circles.iter().any(|cg| cg.instance_count > 0) {
|
||||
pass.set_pipeline(&self.circle_pipeline);
|
||||
for cg in self.gpu_circles.iter() {
|
||||
if cg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..cg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Analytical GPU ellipses (instanced screen-space quads)
|
||||
if self.gpu_ellipses.iter().any(|eg| eg.instance_count > 0) {
|
||||
pass.set_pipeline(&self.ellipse_pipeline);
|
||||
for eg in self.gpu_ellipses.iter() {
|
||||
if eg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..eg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Live overlay wires (command preview / interim / grip drag) always
|
||||
// on top: the xray pipeline (depth_compare=Always, no depth write)
|
||||
// keeps them visible through any occluding geometry — a 3D solid, or
|
||||
@@ -4563,6 +4759,32 @@ impl Pipeline {
|
||||
bind_and_draw_block_wire(&mut pass, wire);
|
||||
}
|
||||
}
|
||||
if !self.gpu_selected_circles.is_empty() {
|
||||
pass.set_pipeline(if hidden_line {
|
||||
&self.circle_pipeline
|
||||
} else {
|
||||
&self.circle_xray_pipeline
|
||||
});
|
||||
for cg in &self.gpu_selected_circles {
|
||||
if cg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..cg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.gpu_selected_ellipses.is_empty() {
|
||||
pass.set_pipeline(if hidden_line {
|
||||
&self.ellipse_pipeline
|
||||
} else {
|
||||
&self.ellipse_xray_pipeline
|
||||
});
|
||||
for eg in &self.gpu_selected_ellipses {
|
||||
if eg.instance_count > 0 {
|
||||
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
|
||||
pass.draw(0..6, 0..eg.instance_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(atlas) = &self.text_atlas_gpu {
|
||||
if !self.text_highlight_gpu.is_empty() {
|
||||
pass.set_pipeline(&self.text_highlight_pipeline);
|
||||
@@ -4711,6 +4933,8 @@ impl Pipeline {
|
||||
|
||||
self.gpu_wires = std::sync::Arc::new(Vec::new());
|
||||
self.gpu_block_wires = std::sync::Arc::new(Vec::new());
|
||||
self.gpu_circles = std::sync::Arc::new(Vec::new());
|
||||
self.gpu_ellipses = std::sync::Arc::new(Vec::new());
|
||||
self.wire_handle_index = std::sync::Arc::new(rustc_hash::FxHashMap::default());
|
||||
self.wire_arena = None;
|
||||
self.wire_arena_mesh = None;
|
||||
@@ -4721,6 +4945,8 @@ impl Pipeline {
|
||||
|
||||
self.gpu_selected_wires.clear();
|
||||
self.gpu_selected_block_wires.clear();
|
||||
self.gpu_selected_circles.clear();
|
||||
self.gpu_selected_ellipses.clear();
|
||||
self.gpu_preview_wires.clear();
|
||||
self.hatch_gpu.clear();
|
||||
self.gpu_wipeouts.clear();
|
||||
@@ -4990,6 +5216,7 @@ impl MultiPipeline {
|
||||
self.wire_buffer_cache.retain(|_, entry| {
|
||||
std::sync::Arc::strong_count(&entry.0) > 1
|
||||
|| std::sync::Arc::strong_count(&entry.1) > 1
|
||||
|| std::sync::Arc::strong_count(&entry.3) > 1
|
||||
});
|
||||
self.block_geometry.retain(|_, chunks| {
|
||||
chunks.iter().any(|chunk| std::sync::Arc::strong_count(&chunk.bind_group) > 1)
|
||||
@@ -5140,6 +5367,8 @@ pub struct MultiPipeline {
|
||||
std::sync::Arc<Vec<WireGpu>>,
|
||||
std::sync::Arc<Vec<BlockWireGpu>>,
|
||||
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
|
||||
std::sync::Arc<Vec<CircleGpu>>,
|
||||
std::sync::Arc<Vec<EllipseGpu>>,
|
||||
),
|
||||
>,
|
||||
}
|
||||
|
||||
@@ -126,7 +126,11 @@ 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()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -853,6 +853,22 @@ impl shader::Primitive for Primitive {
|
||||
&mut pipeline.block_geometry,
|
||||
),
|
||||
);
|
||||
inner.gpu_circles = std::sync::Arc::new(
|
||||
inner.upload_circles(
|
||||
device,
|
||||
queue,
|
||||
&vp_wires,
|
||||
&draw_depths,
|
||||
),
|
||||
);
|
||||
inner.gpu_ellipses = std::sync::Arc::new(
|
||||
inner.upload_ellipses(
|
||||
device,
|
||||
queue,
|
||||
&vp_wires,
|
||||
&draw_depths,
|
||||
),
|
||||
);
|
||||
if _patched {
|
||||
wire_arena::patch_handle_index(
|
||||
&mut inner.wire_handle_index,
|
||||
@@ -911,10 +927,14 @@ impl shader::Primitive for Primitive {
|
||||
// Other panes retain their own references to shared geometry.
|
||||
inner.gpu_wires = std::sync::Arc::new(Vec::new());
|
||||
inner.gpu_block_wires = std::sync::Arc::new(Vec::new());
|
||||
inner.gpu_circles = std::sync::Arc::new(Vec::new());
|
||||
inner.gpu_ellipses = std::sync::Arc::new(Vec::new());
|
||||
let held_before = pipeline.wire_buffer_cache.len();
|
||||
pipeline.wire_buffer_cache.retain(|_, (w, b, _)| {
|
||||
pipeline.wire_buffer_cache.retain(|_, (w, b, _, c, e)| {
|
||||
std::sync::Arc::strong_count(w) > 1
|
||||
|| std::sync::Arc::strong_count(b) > 1
|
||||
|| std::sync::Arc::strong_count(c) > 1
|
||||
|| std::sync::Arc::strong_count(e) > 1
|
||||
});
|
||||
if _perf {
|
||||
crate::perf_record!(
|
||||
@@ -959,6 +979,8 @@ impl shader::Primitive for Primitive {
|
||||
inner.gpu_wires = built.0;
|
||||
inner.gpu_block_wires = built.1;
|
||||
inner.wire_handle_index = built.2;
|
||||
inner.gpu_circles = built.3;
|
||||
inner.gpu_ellipses = built.4;
|
||||
} // end !arena_served
|
||||
inner.cached_wire_id = vp.wire_content_id;
|
||||
if _perf {
|
||||
|
||||
Reference in new issue
Block a user