perf(render): instance block geometry

Share source buffers across block placements and render nested content with GPU instance transforms.\n\nChunk silhouettes and compact mesh material data so large drawings stay under device buffer limits and remain responsive.
This commit is contained in:
Hakan Seven committed 2026-08-12 02:33:10 +03:00
1 parent 7bfb617745
commit 4fd9b98da4
68 files changed
+3417 -547

No files matched your search

+269 -55
View File
@@ -146,6 +146,7 @@ pub struct BlockCache {
expansion_prototypes: std::sync::Mutex<
HashMap<ExpansionPrototypeKey, Arc<std::sync::Mutex<Option<Arc<CachedExpansion>>>>>,
>,
clip_prototypes: std::sync::Mutex<HashMap<(u64, Vec<u64>), u64>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -164,6 +165,24 @@ struct CachedExpansion {
}
impl BlockCache {
pub fn clip_source_id(
&self,
source_id: u64,
polygon: &[[f64; 2]],
translation: [f64; 3],
) -> u64 {
let mut shape = Vec::with_capacity(polygon.len() * 2);
for point in polygon {
shape.push((point[0] - translation[0]).to_bits());
shape.push((point[1] - translation[1]).to_bits());
}
*self
.clip_prototypes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.entry((source_id, shape))
.or_insert_with(crate::scene::model::instance_model::next_source_id)
}
pub fn new() -> Self {
Self::default()
}
@@ -911,9 +930,7 @@ pub fn expand_insert(
xform = xform.then(&scale_about_p);
}
let name = ins_handle.value().to_string();
let prototype_key = if view_aabb.is_none()
&& world_per_pixel.is_none()
&& !ins.is_array()
let prototype_key = if !ins.is_array()
&& cache.prototype_blocks.contains(&ins.block_name)
{
Some(expansion_prototype_key(
@@ -994,7 +1011,71 @@ pub fn expand_insert(
}
}
for offset in &crate::scene::render_graph::array_offsets(ins) {
let ctx = ExpandCtx {
cache,
ins_color: ins_resolved_color,
ins_aci,
ins_pat_len,
ins_pat,
ins_lw_px,
l0: ins_layer,
l0_aci: ins_layer_aci,
selected,
pslt_factor,
view_aabb: None,
world_per_pixel: None,
is_xref,
bg_color,
};
let offsets = crate::scene::render_graph::array_offsets(ins);
if offsets.len() > 1 {
let cell_xform = |offset: &[f64; 3]| {
if offset == &[0.0; 3] {
xform.clone()
} else {
Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2]))
.then(&xform)
}
};
let first_xform = cell_xform(&offsets[0]);
let first_translation = transform_translation(&first_xform);
let mut first_batches = Batches::default();
expand_defn(
defn,
&first_xform,
&ctx,
&mut first_batches,
&mut visited,
0,
(0.0, 1.0),
);
let mut first = first_batches.finalize(&name, selected, bg_color);
for wire in &mut first {
if wire.render_instance.is_none() {
wire.render_instance = Some(
crate::scene::model::instance_model::RenderInstance {
source_id: crate::scene::model::instance_model::next_source_id(),
translation: first_translation,
},
);
}
}
let mut result = first.clone();
for offset in offsets.iter().skip(1) {
let translation = transform_translation(&cell_xform(offset));
let delta = [
translation[0] - first_translation[0],
translation[1] - first_translation[1],
translation[2] - first_translation[2],
];
result.extend(first.iter().map(|wire| {
translated_prototype_wire(wire, &name, delta)
}));
}
return Some(result);
}
for offset in &offsets {
let base_xform = if offset == &[0.0; 3] {
xform.clone()
} else {
@@ -1003,28 +1084,21 @@ pub fn expand_insert(
));
translation.then(&xform)
};
let ctx = ExpandCtx {
cache,
ins_color: ins_resolved_color,
ins_aci,
ins_pat_len,
ins_pat,
ins_lw_px,
l0: ins_layer,
l0_aci: ins_layer_aci,
selected,
pslt_factor,
view_aabb,
world_per_pixel,
is_xref,
bg_color,
};
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0, (0.0, 1.0));
}
let wires = batches.finalize(&name, selected, bg_color);
let mut wires = batches.finalize(&name, selected, bg_color);
if let Some(guard) = prototype_guard.as_mut() {
let translation = transform_translation(&xform);
for wire in &mut wires {
if wire.render_instance.is_none() {
wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
source_id: crate::scene::model::instance_model::next_source_id(),
translation,
});
}
}
let cached = Arc::new(CachedExpansion {
translation: transform_translation(&xform),
translation,
wires: Arc::new(wires.clone()),
});
**guard = Some(cached);
@@ -1088,9 +1162,18 @@ fn expansion_prototype_key(
}
}
fn translated_prototype_wire(source: &WireModel, name: &str, delta: [f64; 3]) -> WireModel {
fn translated_prototype_wire(
source: &WireModel,
name: &str,
delta: [f64; 3],
) -> WireModel {
let mut wire = source.clone();
wire.name = name.to_string();
if let Some(instance) = wire.render_instance.as_mut() {
for axis in 0..3 {
instance.translation[axis] += delta[axis];
}
}
translate_double_single(&mut wire.points, &mut wire.points_low, delta);
translate_double_single(&mut wire.fill_tris, &mut wire.fill_tris_low, delta);
translate_double_single(&mut wire.pick_tris, &mut wire.pick_tris_low, delta);
@@ -1182,6 +1265,39 @@ struct ExpandCtx<'a> {
bg_color: [f32; 4],
}
fn nested_prototype_key(
block_name: &str,
transform: &Transform,
ctx: &ExpandCtx<'_>,
depth_scale: f32,
) -> NestedPrototypeKey {
let matrix = &transform.matrix.m;
let linear = [
matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(),
matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(),
matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(),
];
let mut style = Vec::with_capacity(32);
style.extend(ctx.ins_color.map(f32::to_bits));
style.push(ctx.ins_pat_len.to_bits());
style.extend(ctx.ins_pat.map(f32::to_bits));
style.push(ctx.ins_lw_px.to_bits());
style.extend(ctx.l0.color.map(f32::to_bits));
style.push(ctx.l0.pat_len.to_bits());
style.extend(ctx.l0.pat.map(f32::to_bits));
style.push(ctx.l0.lw_px.to_bits());
style.push(ctx.pslt_factor.to_bits());
style.extend(ctx.bg_color.map(f32::to_bits));
NestedPrototypeKey {
block_name: block_name.to_string(),
linear,
style,
selected: ctx.selected,
is_xref: ctx.is_xref,
depth_scale: depth_scale.to_bits(),
}
}
/// Fade `color` toward `bg` by 50%, preserving alpha. Used to mark xref
/// geometry — the hue stays recognizable but the contrast against the
/// background drops, reading as "washed out".
@@ -1259,6 +1375,23 @@ struct BatchEntry {
max_y: f32,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct NestedPrototypeKey {
block_name: String,
linear: [u64; 9],
style: Vec<u32>,
selected: bool,
is_xref: bool,
depth_scale: u32,
}
#[derive(Clone, Debug)]
struct NestedPrototype {
translation: [f64; 3],
depth_base: f32,
wires: Arc<Vec<WireModel>>,
}
/// Hard cap on point count for a single batched WireModel. Above this the
/// current batch is finalized (pushed into `closed`) and a fresh one is
/// started under the same style. Each WireModel point becomes ~6 GPU
@@ -1276,6 +1409,7 @@ struct Batches {
/// WireModels by `clip_wires`. Appended verbatim at `finalize` (only their
/// name/selected flag are stamped to match the host insert).
extra_wires: Vec<WireModel>,
nested_prototypes: HashMap<NestedPrototypeKey, NestedPrototype>,
}
impl BatchEntry {
@@ -1338,6 +1472,7 @@ impl Batches {
depth_override: b.local_depth,
fill_is_3d: false,
fill_is_2d_solid: b.fill_is_2d_solid,
render_instance: None,
pick_tris: b.pick_tris,
pick_tris_low: b.pick_tris_low,
dash_from_start: false,
@@ -1529,54 +1664,133 @@ fn expand_defn(
d_range.0 + nref.local_rank * d_range.1,
d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0),
);
let composed_for = |offset: &[f64; 3]| {
if offset == &[0.0; 3] {
nref.xform.then(accum_xform)
} else {
Transform::from_translation(Vector3::new(
offset[0], offset[1], offset[2],
))
.then(&nref.xform)
.then(accum_xform)
}
};
if let Some(cp) = &nref.clip_poly {
// XCLIP'd nested insert: expand into an isolated batch set,
// finalize to whole wires, clip them to the boundary mapped
// into world by the accumulated transform, then carry the
// clipped wires out via `extra_wires`. Single instance only —
// an array of clipped nested inserts is vanishingly rare, so
// the array case is not handled (boundary placement per
// instance would differ).
let composed = nref.xform.then(accum_xform);
let mut sub = Batches::default();
expand_defn(
nested_defn,
&composed,
&inner_ctx,
&mut sub,
visited,
depth + 1,
nested_range,
);
let mut wires = sub.finalize("", ctx.selected, ctx.bg_color);
let world_poly: Vec<[f64; 2]> = cp
let base_composed = nref.xform.then(accum_xform);
let base_translation = transform_translation(&base_composed);
let base_poly: Vec<[f64; 2]> = cp
.iter()
.map(|&[x, y]| {
let w = accum_xform.apply(Vector3::new(x, y, 0.0));
[w.x, w.y]
})
.collect();
crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly);
out.extra_wires.append(&mut wires);
} else {
let mut first: Option<(Vec<WireModel>, [f64; 3])> = None;
for offset in &nref.instance_offsets {
let composed = if offset == &[0.0; 3] {
nref.xform.then(accum_xform)
} else {
let translation = Transform::from_translation(Vector3::new(
offset[0], offset[1], offset[2],
));
translation.then(&nref.xform).then(accum_xform)
};
let composed = composed_for(offset);
let translation = transform_translation(&composed);
if let Some((source, source_translation)) = &first {
let delta = [
translation[0] - source_translation[0],
translation[1] - source_translation[1],
translation[2] - source_translation[2],
];
out.extra_wires.extend(source.iter().map(|wire| {
translated_prototype_wire(wire, "", delta)
}));
continue;
}
let mut sub = Batches::default();
expand_defn(
nested_defn,
&composed,
&inner_ctx,
out,
&mut sub,
visited,
depth + 1,
nested_range,
);
let mut wires = sub.finalize("", ctx.selected, ctx.bg_color);
let poly_delta = [
translation[0] - base_translation[0],
translation[1] - base_translation[1],
];
let world_poly: Vec<[f64; 2]> = base_poly
.iter()
.map(|point| [point[0] + poly_delta[0], point[1] + poly_delta[1]])
.collect();
crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly);
for wire in &mut wires {
wire.render_instance = Some(
crate::scene::model::instance_model::RenderInstance {
source_id: crate::scene::model::instance_model::next_source_id(),
translation,
},
);
}
out.extra_wires.extend(wires.iter().cloned());
first = Some((wires, translation));
}
} else {
for offset in &nref.instance_offsets {
let composed = composed_for(offset);
let translation = transform_translation(&composed);
let key = nested_prototype_key(
&nref.block_name,
&composed,
&inner_ctx,
nested_range.1,
);
if let Some(cached) = out.nested_prototypes.get(&key).cloned() {
let delta = [
translation[0] - cached.translation[0],
translation[1] - cached.translation[1],
translation[2] - cached.translation[2],
];
let depth_delta = nested_range.0 - cached.depth_base;
out.extra_wires.extend(cached.wires.iter().map(|wire| {
let mut wire = translated_prototype_wire(
wire,
"",
delta,
);
if let Some(depth) = wire.depth_override.as_mut() {
*depth += depth_delta;
}
wire
}));
} else {
let mut sub = Batches::default();
expand_defn(
nested_defn,
&composed,
&inner_ctx,
&mut sub,
visited,
depth + 1,
nested_range,
);
let mut wires = sub.finalize("", ctx.selected, ctx.bg_color);
for wire in &mut wires {
if wire.render_instance.is_none() {
wire.render_instance = Some(
crate::scene::model::instance_model::RenderInstance {
source_id: crate::scene::model::instance_model::next_source_id(),
translation,
},
);
}
}
out.extra_wires.extend(wires.iter().cloned());
out.nested_prototypes.insert(
key,
NestedPrototype {
translation,
depth_base: nested_range.0,
wires: Arc::new(wires),
},
);
}
}
}
visited.pop();
+14
View File
@@ -713,6 +713,7 @@ pub(crate) fn tessellate_entity(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris,
pick_tris_low,
dash_from_start: false,
@@ -1071,6 +1072,7 @@ pub(crate) fn tessellate_entity(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -1143,6 +1145,16 @@ pub(crate) fn tessellate_entity(
let polygon =
pick::xclip::world_clip_polygon_for_transform(filter, &transform);
pick::xclip::clip_wires(&mut wires, &polygon);
for wire in &mut wires {
if let Some(mut instance) = wire.render_instance {
instance.source_id = cache.clip_source_id(
instance.source_id,
&polygon,
instance.translation,
);
wire.render_instance = Some(instance);
}
}
if document.header.xclip_frame != 0 && polygon.len() >= 3 {
wires.push(pick::xclip::frame_wire(
&polygon,
@@ -1434,6 +1446,7 @@ fn lod_stub_wire(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -1524,6 +1537,7 @@ fn lod_stub_wire_3d(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
+16
View File
@@ -374,6 +374,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
// MLINE dashes: A-type aligned, but the end-dash length is
@@ -673,6 +674,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -714,6 +716,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -762,6 +765,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -791,6 +795,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -850,6 +855,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -891,6 +897,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -929,6 +936,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -960,6 +968,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -1025,6 +1034,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -1144,6 +1154,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris,
pick_tris_low,
dash_from_start: false,
@@ -1204,6 +1215,7 @@ pub fn tessellate(
fill_tris_low,
fill_is_3d,
fill_is_2d_solid: matches!(entity, EntityType::Solid(_)),
render_instance: None,
depth_override: None,
});
}
@@ -1215,6 +1227,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -1259,6 +1272,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris,
pick_tris_low,
dash_from_start: false,
@@ -1304,6 +1318,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris,
pick_tris_low,
dash_from_start: false,
@@ -1422,6 +1437,7 @@ pub fn tessellate(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris,
pick_tris_low,
dash_from_start: false,
+60
View File
@@ -1099,6 +1099,7 @@ impl Scene {
.with_viewport(viewport);
let mut hatch_block_memo = std::collections::HashMap::new();
let mut models = Vec::new();
let mut hatch_sources = rustc_hash::FxHashMap::default();
graph.walk_root(
self.render_scene_root(layout_block),
|entity, context| {
@@ -1185,6 +1186,35 @@ impl Scene {
if tint_selected && self.selected.contains(&context.root_handle) {
model.color = [0.15, 0.55, 1.00, model.color[3]];
}
let matrix = &context.transform.matrix.m;
let linear = [
matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(),
matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(),
matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(),
];
let key = (
source_hatch.common.handle.value(),
linear,
model
.boundary
.iter()
.flat_map(|point| point.map(f32::to_bits))
.collect::<Vec<_>>(),
model.color.map(f32::to_bits),
model.angle_offset.to_bits(),
model.scale.to_bits(),
model.line_weight_px.to_bits(),
model.aci,
);
let source_id = *hatch_sources
.entry(key)
.or_insert_with(crate::scene::model::instance_model::next_source_id);
model.render_instance = Some(
crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
},
);
models.push(model);
},
);
@@ -1233,6 +1263,7 @@ impl Scene {
depth_map.as_ref(),
);
let mut models = Vec::new();
let mut wipeout_sources = rustc_hash::FxHashMap::default();
graph.walk_root(
self.render_scene_root(target_block),
|entity, context| {
@@ -1279,7 +1310,34 @@ impl Scene {
} else {
bg_color
};
let render_instance = if context.is_instanced() {
let matrix = &context.transform.matrix.m;
let linear = [
matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(),
matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(),
matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(),
];
let key = (
source.common.handle.value(),
linear,
boundary
.iter()
.flat_map(|point| point.map(f32::to_bits))
.collect::<Vec<_>>(),
color.map(f32::to_bits),
);
let source_id = *wipeout_sources.entry(key).or_insert_with(
crate::scene::model::instance_model::next_source_id,
);
Some(crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
})
} else {
None
};
models.push(HatchModel {
render_instance,
boundary: Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
@@ -1622,6 +1680,7 @@ impl Scene {
.collect();
Some(HatchModel {
render_instance: None,
boundary: std::sync::Arc::new(boundary_f32),
boundary_wcs: None,
pattern,
@@ -1926,6 +1985,7 @@ impl Scene {
})
.collect();
HatchModel {
render_instance: None,
boundary: std::sync::Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
+17 -19
View File
@@ -1224,25 +1224,6 @@ fn transform_block_mesh_lod_set(
use acadrust::types::Vector3;
let mut out = set.clone();
out.instance_transform = Some(*xform);
let origin = xform.apply(Vector3::ZERO);
let vectors = [
xform.apply_rotation(Vector3::UNIT_X),
xform.apply_rotation(Vector3::UNIT_Y),
xform.apply_rotation(Vector3::UNIT_Z),
];
out.curved_gens.retain_mut(|generator| {
let transformed = cadkernel::brep::mesh::transform_silhouette_affine(
&generator.source,
vectors.map(|vector| [vector.x, vector.y, vector.z]),
[origin.x, origin.y, origin.z],
);
if let Some(source) = transformed {
generator.source = source;
true
} else {
false
}
});
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
@@ -3214,6 +3195,7 @@ impl Scene {
let ((x0, y0), (x1, y1)) = self.paper_limits()?;
let (x0, y0, x1, y1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
Some(HatchModel {
render_instance: None,
world_origin: [0.0, 0.0],
boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]),
boundary_wcs: None,
@@ -5841,6 +5823,7 @@ impl Scene {
)
.with_viewport(viewport);
let mut models = Vec::new();
let mut image_sources = rustc_hash::FxHashMap::default();
let root = if self.block_edit_block.is_none() {
viewport.map(|viewport| render_graph::SceneRoot::Viewport {
paper_block: self.current_layout_block_handle(),
@@ -5898,6 +5881,21 @@ impl Scene {
(point.z - vertex.pos[2] as f64) as f32,
];
}
let matrix = &context.transform.matrix.m;
let linear = [
matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(),
matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(),
matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(),
];
let source_id = *image_sources
.entry((handle.value(), linear))
.or_insert_with(crate::scene::model::instance_model::next_source_id);
placed.render_instance = Some(
crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
},
);
}
placed.draw_depth = context.draw_depth(handle, depth_map.as_ref());
models.push(placed);
+1
View File
@@ -155,6 +155,7 @@ pub const GPU_BOUNDARY_SEP: f32 = 1.0e30;
/// A hatched region defined by a closed polygon boundary.
#[derive(Clone, Debug)]
pub struct HatchModel {
pub render_instance: Option<super::instance_model::RenderInstance>,
/// World XY anchor (in the same offset-relative coordinate space as
/// the rest of the scene — `world_offset` already subtracted, but
/// kept at f64 precision). Boundary vertices are stored as f32
+4
View File
@@ -82,6 +82,7 @@ fn clip_triangles_px(img: &acadrust::entities::RasterImage) -> Vec<[f64; 2]> {
#[derive(Clone, Debug)]
pub struct ImageModel {
pub render_instance: Option<super::instance_model::RenderInstance>,
/// Original file path (used for reload / display in properties).
pub file_path: String,
/// RGBA8 pixel data in row-major order. Arc-wrapped so cloning ImageModel
@@ -168,6 +169,7 @@ impl ImageModel {
let decoded = resolve_image(&img.file_path)?;
Some(Self {
render_instance: None,
file_path: img.file_path.clone(),
pixels: decoded.pixels,
width: decoded.width,
@@ -278,6 +280,7 @@ impl ImageModel {
.collect();
Some(Self {
render_instance: None,
file_path: def.file_path.clone(),
pixels: raster.pixels.clone(),
width: raster.width,
@@ -334,6 +337,7 @@ impl ImageModel {
let corners_low = [l0, l1, l2, l3];
let verts = quad_verts(&corners, &corners_low);
Some(Self {
render_instance: None,
file_path: "OLE2FRAME".to_string(),
pixels: Arc::new(pixels),
width,
+13
View File
@@ -0,0 +1,13 @@
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_SOURCE_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RenderInstance {
pub source_id: u64,
pub translation: [f64; 3],
}
pub fn next_source_id() -> u64 {
NEXT_SOURCE_ID.fetch_add(1, Ordering::Relaxed)
}
+1
View File
@@ -3,6 +3,7 @@ pub mod gdi;
pub mod hatch_model;
pub mod hatch_patterns;
pub mod image_model;
pub mod instance_model;
pub mod material_model;
pub mod ole_pres;
pub mod pdf_raster;
+4
View File
@@ -156,6 +156,8 @@ pub struct WireModel {
/// `true` only for a planar SOLID entity's interior. Wireframe 3D omits
/// this fill while preserving its perimeter and every other 2-D overlay.
pub fill_is_2d_solid: bool,
/// Shared block-render source and this placement's translation.
pub render_instance: Option<super::instance_model::RenderInstance>,
}
impl WireModel {
@@ -208,6 +210,7 @@ impl WireModel {
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
text_verts: Vec::new(),
@@ -467,6 +470,7 @@ impl Default for WireModel {
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
}
+1
View File
@@ -262,6 +262,7 @@ pub fn frame_wire(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
+176
View File
@@ -85,6 +85,37 @@ pub struct Face3DChunk {
pub vertex_count: u32,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Face3DInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub draw_depth: f32,
pub _pad: [f32; 3],
}
impl Face3DInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, translation) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, translation_low) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(Face3DInstance, draw_depth) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
pub struct BlockFace3DChunk {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub vertex_count: u32,
pub instance_count: u32,
}
pub struct Face3DGpu {
/// 3DFACE quads + PolyfaceMesh / PolygonMesh face triangles.
/// HiddenLine routes this through the depth-only pipeline so the
@@ -95,6 +126,8 @@ pub struct Face3DGpu {
/// source wire has an empty `points` list. Always rendered with the
/// normal face3d pipeline (visible in every mode).
pub chunks_2d: Vec<Face3DChunk>,
pub block_chunks_3d: Vec<BlockFace3DChunk>,
pub block_chunks_2d: Vec<BlockFace3DChunk>,
}
impl Face3DGpu {
@@ -179,6 +212,9 @@ impl Face3DGpu {
if wire.fill_tris.is_empty() {
continue;
}
if wire.render_instance.is_some() {
continue;
}
if !show_2d_solid_fills && wire.fill_is_2d_solid {
continue;
}
@@ -236,13 +272,153 @@ impl Face3DGpu {
}
}
let (block_chunks_3d, block_chunks_2d) = upload_block_chunks(
device,
all_wires,
keep_3d_mesh_fills,
show_2d_solid_fills,
depth_map,
);
Self {
chunks_3d: upload_chunks(device, &verts_3d, "face3d.vbuf.3d"),
chunks_2d: upload_chunks(device, &verts_2d, "face3d.vbuf.2d"),
block_chunks_3d,
block_chunks_2d,
}
}
}
fn upload_block_chunks(
device: &wgpu::Device,
wires: &[WireModel],
keep_3d_mesh_fills: bool,
show_2d_solid_fills: bool,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> (Vec<BlockFace3DChunk>, Vec<BlockFace3DChunk>) {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new();
for wire in wires {
let Some(instance) = wire.render_instance else {
continue;
};
if wire.fill_tris.is_empty()
|| (!show_2d_solid_fills && wire.fill_is_2d_solid)
|| (wire.fill_is_3d && !keep_3d_mesh_fills)
{
continue;
}
let key = (instance.source_id, wire.fill_is_3d);
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push((wire.fill_is_3d, Vec::new()));
slot
});
groups[slot].1.push(wire);
}
let mut chunks_3d = Vec::new();
let mut chunks_2d = Vec::new();
for (is_3d, group) in groups {
let Some(&source) = group.first() else {
continue;
};
let Some(base) = source.render_instance else {
continue;
};
let [r, g, b, a] = source.color;
let color = if is_3d { [r * 0.45, g * 0.45, b * 0.45, a] } else { source.color };
let vertices: Vec<Face3DVertex> = source
.fill_tris
.iter()
.enumerate()
.map(|(index, &position)| Face3DVertex {
position,
color,
draw_depth: 0.0,
position_low: source
.fill_tris_low
.get(index)
.copied()
.unwrap_or([0.0; 3]),
})
.collect();
if vertices.is_empty() {
continue;
}
let budget = (device.limits().max_buffer_size as usize / 10) * 9;
let max_vertices = ((budget / std::mem::size_of::<Face3DVertex>()).max(3) / 3) * 3;
let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9
/ std::mem::size_of::<Face3DInstance>())
.max(1);
let instances: Vec<Face3DInstance> = group
.iter()
.filter_map(|wire| {
let instance = wire.render_instance?;
let delta = [
instance.translation[0] - base.translation[0],
instance.translation[1] - base.translation[1],
instance.translation[2] - base.translation[2],
];
let high = delta.map(|value| value as f32);
let (mut zmin, mut zmax) = (f32::INFINITY, f32::NEG_INFINITY);
for point in &wire.fill_tris {
zmin = zmin.min(point[2]);
zmax = zmax.max(point[2]);
}
let draw_depth = if is_3d || zmax - zmin > 1e-4 {
0.0
} else {
super::wire_gpu::wire_draw_depth(wire, depth_map)
};
Some(Face3DInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
draw_depth,
_pad: [0.0; 3],
})
})
.collect();
let instance_buffers: Vec<(wgpu::Buffer, u32)> = instances
.chunks(max_instances)
.map(|chunk| {
(
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("face3d.block.instances"),
contents: bytemuck::cast_slice(chunk),
usage: wgpu::BufferUsages::VERTEX,
}),
chunk.len() as u32,
)
})
.collect();
for vertex_chunk in vertices.chunks(max_vertices) {
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("face3d.block.vertices"),
contents: bytemuck::cast_slice(vertex_chunk),
usage: wgpu::BufferUsages::VERTEX,
});
for (instance_buffer, instance_count) in &instance_buffers {
let chunk = BlockFace3DChunk {
vertex_buffer: vertex_buffer.clone(),
instance_buffer: instance_buffer.clone(),
vertex_count: vertex_chunk.len() as u32,
instance_count: *instance_count,
};
if is_3d {
chunks_3d.push(chunk);
} else {
chunks_2d.push(chunk);
}
}
}
}
(chunks_3d, chunks_2d)
}
/// Upload `verts` as one or more VERTEX buffers, each under 90% of the
/// device's `max_buffer_size`, split on whole-triangle boundaries. Also
/// keeps every chunk's vertex count well below `u32::MAX` so the draw
+38 -12
View File
@@ -83,10 +83,10 @@ impl HatchGpu {
include_str!("../../../shaders/hatch_texture.wgsl")
})),
});
let vertex_layout = if uses_storage {
storage::HatchVertex::layout()
let vertex_layouts = if uses_storage {
vec![storage::HatchVertex::layout(), storage::HatchPlacement::layout()]
} else {
texture::vertex_layout()
vec![texture::vertex_layout(), texture::TextureHatchPlacement::layout()]
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(if uses_storage {
@@ -98,7 +98,7 @@ impl HatchGpu {
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[vertex_layout],
buffers: &vertex_layouts,
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
@@ -170,11 +170,17 @@ impl HatchGpu {
*resident = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable);
}
HatchBackend::Texture { resident, .. } => {
*resident = hatches
let renderable: Vec<HatchModel> = hatches
.iter()
.filter(|hatch| hatch.boundary.len() >= 3)
.map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout))
.cloned()
.collect();
*resident = TextureHatch::from_models(
device,
queue,
&renderable,
&self.bind_group_layout,
);
}
}
}
@@ -195,11 +201,17 @@ impl HatchGpu {
*preview = StorageHatchBatch::build(device, &self.bind_group_layout, &renderable);
}
HatchBackend::Texture { preview, .. } => {
*preview = hatches
let renderable: Vec<HatchModel> = hatches
.iter()
.filter(|hatch| hatch.boundary.len() >= 3)
.map(|hatch| TextureHatch::new(device, queue, hatch, &self.bind_group_layout))
.cloned()
.collect();
*preview = TextureHatch::from_models(
device,
queue,
&renderable,
&self.bind_group_layout,
);
}
}
}
@@ -218,11 +230,21 @@ impl HatchGpu {
else {
return 0;
};
for index in 0..batch.unique_source_count {
batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index]));
}
for index in batch.unique_source_count..batch.source_visibility.len() {
batch.source_visibility[index] = 1;
}
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
batch.visibility[index] = u32::from(is_visible(aabb));
batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 {
1
} else {
u32::from(is_visible(aabb))
};
}
batch.upload_visibility(queue);
batch.instance_aabbs.len()
batch.instance_aabbs.len() + batch.unique_source_count
}
pub fn draw<'pass>(
@@ -239,14 +261,18 @@ impl HatchGpu {
for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() {
pass.set_bind_group(1, &batch.bind_group, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.draw(0..batch.vertex_count, 0..1);
pass.set_vertex_buffer(1, batch.placement_buffer.slice(..));
for (vertices, instances) in &batch.draws {
pass.draw(vertices.clone(), instances.clone());
}
}
}
HatchBackend::Texture { resident, preview } => {
for hatch in resident.iter().chain(preview) {
pass.set_bind_group(1, &hatch.bind_group, &[]);
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
pass.draw(0..6, 0..1);
pass.set_vertex_buffer(1, hatch.placement_buffer.slice(..));
pass.draw(0..6, 0..hatch.instance_count);
}
}
}
+129 -24
View File
@@ -152,6 +152,30 @@ impl HatchVertex {
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct HatchPlacement {
pub translation: [f32; 2],
pub translation_low: [f32; 2],
pub draw_depth: f32,
pub visible: u32,
}
impl HatchPlacement {
pub(super) fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 3, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 4, format: wgpu::VertexFormat::Float32 },
wgpu::VertexAttribute { offset: 20, shader_location: 5, format: wgpu::VertexFormat::Uint32 },
],
}
}
}
// ── Batch builder ──────────────────────────────────────────────────────────
/// Pack a list of `HatchModel`s into the four concatenated storage
@@ -160,7 +184,8 @@ impl HatchVertex {
/// hatch render pass entirely).
pub(super) struct StorageHatchBatch {
pub vertex_buffer: wgpu::Buffer,
pub vertex_count: u32,
pub placement_buffer: wgpu::Buffer,
pub draws: Vec<(std::ops::Range<u32>, std::ops::Range<u32>)>,
// The four storage buffers below are referenced via `bind_group` —
// dropping them would invalidate it, but the bind group is the
// only direct consumer. Keep them as fields to keep ownership in
@@ -175,14 +200,17 @@ pub(super) struct StorageHatchBatch {
/// `visibility[instance_index]` — when 0 it emits an out-of-NDC
/// clip position so the GPU clips the primitive before the
/// fragment stage runs.
pub visibility_buffer: wgpu::Buffer,
#[allow(dead_code)] pub visibility_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
#[allow(dead_code)] pub instance_count: u32,
/// CPU mirror — `update_visibility` re-uploads this whole slice
/// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches
/// per pan tick. Far cheaper than touching the 128 B-per-instance
/// data.
pub visibility: Vec<u32>,
pub placements: Vec<HatchPlacement>,
pub source_visibility: Vec<u32>,
pub source_aabbs: Vec<[f32; 4]>,
pub unique_source_count: usize,
/// CPU-side mirror of each instance's local-space AABB (world-
/// offset-subtracted, world_origin already added back). Used by
/// `compute_hatch_lod` to evaluate the sub-pixel + frustum cull
@@ -208,8 +236,25 @@ impl StorageHatchBatch {
let mut boundary: Vec<[f32; 4]> = Vec::new();
let mut families: Vec<LineFamilyGpu> = Vec::new();
let mut dashes: Vec<f32> = Vec::new();
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
for (index, hatch) in hatches.iter().enumerate() {
let key = hatch
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(hatch);
}
groups.sort_by_key(|group| std::cmp::Reverse(group.len() == 1));
let unique_source_count = groups.iter().take_while(|group| group.len() == 1).count();
for h in hatches {
for group in &groups {
let h = group[0];
let boundary_offset = boundary.len() as u32;
for &[x, y] in h.boundary.iter() {
// NaN sub-loop separators become the finite sentinel on the
@@ -325,7 +370,7 @@ impl StorageHatchBatch {
boundary_count,
family_offset,
family_count,
draw_depth: h.draw_depth,
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
poly_test: 1,
grad_kind,
_pad2: 0,
@@ -368,7 +413,7 @@ impl StorageHatchBatch {
boundary_count,
family_offset,
family_count,
draw_depth: h.draw_depth,
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
// Always resolve inside/outside per fragment on the GPU (the
// boundary is uploaded below). Dropping CPU pre-triangulation
// keeps the vertex buffer at 6 verts/hatch regardless of
@@ -406,12 +451,71 @@ impl StorageHatchBatch {
verts.push(HatchVertex { local_xy: c, instance_index: i as u32 });
}
}
let mut placements = Vec::new();
let mut draws = Vec::with_capacity(groups.len());
let mut instance_aabbs = Vec::new();
if unique_source_count > 0 {
placements.push(HatchPlacement {
translation: [0.0; 2],
translation_low: [0.0; 2],
draw_depth: 0.0,
visible: 1,
});
draws.push((0..unique_source_count as u32 * 6, 0..1));
let mut union = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
for inst in instances.iter().take(unique_source_count) {
union[0] = union[0].min(inst.aabb[0] + inst.world_origin[0]);
union[1] = union[1].min(inst.aabb[1] + inst.world_origin[1]);
union[2] = union[2].max(inst.aabb[2] + inst.world_origin[0]);
union[3] = union[3].max(inst.aabb[3] + inst.world_origin[1]);
}
instance_aabbs.push(union);
}
for (source_index, group) in groups.iter().enumerate().skip(unique_source_count) {
let source = group[0];
let base = source
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let placement_start = placements.len() as u32;
for hatch in group {
let translation = hatch
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [translation[0] - base[0], translation[1] - base[1]];
let high = [delta[0] as f32, delta[1] as f32];
placements.push(HatchPlacement {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
],
draw_depth: hatch.draw_depth,
visible: 1,
});
let inst = &instances[source_index];
instance_aabbs.push([
inst.aabb[0] + inst.world_origin[0] + high[0],
inst.aabb[1] + inst.world_origin[1] + high[1],
inst.aabb[2] + inst.world_origin[0] + high[0],
inst.aabb[3] + inst.world_origin[1] + high[1],
]);
}
draws.push((
source_index as u32 * 6..source_index as u32 * 6 + 6,
placement_start..placements.len() as u32,
));
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.vertex"),
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.placements"),
contents: bytemuck::cast_slice(&placements),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.instances"),
contents: bytemuck::cast_slice(&instances),
@@ -466,27 +570,20 @@ impl StorageHatchBatch {
},
],
});
// CPU AABB mirror — local-space rect with world_origin added
// back, ready for `aabb_offscreen` / `aabb_below_pixel` to
// compare against the camera view_proj.
let instance_aabbs: Vec<[f32; 4]> = instances
let source_aabbs = instances
.iter()
.map(|i| {
let ox = i.world_origin[0];
let oy = i.world_origin[1];
[
i.aabb[0] + ox,
i.aabb[1] + oy,
i.aabb[2] + ox,
i.aabb[3] + oy,
]
})
.map(|inst| [
inst.aabb[0] + inst.world_origin[0],
inst.aabb[1] + inst.world_origin[1],
inst.aabb[2] + inst.world_origin[0],
inst.aabb[3] + inst.world_origin[1],
])
.collect();
Some(Self {
vertex_buffer,
vertex_count: verts.len() as u32,
placement_buffer,
draws,
instance_buffer,
boundary_buffer,
family_buffer,
@@ -494,7 +591,10 @@ impl StorageHatchBatch {
visibility_buffer,
bind_group,
instance_count: instances.len() as u32,
visibility,
placements,
source_visibility: visibility,
source_aabbs,
unique_source_count,
instance_aabbs,
})
}
@@ -502,10 +602,15 @@ impl StorageHatchBatch {
/// Push the CPU `visibility` slice to GPU. Call when any
/// element changes (typically per-frame from compute_hatch_lod).
pub(super) fn upload_visibility(&self, queue: &wgpu::Queue) {
queue.write_buffer(
&self.placement_buffer,
0,
bytemuck::cast_slice(&self.placements),
);
queue.write_buffer(
&self.visibility_buffer,
0,
bytemuck::cast_slice(&self.visibility),
bytemuck::cast_slice(&self.source_visibility),
);
}
+82 -1
View File
@@ -39,6 +39,29 @@ pub(super) fn vertex_layout<'a>() -> wgpu::VertexBufferLayout<'a> {
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct TextureHatchPlacement {
translation: [f32; 2],
translation_low: [f32; 2],
draw_depth: f32,
_pad: [f32; 3],
}
impl TextureHatchPlacement {
pub(super) fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32 },
],
}
}
}
// ── Per-hatch uniform (binding 0) — 96 bytes, matches HatchUniforms in
// hatch_texture.wgsl. ──────────────────────────────────────────────────────
@@ -67,6 +90,8 @@ struct TextureHatchUniform {
pub(super) struct TextureHatch {
pub(super) vertex_buffer: wgpu::Buffer,
pub(super) placement_buffer: wgpu::Buffer,
pub(super) instance_count: u32,
pub(super) bind_group: wgpu::BindGroup,
/// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired
/// into the web hatch draw loop — the native batched path doesn't do
@@ -78,6 +103,32 @@ pub(super) struct TextureHatch {
}
impl TextureHatch {
pub(super) fn from_models(
device: &wgpu::Device,
queue: &wgpu::Queue,
models: &[HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Vec<Self> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
for (index, model) in models.iter().enumerate() {
let key = model
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(model);
}
groups
.into_iter()
.map(|group| Self::new(device, queue, &group, bgl1))
.collect()
}
/// Group-1 layout: uniform header (binding 0) + non-filterable float data
/// texture (binding 1).
pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
@@ -111,9 +162,10 @@ impl TextureHatch {
pub(super) fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
model: &HatchModel,
models: &[&HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Self {
let model = models[0];
// ── Decode pattern mode (mirrors WipeoutGpu::new) ─────────────────
// The gradient shape (kind + invert bit) rides in the mode's high
// bits — the 96-byte uniform has no spare slot.
@@ -180,6 +232,33 @@ impl TextureHatch {
contents: bytemuck::cast_slice(&quad),
usage: wgpu::BufferUsages::VERTEX,
});
let base = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let placements: Vec<TextureHatchPlacement> = models
.iter()
.map(|model| {
let translation = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [translation[0] - base[0], translation[1] - base[1]];
let high = [delta[0] as f32, delta[1] as f32];
TextureHatchPlacement {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
],
draw_depth: model.draw_depth,
_pad: [0.0; 3],
}
})
.collect();
let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.texture.placements"),
contents: bytemuck::cast_slice(&placements),
usage: wgpu::BufferUsages::VERTEX,
});
// ── Gradient projection range (snapped-local space) ───────────────
let (grad_min, grad_range) = if mode == 2 {
@@ -356,6 +435,8 @@ impl TextureHatch {
Self {
vertex_buffer,
placement_buffer,
instance_count: placements.len() as u32,
bind_group,
world_aabb,
_uniform_buf,
+90 -3
View File
@@ -46,6 +46,30 @@ impl ImageVertex {
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ImageInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub draw_depth: f32,
pub _pad: [f32; 3],
}
impl ImageInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, translation) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, translation_low) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(ImageInstance, draw_depth) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
// ── Uniform ───────────────────────────────────────────────────────────────
#[repr(C)]
@@ -62,9 +86,11 @@ struct ImageParams {
pub struct ImageGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
/// Number of triangle vertices in `vertex_buffer` — 6 for a plain quad, or
/// more when the raster is clipped to a triangulated polygon.
pub vertex_count: u32,
pub instance_count: u32,
pub bind_group: wgpu::BindGroup,
_texture: wgpu::Texture,
_sampler: wgpu::Sampler,
@@ -72,12 +98,39 @@ pub struct ImageGpu {
}
impl ImageGpu {
pub fn new(
pub fn from_models(
device: &wgpu::Device,
queue: &wgpu::Queue,
model: &ImageModel,
models: &[ImageModel],
bgl1: &wgpu::BindGroupLayout,
) -> Vec<Self> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&ImageModel>> = Vec::new();
for (index, model) in models.iter().enumerate() {
let key = model
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(model);
}
groups
.into_iter()
.filter_map(|group| Self::new(device, queue, &group, bgl1))
.collect()
}
fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
models: &[&ImageModel],
bgl1: &wgpu::BindGroupLayout,
) -> Option<Self> {
let &model = models.first()?;
if model.pixels.is_empty() || model.width == 0 || model.height == 0 {
return None;
}
@@ -129,7 +182,7 @@ impl ImageGpu {
// ── Opacity uniform ───────────────────────────────────────────────
let params = ImageParams {
opacity: model.opacity.clamp(0.0, 1.0),
draw_depth: model.draw_depth,
draw_depth: 0.0,
_pad: [0.0; 2],
};
let _params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -179,10 +232,44 @@ impl ImageGpu {
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let base = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let instances: Vec<ImageInstance> = models
.iter()
.map(|model| {
let translation = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [
translation[0] - base[0],
translation[1] - base[1],
translation[2] - base[2],
];
let high = delta.map(|value| value as f32);
ImageInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
draw_depth: model.draw_depth,
_pad: [0.0; 3],
}
})
.collect();
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image.instances"),
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::VERTEX,
});
Some(Self {
vertex_buffer,
instance_buffer,
vertex_count,
instance_count: instances.len() as u32,
bind_group,
_texture: texture,
_sampler,
File diff suppressed because it is too large. Load diff
+709 -109
View File
File diff suppressed because it is too large. Load diff
+189 -1
View File
@@ -68,6 +68,37 @@ impl TextVertex {
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BlockTextInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub draw_depth: f32,
pub _pad: [f32; 3],
}
impl BlockTextInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, translation) as u64, shader_location: 5, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, translation_low) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockTextInstance, draw_depth) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
pub struct BlockTextGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub vertex_count: u32,
pub instance_count: u32,
}
/// Split an f64 into the double-single (high f32, low residual f32) pair the
/// shaders reconstruct relative-to-eye.
pub(crate) fn split_ds(v: f64) -> (f32, f32) {
@@ -261,7 +292,12 @@ pub fn create_pipelines(
color_format: wgpu::TextureFormat,
sample_count: u32,
content_stencil: &wgpu::StencilState,
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
) -> (
wgpu::RenderPipeline,
wgpu::RenderPipeline,
wgpu::RenderPipeline,
wgpu::RenderPipeline,
) {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()),
@@ -311,6 +347,50 @@ pub fn create_pipelines(
cache: None,
})
};
let block_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("block_text.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/block_text.wgsl").into()),
});
let create_block = |label, depth_write_enabled, depth_compare| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &block_shader,
entry_point: Some("vs_main"),
buffers: &[TextVertex::layout(), BlockTextInstance::layout()],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &block_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,
depth_compare,
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,
})
};
(
create(
"text.pipeline",
@@ -322,9 +402,117 @@ pub fn create_pipelines(
Some(false),
Some(wgpu::CompareFunction::Always),
),
create_block(
"block_text.pipeline",
Some(true),
Some(wgpu::CompareFunction::LessEqual),
),
create_block(
"block_text.highlight.pipeline",
Some(false),
Some(wgpu::CompareFunction::Always),
),
)
}
pub fn upload_block_vertices(
device: &wgpu::Device,
wires: &[crate::scene::model::wire_model::WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Vec<BlockTextGpu> {
let refs: Vec<&crate::scene::model::wire_model::WireModel> = wires.iter().collect();
upload_block_vertex_refs(device, &refs, depth_map, None)
}
pub fn upload_block_vertex_refs(
device: &wgpu::Device,
wires: &[&crate::scene::model::wire_model::WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
tint: Option<[f32; 4]>,
) -> Vec<BlockTextGpu> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&crate::scene::model::wire_model::WireModel>> = Vec::new();
for &wire in wires {
let Some(instance) = wire.render_instance else {
continue;
};
if wire.text_verts.is_empty() {
continue;
}
let slot = *slots.entry(instance.source_id).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(wire);
}
let mut out = Vec::new();
for group in groups {
let Some(&source) = group.first() else {
continue;
};
let Some(base) = source.render_instance else {
continue;
};
let tinted;
let vertices = if let Some(tint) = tint {
tinted = source
.text_verts
.iter()
.map(|vertex| TextVertex {
color: [tint[0], tint[1], tint[2], vertex.color[3]],
..*vertex
})
.collect::<Vec<_>>();
tinted.as_slice()
} else {
source.text_verts.as_slice()
};
let Some(vertex_buffer) = upload_vertices(device, vertices) else {
continue;
};
let instances: Vec<BlockTextInstance> = group
.iter()
.filter_map(|wire| {
let instance = wire.render_instance?;
let delta = [
instance.translation[0] - base.translation[0],
instance.translation[1] - base.translation[1],
instance.translation[2] - base.translation[2],
];
let high = delta.map(|value| value as f32);
Some(BlockTextInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
draw_depth: super::wire_gpu::wire_draw_depth(wire, depth_map),
_pad: [0.0; 3],
})
})
.collect();
let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9
/ std::mem::size_of::<BlockTextInstance>())
.max(1);
for chunk in instances.chunks(max_instances) {
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_text.instances"),
contents: bytemuck::cast_slice(chunk),
usage: wgpu::BufferUsages::VERTEX,
});
out.push(BlockTextGpu {
vertex_buffer: vertex_buffer.clone(),
instance_buffer,
vertex_count: vertices.len() as u32,
instance_count: chunk.len() as u32,
});
}
}
out
}
/// Upload a finished vertex list; `None` if empty.
pub fn upload_vertices(device: &wgpu::Device, verts: &[TextVertex]) -> Option<wgpu::Buffer> {
if verts.is_empty() {
+90 -3
View File
@@ -45,6 +45,29 @@ impl HatchVertex {
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct WipeoutPlacement {
pub translation: [f32; 2],
pub translation_low: [f32; 2],
pub draw_depth: f32,
pub _pad: [f32; 3],
}
impl WipeoutPlacement {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32 },
],
}
}
}
// ── Uniform structs ───────────────────────────────────────────────────────
/// Per-hatch parameters (binding 0). Must be 96 bytes (16-byte aligned).
@@ -120,6 +143,8 @@ pub struct FamilyBatchData {
pub struct WipeoutGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub instance_count: u32,
pub bind_group: wgpu::BindGroup,
/// World-space XY bounding rect [min_x, min_y, max_x, max_y] of the
/// boundary polygon. Used by the per-frame LOD pass to skip hatches
@@ -131,7 +156,33 @@ pub struct WipeoutGpu {
}
impl WipeoutGpu {
pub fn new(device: &wgpu::Device, model: &HatchModel, bgl1: &wgpu::BindGroupLayout) -> Self {
pub fn from_models(
device: &wgpu::Device,
models: &[HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Vec<Self> {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
for (index, model) in models.iter().enumerate() {
let key = model
.render_instance
.map(|instance| (true, instance.source_id))
.unwrap_or((false, index as u64));
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push(Vec::new());
slot
});
groups[slot].push(model);
}
groups
.into_iter()
.map(|group| Self::new(device, &group, bgl1))
.collect()
}
fn new(device: &wgpu::Device, models: &[&HatchModel], bgl1: &wgpu::BindGroupLayout) -> Self {
let model = models[0];
// ── Decode pattern mode ──────────────────────────────────────────
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
@@ -300,7 +351,7 @@ impl WipeoutGpu {
(origin[0] - origin[0] as f32 as f64) as f32,
(origin[1] - origin[1] as f32 as f64) as f32,
],
draw_depth: model.draw_depth,
draw_depth: 0.0,
_pad: [0.0; 3],
};
@@ -373,16 +424,52 @@ impl WipeoutGpu {
// is in that absolute local space rather than relative to the
// anchor. (Sub-pixel LOD via `aabb_below_pixel` is unaffected:
// it measures a projected diagonal, invariant under translation.)
let base = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let placements: Vec<WipeoutPlacement> = models
.iter()
.map(|model| {
let translation = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
let delta = [translation[0] - base[0], translation[1] - base[1]];
let high = [delta[0] as f32, delta[1] as f32];
WipeoutPlacement {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
],
draw_depth: model.draw_depth,
_pad: [0.0; 3],
}
})
.collect();
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("wipeout.instances"),
contents: bytemuck::cast_slice(&placements),
usage: wgpu::BufferUsages::VERTEX,
});
let ox = model.world_origin[0] as f32;
let oy = model.world_origin[1] as f32;
let world_aabb = if min_x.is_finite() && min_y.is_finite() {
[min_x + ox, min_y + oy, max_x + ox, max_y + oy]
let mut aabb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
for placement in &placements {
aabb[0] = aabb[0].min(min_x + ox + placement.translation[0]);
aabb[1] = aabb[1].min(min_y + oy + placement.translation[1]);
aabb[2] = aabb[2].max(max_x + ox + placement.translation[0]);
aabb[3] = aabb[3].max(max_y + oy + placement.translation[1]);
}
aabb
} else {
[min_x, min_y, max_x, max_y]
};
Self {
vertex_buffer,
instance_buffer,
instance_count: placements.len() as u32,
bind_group,
world_aabb,
_uniform_buf,
+205
View File
@@ -285,6 +285,84 @@ pub struct WireGpu {
pub const_bind_group: Option<std::sync::Arc<wgpu::BindGroup>>,
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BlockWireVertex {
pub pos_a: [f32; 3],
pub pos_a_low: [f32; 3],
pub pos_b: [f32; 3],
pub pos_b_low: [f32; 3],
pub distances: [f32; 2],
pub taper_ratio: [u16; 2],
}
impl BlockWireVertex {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_a) as u64, shader_location: 0, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_b) as u64, shader_location: 1, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_a_low) as u64, shader_location: 2, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, pos_b_low) as u64, shader_location: 3, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, distances) as u64, shader_location: 4, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireVertex, taper_ratio) as u64, shader_location: 5, format: wgpu::VertexFormat::Unorm16x2 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: ATTRS,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BlockWireInstance {
pub translation: [f32; 3],
pub translation_low: [f32; 3],
pub depth: [f32; 2],
}
impl BlockWireInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
const ATTRS: &[wgpu::VertexAttribute] = &[
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, translation) as u64, shader_location: 6, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, translation_low) as u64, shader_location: 7, format: wgpu::VertexFormat::Float32x3 },
wgpu::VertexAttribute { offset: std::mem::offset_of!(BlockWireInstance, depth) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x2 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: ATTRS,
}
}
}
#[derive(Clone)]
pub struct BlockWireGpu {
pub vertex_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub vertex_count: u32,
pub instance_count: u32,
pub is_3d_mesh_edge: bool,
pub const_bind_group: std::sync::Arc<wgpu::BindGroup>,
}
pub fn block_const_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("block_wire_const.bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
})
}
/// Expand one `WireModel` into its per-segment instance stream (1 instance per
/// finite segment). Pulled out so both the single-wire and batched paths share
/// the same emission logic, and so the batched path can `par_iter` across
@@ -563,6 +641,133 @@ pub(crate) fn wire_draw_depth(
}
}
impl BlockWireGpu {
pub fn from_wires(
device: &wgpu::Device,
wires: &[&WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
mesh_names: &rustc_hash::FxHashSet<&str>,
color_override: Option<[f32; 4]>,
const_bgl: &wgpu::BindGroupLayout,
) -> Vec<Self> {
use wgpu::util::DeviceExt;
let mut slots: rustc_hash::FxHashMap<(u64, bool), usize> =
rustc_hash::FxHashMap::default();
let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new();
for &wire in wires {
let Some(instance) = wire.render_instance else {
continue;
};
if wire.points.len() < 2 {
continue;
}
let mesh_edge = mesh_names.contains(wire.name.as_str());
let key = (instance.source_id, mesh_edge);
let slot = *slots.entry(key).or_insert_with(|| {
let slot = groups.len();
groups.push((mesh_edge, Vec::new()));
slot
});
groups[slot].1.push(wire);
}
let mut out = Vec::new();
for (mesh_edge, group) in groups {
let Some(&source) = group.first() else {
continue;
};
let Some(base) = source.render_instance else {
continue;
};
let color = color_override.unwrap_or(source.color);
let (segments, mut constant) = emit_wire_native(source, 0, color, 0.0);
if segments.is_empty() {
continue;
}
constant.draw_depth = 0.0;
let mut vertices = Vec::with_capacity(segments.len() * 6);
for segment in segments {
let vertex = BlockWireVertex {
pos_a: segment.pos_a,
pos_a_low: segment.pos_a_low,
pos_b: segment.pos_b,
pos_b_low: segment.pos_b_low,
distances: [segment.distance_a, segment.distance_b],
taper_ratio: segment.taper_ratio,
};
vertices.extend_from_slice(&[vertex; 6]);
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_wire.vertices"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let const_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_wire.const"),
contents: bytemuck::bytes_of(&constant),
usage: wgpu::BufferUsages::UNIFORM,
});
let const_bind_group = std::sync::Arc::new(device.create_bind_group(
&wgpu::BindGroupDescriptor {
label: Some("block_wire.const.bg"),
layout: const_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: const_buffer.as_entire_binding(),
}],
},
));
let instances: Vec<BlockWireInstance> = group
.iter()
.filter_map(|wire| {
let instance = wire.render_instance?;
let delta = [
instance.translation[0] - base.translation[0],
instance.translation[1] - base.translation[1],
instance.translation[2] - base.translation[2],
];
let high = delta.map(|value| value as f32);
Some(BlockWireInstance {
translation: high,
translation_low: [
(delta[0] - high[0] as f64) as f32,
(delta[1] - high[1] as f64) as f32,
(delta[2] - high[2] as f64) as f32,
],
depth: [
if mesh_edge {
0.0
} else {
wire_draw_depth(wire, depth_map)
},
0.0,
],
})
})
.collect();
let max_instances = ((device.limits().max_buffer_size as usize / 10) * 9
/ std::mem::size_of::<BlockWireInstance>())
.max(1);
for chunk in instances.chunks(max_instances) {
out.push(Self {
vertex_buffer: vertex_buffer.clone(),
instance_buffer: instance_buffer_mapped(
device,
"block_wire.instances",
chunk,
),
vertex_count: vertices.len() as u32,
instance_count: chunk.len() as u32,
is_3d_mesh_edge: mesh_edge,
const_bind_group: const_bind_group.clone(),
});
}
}
out
}
}
/// Build the shared per-wire `WireConst` storage buffer and its bind group
/// (storage path). All chunks from one build reference the same
/// buffer via their global `wire_id`, so a single bind group is cloned into
+1
View File
@@ -150,6 +150,7 @@ impl Scene {
return None;
}
let mut model = HatchModel {
render_instance: None,
world_origin: origin,
boundary: std::sync::Arc::new(boundary),
boundary_wcs: None,
+2
View File
@@ -314,6 +314,7 @@ pub fn apply_along(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
@@ -357,6 +358,7 @@ pub fn apply_along(
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
+48 -12
View File
@@ -389,6 +389,7 @@ impl shader::Primitive for Primitive {
inner.hatch_lod_key = (usize::MAX, u64::MAX, 0, 0, false);
inner.wipeout_lod_key = (usize::MAX, u64::MAX, 0, 0, false);
inner.mesh_lod_key = (usize::MAX, u64::MAX, 0, 0);
inner.silhouette_key = (usize::MAX, u64::MAX, u64::MAX, false);
inner.render_sig = u64::MAX;
}
// The MSAA / depth / resolve textures are always sized to the
@@ -463,9 +464,13 @@ impl shader::Primitive for Primitive {
continue;
}
let Some(vp_wires) = vp.wires.upgrade() else {
inner.render_sig = u64::MAX;
inner.skip_geometry = true;
continue;
};
let Some(draw_depths) = vp.draw_depths.upgrade() else {
inner.render_sig = u64::MAX;
inner.skip_geometry = true;
continue;
};
// Third component is the *selected-set* signature (not
@@ -531,8 +536,15 @@ impl shader::Primitive for Primitive {
.cached_text_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.text_verts))
|| vp.wire_content_id != inner.cached_wire_id
{
inner.upload_text(device, queue, &vp.text_verts[..]);
inner.upload_text(
device,
queue,
&vp.text_verts[..],
&vp_wires[..],
&draw_depths,
);
inner.cached_text_source = Some(Arc::clone(&vp.text_verts));
}
inner.cached_fill_mode = fill_mode;
@@ -607,7 +619,8 @@ impl shader::Primitive for Primitive {
&& (inner.wire_const_bgl.is_some()
|| ((vp.wire_patch.is_some()
|| inner.wire_arena_id != u64::MAX)
&& packed_arena_owner));
&& packed_arena_owner))
&& !vp_wires.iter().any(|wire| wire.render_instance.is_some());
if use_wire_arena {
use crate::scene::pipeline::wire_arena::{
self, PersistentWireArena as WireArena,
@@ -824,6 +837,7 @@ impl shader::Primitive for Primitive {
gpus.extend(arena.wire_gpus());
}
inner.gpu_wires = std::sync::Arc::new(gpus);
inner.gpu_block_wires = std::sync::Arc::new(Vec::new());
if _patched {
wire_arena::patch_handle_index(
&mut inner.wire_handle_index,
@@ -881,17 +895,26 @@ impl shader::Primitive for Primitive {
if pipeline.wire_buffer_cache.len() > 16 {
pipeline
.wire_buffer_cache
.retain(|_, (w, _)| std::sync::Arc::strong_count(w) > 1);
.retain(|_, (w, b, _)| {
std::sync::Arc::strong_count(w) > 1
|| std::sync::Arc::strong_count(b) > 1
});
}
entry
}
};
inner.gpu_wires = built.0;
inner.wire_handle_index = built.1;
inner.gpu_block_wires = built.1;
inner.wire_handle_index = built.2;
} // end !arena_served
inner.cached_wire_id = vp.wire_content_id;
if _perf {
let gi: u32 = inner.gpu_wires.iter().map(|w| w.instance_count).sum();
let bi: u32 = inner
.gpu_block_wires
.iter()
.map(|w| w.instance_count)
.sum();
let outcome = if !arena_served {
"shared-fullupload"
} else if _patched {
@@ -902,11 +925,12 @@ impl shader::Primitive for Primitive {
"arena-build"
};
crate::perf_record!(
"[perf] wire {:>7.1}ms {:<18} wires={} gpu_instances={}",
"[perf] wire {:>7.1}ms {:<18} wires={} gpu_instances={} block_instances={}",
_t0.elapsed().as_secs_f64() * 1000.0,
outcome,
vp_wires.len(),
gi,
bi,
);
}
}
@@ -953,6 +977,7 @@ impl shader::Primitive for Primitive {
&vp.selected_handles,
&vp.hover_handles,
&vp.annotation_context_wires,
&draw_depths,
);
inner.cached_annotation_highlight_source =
Some(Arc::clone(&vp.annotation_context_wires));
@@ -1007,13 +1032,21 @@ impl shader::Primitive for Primitive {
vp.uniforms.eye_high[1] as f64 + vp.uniforms.eye_low[1] as f64,
vp.uniforms.eye_high[2] as f64 + vp.uniforms.eye_low[2] as f64,
);
// Rebuild view-dependent silhouettes each frame when requested by
// DISPSILH or by a visual style such as HiddenLine. Only modes that
// draw edges consume them — pure shaded hides them.
if vp.display_silhouette && (vp.view_wireframe || vp.show_3d_edges) {
inner.upload_silhouettes(device, &vp.meshes[..], vp.view_dir);
} else {
inner.upload_silhouettes(device, &[], vp.view_dir);
let silhouette_enabled =
vp.display_silhouette && (vp.view_wireframe || vp.show_3d_edges);
let silhouette_key = (
Arc::as_ptr(&vp.meshes) as usize,
vp.wire_content_id,
vp.camera_generation,
silhouette_enabled,
);
if inner.silhouette_key != silhouette_key {
inner.upload_silhouettes(
device,
if silhouette_enabled { &vp.meshes[..] } else { &[] },
vp.view_dir,
);
inner.silhouette_key = silhouette_key;
}
inner.upload_clip_boundary(device, &vp.clip_boundary_ndc);
let hatch_lod_key = (
@@ -2832,6 +2865,9 @@ impl Scene {
}
let mut out: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
for w in wires {
if w.render_instance.is_some() {
continue;
}
if !w.text_verts.is_empty() {
// Bake the host wire's draw-order depth into its glyphs so
// text layers like the rest of the entity: its own background