use crate::math::{Fix128, Vec3Fix};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(C, align(16))]
pub struct GpuSdfQuery {
pub x: f32,
pub y: f32,
pub z: f32,
pub radius: f32,
}
#[derive(Clone, Copy, Debug, Default)]
#[repr(C, align(16))]
pub struct GpuSdfResult {
pub distance: f32,
pub normal_x: f32,
pub normal_y: f32,
pub normal_z: f32,
}
const _: () = {
assert!(core::mem::size_of::<GpuSdfQuery>() == 16);
assert!(core::mem::size_of::<GpuSdfResult>() == 16);
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GpuDispatchConfig {
pub workgroup_size: u32,
pub max_queries: u32,
pub compute_normals: bool,
}
impl Default for GpuDispatchConfig {
fn default() -> Self {
Self {
workgroup_size: 64,
max_queries: 65536,
compute_normals: true,
}
}
}
pub struct GpuSdfBatch {
pub queries: Vec<GpuSdfQuery>,
pub results: Vec<GpuSdfResult>,
pub body_indices: Vec<usize>,
pub config: GpuDispatchConfig,
}
impl GpuSdfBatch {
#[must_use]
pub const fn new(config: GpuDispatchConfig) -> Self {
Self {
queries: Vec::new(),
results: Vec::new(),
body_indices: Vec::new(),
config,
}
}
pub fn add_query(&mut self, body_idx: usize, position: Vec3Fix, radius: Fix128) {
let (x, y, z) = position.to_f32();
self.queries.push(GpuSdfQuery {
x,
y,
z,
radius: radius.to_f32(),
});
self.body_indices.push(body_idx);
}
#[inline]
#[must_use]
pub fn query_count(&self) -> usize {
self.queries.len()
}
#[must_use]
pub fn num_workgroups(&self) -> u32 {
let n = self.queries.len() as u32;
(n + self.config.workgroup_size - 1) / self.config.workgroup_size
}
pub fn prepare_output(&mut self) {
self.results
.resize(self.queries.len(), GpuSdfResult::default());
}
pub fn clear(&mut self) {
self.queries.clear();
self.results.clear();
self.body_indices.clear();
}
#[must_use]
pub fn query_bytes(&self) -> &[u8] {
unsafe {
core::slice::from_raw_parts(
self.queries.as_ptr().cast::<u8>(),
self.queries.len() * core::mem::size_of::<GpuSdfQuery>(),
)
}
}
pub fn result_bytes_mut(&mut self) -> &mut [u8] {
unsafe {
core::slice::from_raw_parts_mut(
self.results.as_mut_ptr().cast::<u8>(),
self.results.len() * core::mem::size_of::<GpuSdfResult>(),
)
}
}
#[must_use]
pub fn extract_contacts(&self, collision_radius: f32) -> Vec<GpuSdfContact> {
let mut contacts = Vec::new();
for (i, result) in self.results.iter().enumerate() {
let penetration = collision_radius - result.distance;
if penetration > 0.0 {
contacts.push(GpuSdfContact {
body_index: self.body_indices[i],
penetration,
normal: (result.normal_x, result.normal_y, result.normal_z),
distance: result.distance,
});
}
}
contacts
}
}
#[repr(C, align(16))]
pub struct GpuSdfInstancedBatch {
pub sdf_id: u32,
pub query_count: u32,
pub queries: Vec<GpuSdfQuery>,
}
pub struct GpuSdfMultiDispatch {
pub batches: Vec<GpuSdfInstancedBatch>,
}
impl GpuSdfMultiDispatch {
#[must_use]
pub const fn new() -> Self {
Self {
batches: Vec::new(),
}
}
pub fn add_batch(&mut self, sdf_id: u32, queries: Vec<GpuSdfQuery>) {
self.batches.push(GpuSdfInstancedBatch {
sdf_id,
query_count: queries.len() as u32,
queries,
});
}
#[must_use]
pub fn total_queries(&self) -> usize {
self.batches.iter().map(|b| b.queries.len()).sum()
}
#[must_use]
pub fn total_dispatches(&self) -> usize {
self.batches.len()
}
}
impl Default for GpuSdfMultiDispatch {
fn default() -> Self {
Self::new()
}
}
#[inline(always)]
#[must_use]
pub const fn batch_size() -> usize {
crate::math::SIMD_WIDTH
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GpuSdfContact {
pub body_index: usize,
pub penetration: f32,
pub normal: (f32, f32, f32),
pub distance: f32,
}
#[cfg(feature = "std")]
pub fn execute_batch_cpu(batch: &mut GpuSdfBatch, sdf: &dyn crate::sdf_collider::SdfField) {
batch.prepare_output();
for (i, query) in batch.queries.iter().enumerate() {
let dist = sdf.distance(query.x, query.y, query.z);
let (nx, ny, nz) = if batch.config.compute_normals {
sdf.normal(query.x, query.y, query.z)
} else {
(0.0, 0.0, 0.0)
};
batch.results[i] = GpuSdfResult {
distance: dist,
normal_x: nx,
normal_y: ny,
normal_z: nz,
};
}
}
pub const SDF_EVAL_WGSL: &str = r"
struct Query {
x: f32,
y: f32,
z: f32,
radius: f32,
};
struct Result {
distance: f32,
normal_x: f32,
normal_y: f32,
normal_z: f32,
};
@group(0) @binding(0) var<storage, read> queries: array<Query>;
@group(0) @binding(1) var<storage, read_write> results: array<Result>;
// SDF evaluation function (generated per-SDF)
fn sdf_distance(x: f32, y: f32, z: f32) -> f32 {
// Placeholder - replaced by ALICE-SDF code generation
return length(vec3(x, y, z)) - 1.0;
}
fn sdf_normal(x: f32, y: f32, z: f32) -> vec3<f32> {
let eps = 0.001;
let dx = sdf_distance(x + eps, y, z) - sdf_distance(x - eps, y, z);
let dy = sdf_distance(x, y + eps, z) - sdf_distance(x, y - eps, z);
let dz = sdf_distance(x, y, z + eps) - sdf_distance(x, y, z - eps);
return normalize(vec3(dx, dy, dz));
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
if idx >= arrayLength(&queries) {
return;
}
let q = queries[idx];
let d = sdf_distance(q.x, q.y, q.z);
let n = sdf_normal(q.x, q.y, q.z);
results[idx] = Result(d, n.x, n.y, n.z);
}
";
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf_collider::ClosureSdf;
#[test]
fn test_gpu_batch_creation() {
let mut batch = GpuSdfBatch::new(GpuDispatchConfig::default());
batch.add_query(0, Vec3Fix::from_f32(1.0, 0.0, 0.0), Fix128::from_f32(0.5));
batch.add_query(1, Vec3Fix::from_f32(0.0, 2.0, 0.0), Fix128::from_f32(0.5));
assert_eq!(batch.query_count(), 2);
assert_eq!(batch.num_workgroups(), 1);
}
#[test]
fn test_cpu_fallback() {
let sphere = ClosureSdf::new(
|x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
|x, y, z| {
let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
if len < 1e-10 {
(0.0, 1.0, 0.0)
} else {
(x / len, y / len, z / len)
}
},
);
let mut batch = GpuSdfBatch::new(GpuDispatchConfig::default());
batch.add_query(0, Vec3Fix::from_f32(2.0, 0.0, 0.0), Fix128::from_f32(0.5));
batch.add_query(1, Vec3Fix::from_f32(0.5, 0.0, 0.0), Fix128::from_f32(0.5));
execute_batch_cpu(&mut batch, &sphere);
assert!(batch.results[0].distance > 0.0, "Should be outside sphere");
assert!(batch.results[1].distance < 0.0, "Should be inside sphere");
}
#[test]
fn test_extract_contacts() {
let sphere = ClosureSdf::new(
|x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
|x, y, z| {
let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
if len < 1e-10 {
(0.0, 1.0, 0.0)
} else {
(x / len, y / len, z / len)
}
},
);
let mut batch = GpuSdfBatch::new(GpuDispatchConfig::default());
batch.add_query(0, Vec3Fix::from_f32(0.5, 0.0, 0.0), Fix128::from_f32(0.5));
execute_batch_cpu(&mut batch, &sphere);
let contacts = batch.extract_contacts(0.5);
assert!(
!contacts.is_empty(),
"Should detect contact for body inside sphere"
);
}
#[test]
fn test_buffer_layout() {
assert_eq!(core::mem::size_of::<GpuSdfQuery>(), 16);
assert_eq!(core::mem::size_of::<GpuSdfResult>(), 16);
}
}