#[cfg(test)]
mod tests {
use crate::gpu_physics::fem::{GpuFemParams, GpuFemSystem, GpuSoftBodyNode, GpuTetrahedron};
async fn setup_headless_gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
})
.await?;
adapter
.request_device(&wgpu::DeviceDescriptor::default(), None)
.await
.ok()
}
async fn read_buffer<T: bytemuck::Pod>(
device: &wgpu::Device,
queue: &wgpu::Queue,
buffer: &wgpu::Buffer,
) -> Vec<T> {
let size = buffer.size();
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Test Staging Buffer"),
size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
encoder.copy_buffer_to_buffer(buffer, 0, &staging_buffer, 0, size);
queue.submit(Some(encoder.finish()));
let buffer_slice = staging_buffer.slice(..);
let (sender, receiver) = std::sync::mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap());
device.poll(wgpu::Maintain::Wait);
receiver.recv().unwrap().unwrap();
let data = buffer_slice.get_mapped_range();
let result = bytemuck::cast_slice(&data).to_vec();
drop(data);
staging_buffer.unmap();
result
}
#[test]
fn test_fem_struct_sizes() {
assert_eq!(
std::mem::size_of::<GpuSoftBodyNode>(),
48,
"Node size must be 48 bytes"
);
assert_eq!(
std::mem::size_of::<GpuTetrahedron>(),
80,
"Tetrahedron size must be 80 bytes"
);
assert_eq!(
std::mem::size_of::<GpuFemParams>(),
48,
"FEM Params size must be 48 bytes"
);
}
#[test]
fn test_fem_compute_clear_forces() {
pollster::block_on(async {
let Some((device, queue)) = setup_headless_gpu().await else {
tracing::info!("Skipping GPU test: no wgpu adapter found");
return;
};
let nodes = vec![GpuSoftBodyNode {
position_mass: [0.0, 0.0, 0.0, 10.0],
velocity_fixed: [0.0, 0.0, 0.0, 0.0],
forces: [500, 500, 500, 0], }];
let elements = vec![GpuTetrahedron {
indices: [0, 0, 0, 0],
inv_rest_col0: [0.0; 4],
inv_rest_col1: [0.0; 4],
inv_rest_col2: [0.0; 4],
rest_volume_pad: [0.0; 4],
}];
let params = GpuFemParams {
properties: [0.001, 1.0, 1.0, 1.0],
gravity: [0.0, -9.81, 0.0, 0.0],
counts: [1, 0, 0, 0],
};
let fem_system = GpuFemSystem::new(&device, &nodes, &elements, &[], ¶ms);
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None,
});
cpass.set_bind_group(0, &fem_system.compute_bind_group, &[]);
cpass.set_pipeline(&fem_system.pipeline_clear);
cpass.dispatch_workgroups(1, 1, 1);
}
queue.submit(Some(encoder.finish()));
let result_nodes: Vec<GpuSoftBodyNode> =
read_buffer(&device, &queue, &fem_system.nodes_buffer).await;
let expected_fy = (10.0 * -9.81 * 100000.0) as i32;
assert_eq!(result_nodes[0].forces[0], 0);
assert!(
(result_nodes[0].forces[1] - expected_fy).abs() <= 10,
"Y force mismatch: got {}, expected {}",
result_nodes[0].forces[1],
expected_fy
);
assert_eq!(result_nodes[0].forces[2], 0);
});
}
#[test]
fn test_fem_compute_integration_and_collision() {
pollster::block_on(async {
let Some((device, queue)) = setup_headless_gpu().await else {
tracing::info!("Skipping GPU test: no wgpu adapter found");
return;
};
let nodes = vec![
GpuSoftBodyNode {
position_mass: [0.0, 2.0, 0.0, 1.0],
velocity_fixed: [0.0, -10.0, 0.0, 0.0],
forces: [0, 0, 0, 0],
},
GpuSoftBodyNode {
position_mass: [0.0, -1.0, 0.0, 1.0], velocity_fixed: [5.0, -10.0, 5.0, 0.0], forces: [0, 0, 0, 0],
},
];
let elements = vec![GpuTetrahedron {
indices: [0, 0, 0, 0],
inv_rest_col0: [0.0; 4],
inv_rest_col1: [0.0; 4],
inv_rest_col2: [0.0; 4],
rest_volume_pad: [0.0; 4],
}];
use crate::gpu_physics::fem::GpuFemCollider;
let colliders = vec![GpuFemCollider {
shape_type: 0, radius: 0.0,
_pad0: 0,
_pad1: 0,
position: [0.0, 0.0, 0.0, 0.0], normal: [0.0, 1.0, 0.0, 0.0], }];
let params = GpuFemParams {
properties: [0.1, 1.0, 1.0, 0.9], gravity: [0.0, 0.0, 0.0, 0.0], counts: [2, 0, 1, 0], };
let fem_system = GpuFemSystem::new(&device, &nodes, &elements, &colliders, ¶ms);
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None,
});
cpass.set_bind_group(0, &fem_system.compute_bind_group, &[]);
cpass.set_pipeline(&fem_system.pipeline_integrate);
cpass.dispatch_workgroups(1, 1, 1);
}
queue.submit(Some(encoder.finish()));
let result_nodes: Vec<GpuSoftBodyNode> =
read_buffer(&device, &queue, &fem_system.nodes_buffer).await;
let n0 = &result_nodes[0];
assert!((n0.velocity_fixed[1] - (-9.0)).abs() < 0.001,
"Node 0 velocity Y: expected -9.0, got {}", n0.velocity_fixed[1]);
assert!((n0.position_mass[1] - 1.1).abs() < 0.001,
"Node 0 position Y: expected 1.1, got {}", n0.position_mass[1]);
let n1 = &result_nodes[1];
assert!((n1.velocity_fixed[1] - 1.8).abs() < 0.01,
"Node 1 velocity Y: expected 1.8 (bounce), got {}", n1.velocity_fixed[1]);
assert!((n1.velocity_fixed[0] - 3.6).abs() < 0.01,
"Node 1 velocity X: expected 3.6 (friction), got {}", n1.velocity_fixed[0]);
assert!((n1.velocity_fixed[2] - 3.6).abs() < 0.01,
"Node 1 velocity Z: expected 3.6 (friction), got {}", n1.velocity_fixed[2]);
assert!((n1.position_mass[1] - 0.0).abs() < 0.01,
"Node 1 position Y: expected 0.0, got {}", n1.position_mass[1]);
});
}
#[test]
fn test_fem_compute_stress() {
pollster::block_on(async {
let Some((device, queue)) = setup_headless_gpu().await else {
tracing::info!("Skipping GPU test: no wgpu adapter found");
return;
};
let nodes = vec![
GpuSoftBodyNode {
position_mass: [0.0, 0.0, 0.0, 1.0],
velocity_fixed: [0.0; 4],
forces: [0; 4],
},
GpuSoftBodyNode {
position_mass: [2.0, 0.0, 0.0, 1.0],
velocity_fixed: [0.0; 4],
forces: [0; 4],
}, GpuSoftBodyNode {
position_mass: [0.0, 1.0, 0.0, 1.0],
velocity_fixed: [0.0; 4],
forces: [0; 4],
},
GpuSoftBodyNode {
position_mass: [0.0, 0.0, 1.0, 1.0],
velocity_fixed: [0.0; 4],
forces: [0; 4],
},
];
let elements = vec![GpuTetrahedron {
indices: [0, 1, 2, 3],
inv_rest_col0: [1.0, 0.0, 0.0, 0.0],
inv_rest_col1: [0.0, 1.0, 0.0, 0.0],
inv_rest_col2: [0.0, 0.0, 1.0, 0.0],
rest_volume_pad: [1.0 / 6.0, 0.0, 0.0, 0.0], }];
let params = GpuFemParams {
properties: [0.1, 1000.0, 1000.0, 1.0], gravity: [0.0, 0.0, 0.0, 0.0],
counts: [4, 1, 0, 0],
};
let fem_system = GpuFemSystem::new(&device, &nodes, &elements, &[], ¶ms);
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None,
});
cpass.set_bind_group(0, &fem_system.compute_bind_group, &[]);
cpass.set_pipeline(&fem_system.pipeline_stress);
cpass.dispatch_workgroups(1, 1, 1);
}
queue.submit(Some(encoder.finish()));
let result_nodes: Vec<GpuSoftBodyNode> =
read_buffer(&device, &queue, &fem_system.nodes_buffer).await;
let f1_x = result_nodes[1].forces[0];
let f0_x = result_nodes[0].forces[0];
assert!(
f1_x < 0,
"Node 1 should feel restorative force in -X direction. Got {}",
f1_x
);
assert!(
f0_x > 0,
"Node 0 should feel restorative force in +X direction. Got {}",
f0_x
);
let sum_fx = result_nodes[0].forces[0]
+ result_nodes[1].forces[0]
+ result_nodes[2].forces[0]
+ result_nodes[3].forces[0];
assert!(
sum_fx.abs() <= 10,
"Forces must sum up to zero for equilibrium"
);
});
}
}