Display tessellation plans (4/4): one GPU dispatch for the scene, intersection edges by Newton (#806)
Packs the scene's planned bodies into one GPU upload and dispatch, and evaluates intersection edges by seeded Newton refinement from a fitted B-spline through exactly solved leader points, falling back to the previous evaluation near tangency. Intersection nodes carry their faces' parameters. This changes speed, not what the app draws. Part 4 of 4 of #303. Co-authored-by: jyviko <[email protected]>
This commit is contained in:
1 parent
c1c813e572
commit
587cf865b8
9 files changed
+612
-106
No files matched your search
+70
-41
@@ -50,13 +50,22 @@ pub struct GpuMesh {
|
||||
pub indices: wgpu::Buffer,
|
||||
pub index_count: u32,
|
||||
pub vertex_count: u32,
|
||||
/// Each vertex's face (the plan's face numbering), for colouring by face.
|
||||
/// Each vertex's face (its plan's face numbering), for colouring by face.
|
||||
pub vertex_face: Vec<u32>,
|
||||
/// Per plan (in the order given), its vertices and indices.
|
||||
pub parts: Vec<Part>,
|
||||
bind: wgpu::BindGroup,
|
||||
// The inputs, kept until the evaluation has run.
|
||||
_inputs: [wgpu::Buffer; 4],
|
||||
}
|
||||
|
||||
/// One plan's share of a [`GpuMesh`]: its vertices and its entries in the index buffer.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Part {
|
||||
pub vertices: std::ops::Range<u32>,
|
||||
pub indices: std::ops::Range<u32>,
|
||||
}
|
||||
|
||||
/// The compute pipeline, ready to evaluate plans on one device.
|
||||
pub struct TessGpu {
|
||||
adapter: String,
|
||||
@@ -144,7 +153,7 @@ impl TessGpu {
|
||||
/// and normals in f32, fixed edge nodes exact). Blocks until the GPU is done (native).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn evaluate(&self, plan: &TessPlan) -> Result<Mesh> {
|
||||
let packed = pack(plan)?;
|
||||
let packed = pack(&[plan])?;
|
||||
let mut mesh = Mesh::default();
|
||||
let mut base = 0usize;
|
||||
for chunk in packed.vertices.chunks(MAX_BATCH * VERTEX_WORDS) {
|
||||
@@ -186,9 +195,16 @@ impl TessGpu {
|
||||
/// [`TessGpu::record`]. Refused (so the caller draws its CPU mesh) for a plan with more than
|
||||
/// [`MAX_BATCH`] vertices or a face without an evaluable surface.
|
||||
pub fn prepare(&self, plan: &TessPlan) -> Result<GpuMesh> {
|
||||
let packed = pack(plan)?;
|
||||
self.prepare_many(&[plan])
|
||||
}
|
||||
|
||||
/// Like [`TessGpu::prepare`] for several plans at once: one upload, one buffer and one
|
||||
/// dispatch for all of them (a viewport's whole scene), each plan's vertices and triangles in
|
||||
/// [`GpuMesh::parts`]. Refused past [`MAX_BATCH`] vertices in all.
|
||||
pub fn prepare_many(&self, plans: &[&TessPlan]) -> Result<GpuMesh> {
|
||||
let packed = pack(plans)?;
|
||||
if packed.count > MAX_BATCH {
|
||||
return Err(GpuError::Unsupported(format!("a plan of {} vertices on the GPU", packed.count)));
|
||||
return Err(GpuError::Unsupported(format!("{} vertices in one dispatch", packed.count)));
|
||||
}
|
||||
let d = &self.device;
|
||||
let count = word(packed.count)?;
|
||||
@@ -211,14 +227,19 @@ impl TessGpu {
|
||||
});
|
||||
let mut tri: Vec<u32> = Vec::new();
|
||||
let mut vertex_face = Vec::with_capacity(packed.count);
|
||||
let mut parts = Vec::with_capacity(plans.len());
|
||||
let mut offset = 0u32;
|
||||
for f in &plan.faces {
|
||||
let n = word(f.uv.len())?;
|
||||
for t in f.triangles.iter().filter(|t| t.iter().all(|&i| i < n)) {
|
||||
tri.extend([offset + t[0], offset + t[1], offset + t[2]]);
|
||||
for plan in plans {
|
||||
let (v0, i0) = (offset, word(tri.len())?);
|
||||
for f in &plan.faces {
|
||||
let n = word(f.uv.len())?;
|
||||
for t in f.triangles.iter().filter(|t| t.iter().all(|&i| i < n)) {
|
||||
tri.extend([offset + t[0], offset + t[1], offset + t[2]]);
|
||||
}
|
||||
vertex_face.extend(std::iter::repeat_n(f.face, f.uv.len()));
|
||||
offset += n;
|
||||
}
|
||||
vertex_face.extend(std::iter::repeat_n(f.face, f.uv.len()));
|
||||
offset += n;
|
||||
parts.push(Part { vertices: v0..offset, indices: i0..word(tri.len())? });
|
||||
}
|
||||
let indices = init("tess mesh indices", &words(&tri), wgpu::BufferUsages::INDEX);
|
||||
let bind = d.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
@@ -238,6 +259,7 @@ impl TessGpu {
|
||||
index_count: word(tri.len())?,
|
||||
vertex_count: count,
|
||||
vertex_face,
|
||||
parts,
|
||||
bind,
|
||||
_inputs: [params, surfaces, data, verts],
|
||||
})
|
||||
@@ -348,10 +370,12 @@ fn push_curve(data: &mut Vec<f32>, c: &EvalCurve) -> Result<(u32, u32)> {
|
||||
Ok((knots, ctrl))
|
||||
}
|
||||
|
||||
fn pack(plan: &TessPlan) -> Result<Packed> {
|
||||
let mut surfaces = Vec::with_capacity(plan.surfaces.len() * SURFACE_WORDS);
|
||||
/// Several plans as one: their surfaces one after another, each face's surface index offset by
|
||||
/// the surfaces of the plans before it.
|
||||
fn pack(plans: &[&TessPlan]) -> Result<Packed> {
|
||||
let mut surfaces = Vec::with_capacity(plans.iter().map(|p| p.surfaces.len()).sum::<usize>() * SURFACE_WORDS);
|
||||
let mut data: Vec<f32> = Vec::new();
|
||||
for s in &plan.surfaces {
|
||||
for s in plans.iter().flat_map(|p| &p.surfaces) {
|
||||
let mut head = [0u32; SURFACE_WORDS];
|
||||
match s {
|
||||
EvalSurface::Plane { origin, du, dv } => {
|
||||
@@ -394,39 +418,44 @@ fn pack(plan: &TessPlan) -> Result<Packed> {
|
||||
}
|
||||
surfaces.extend(head);
|
||||
}
|
||||
let mut vertices = Vec::with_capacity(plan.vertex_count() * VERTEX_WORDS);
|
||||
let mut vertices = Vec::with_capacity(plans.iter().map(|p| p.vertex_count()).sum::<usize>() * VERTEX_WORDS);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut fixed = Vec::new();
|
||||
let mut count = 0usize;
|
||||
for f in &plan.faces {
|
||||
let Some(surface) = f.surface else {
|
||||
return Err(GpuError::Unsupported("a face without an evaluable surface".into()));
|
||||
};
|
||||
if surface as usize >= plan.surfaces.len() {
|
||||
return Err(GpuError::Failed("surface index out of range".into()));
|
||||
}
|
||||
for (k, uv) in f.uv.iter().enumerate() {
|
||||
let pinned = f.fixed.get(k).copied();
|
||||
let flags = u32::from(f.flip) | if pinned.is_some() { 2 } else { 0 };
|
||||
let p = pinned.unwrap_or(Vec3::ZERO);
|
||||
vertices.extend([
|
||||
(uv[0] as f32).to_bits(),
|
||||
(uv[1] as f32).to_bits(),
|
||||
surface,
|
||||
flags,
|
||||
(p.x as f32).to_bits(),
|
||||
(p.y as f32).to_bits(),
|
||||
(p.z as f32).to_bits(),
|
||||
0,
|
||||
]);
|
||||
if let Some(p) = pinned {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fixed.push((count, p));
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = p;
|
||||
let mut base = 0u32;
|
||||
for plan in plans {
|
||||
for f in &plan.faces {
|
||||
let Some(surface) = f.surface else {
|
||||
return Err(GpuError::Unsupported("a face without an evaluable surface".into()));
|
||||
};
|
||||
if surface as usize >= plan.surfaces.len() {
|
||||
return Err(GpuError::Failed("surface index out of range".into()));
|
||||
}
|
||||
let surface = base + surface;
|
||||
for (k, uv) in f.uv.iter().enumerate() {
|
||||
let pinned = f.fixed.get(k).copied();
|
||||
let flags = u32::from(f.flip) | if pinned.is_some() { 2 } else { 0 };
|
||||
let p = pinned.unwrap_or(Vec3::ZERO);
|
||||
vertices.extend([
|
||||
(uv[0] as f32).to_bits(),
|
||||
(uv[1] as f32).to_bits(),
|
||||
surface,
|
||||
flags,
|
||||
(p.x as f32).to_bits(),
|
||||
(p.y as f32).to_bits(),
|
||||
(p.z as f32).to_bits(),
|
||||
0,
|
||||
]);
|
||||
if let Some(p) = pinned {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fixed.push((count, p));
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = p;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
base += word(plan.surfaces.len())?;
|
||||
}
|
||||
Ok(Packed {
|
||||
surfaces,
|
||||
|
||||
@@ -112,6 +112,43 @@ fn a_resident_mesh_matches_the_cpu() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Several bodies in one buffer and one dispatch (a viewport's scene): each plan's part holds
|
||||
/// exactly its own vertices and triangles, evaluated as the CPU evaluates that plan alone.
|
||||
#[test]
|
||||
fn a_scene_in_one_dispatch_matches_each_body_on_the_cpu() {
|
||||
let Some(gpu) = gpu() else { return };
|
||||
let plans: Vec<TessPlan> = bodies().iter().map(|(_, b)| ok(b.display_plan_angular(0.01, std::f64::consts::PI / 9.0))).collect();
|
||||
let refs: Vec<&TessPlan> = plans.iter().collect();
|
||||
let mesh = ok(gpu.prepare_many(&refs));
|
||||
assert_eq!(mesh.parts.len(), plans.len());
|
||||
let d = &gpu.device;
|
||||
let size = u64::from(mesh.vertex_count) * VERTEX_STRIDE;
|
||||
let dst = d.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("test read"),
|
||||
size,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let mut enc = d.create_command_encoder(&Default::default());
|
||||
gpu.record(&mut enc, &mesh);
|
||||
enc.copy_buffer_to_buffer(&mesh.vertices, 0, &dst, 0, size);
|
||||
gpu.queue.submit([enc.finish()]);
|
||||
let slice = dst.slice(..);
|
||||
slice.map_async(wgpu::MapMode::Read, |_| {});
|
||||
ok(d.poll(wgpu::PollType::wait_indefinitely()));
|
||||
let out: Vec<f32> = ok(slice.get_mapped_range()).as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes(*b)).collect();
|
||||
for ((plan, part), (name, _)) in plans.iter().zip(&mesh.parts).zip(bodies()) {
|
||||
let cpu = plan.evaluate();
|
||||
assert_eq!((part.vertices.end - part.vertices.start) as usize, cpu.positions.len(), "{name}");
|
||||
assert_eq!((part.indices.end - part.indices.start) as usize, cpu.triangles.len() * 3, "{name}");
|
||||
for (k, p) in cpu.positions.iter().enumerate() {
|
||||
let i = part.vertices.start as usize + k;
|
||||
let q = Vec3::new(f64::from(out[8 * i]), f64::from(out[8 * i + 1]), f64::from(out[8 * i + 2]));
|
||||
assert!(q.dist(*p) < 20.0 * 1e-5, "{name}: vertex {k}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Display meshing of a 400-hole plate (#303's benchmark): reference mesher, plan, CPU and GPU
|
||||
/// evaluation. `cargo test --release -p solidcraft-gpu bench_plate -- --ignored --nocapture`.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user