1use crate::math::{Fix128, Vec3Fix};
18
19#[cfg(not(feature = "std"))]
20use alloc::vec::Vec;
21
22#[derive(Clone, Copy, Debug, PartialEq)]
30#[repr(C, align(16))]
31pub struct GpuSdfQuery {
32 pub x: f32,
34 pub y: f32,
36 pub z: f32,
38 pub radius: f32,
40}
41
42#[derive(Clone, Copy, Debug, Default)]
46#[repr(C, align(16))]
47pub struct GpuSdfResult {
48 pub distance: f32,
50 pub normal_x: f32,
52 pub normal_y: f32,
54 pub normal_z: f32,
56}
57
58const _: () = {
59 assert!(core::mem::size_of::<GpuSdfQuery>() == 16);
60 assert!(core::mem::size_of::<GpuSdfResult>() == 16);
61};
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub struct GpuDispatchConfig {
66 pub workgroup_size: u32,
68 pub max_queries: u32,
70 pub compute_normals: bool,
72}
73
74impl Default for GpuDispatchConfig {
75 fn default() -> Self {
76 Self {
77 workgroup_size: 64,
78 max_queries: 65536,
79 compute_normals: true,
80 }
81 }
82}
83
84pub struct GpuSdfBatch {
90 pub queries: Vec<GpuSdfQuery>,
92 pub results: Vec<GpuSdfResult>,
94 pub body_indices: Vec<usize>,
96 pub config: GpuDispatchConfig,
98}
99
100impl GpuSdfBatch {
101 #[must_use]
103 pub const fn new(config: GpuDispatchConfig) -> Self {
104 Self {
105 queries: Vec::new(),
106 results: Vec::new(),
107 body_indices: Vec::new(),
108 config,
109 }
110 }
111
112 pub fn add_query(&mut self, body_idx: usize, position: Vec3Fix, radius: Fix128) {
114 let (x, y, z) = position.to_f32();
115 self.queries.push(GpuSdfQuery {
116 x,
117 y,
118 z,
119 radius: radius.to_f32(),
120 });
121 self.body_indices.push(body_idx);
122 }
123
124 #[inline]
126 #[must_use]
127 pub fn query_count(&self) -> usize {
128 self.queries.len()
129 }
130
131 #[must_use]
133 pub fn num_workgroups(&self) -> u32 {
134 let n = self.queries.len() as u32;
135 n.div_ceil(self.config.workgroup_size)
136 }
137
138 pub fn prepare_output(&mut self) {
140 self.results
141 .resize(self.queries.len(), GpuSdfResult::default());
142 }
143
144 pub fn clear(&mut self) {
146 self.queries.clear();
147 self.results.clear();
148 self.body_indices.clear();
149 }
150
151 #[must_use]
153 pub fn query_bytes(&self) -> &[u8] {
154 unsafe {
158 core::slice::from_raw_parts(
159 self.queries.as_ptr().cast::<u8>(),
160 self.queries.len() * core::mem::size_of::<GpuSdfQuery>(),
161 )
162 }
163 }
164
165 pub fn result_bytes_mut(&mut self) -> &mut [u8] {
167 unsafe {
172 core::slice::from_raw_parts_mut(
173 self.results.as_mut_ptr().cast::<u8>(),
174 self.results.len() * core::mem::size_of::<GpuSdfResult>(),
175 )
176 }
177 }
178
179 #[must_use]
185 pub fn extract_contacts(&self, collision_radius: f32) -> Vec<GpuSdfContact> {
186 let mut contacts = Vec::new();
187
188 for (i, result) in self.results.iter().enumerate() {
189 let radius = self.queries.get(i).map_or(0.0, |q| q.radius);
190 let penetration = collision_radius + radius - result.distance;
191 if penetration > 0.0 {
192 contacts.push(GpuSdfContact {
193 body_index: self.body_indices[i],
194 penetration,
195 normal: (result.normal_x, result.normal_y, result.normal_z),
196 distance: result.distance,
197 });
198 }
199 }
200
201 contacts
202 }
203}
204
205#[repr(C, align(16))]
216pub struct GpuSdfInstancedBatch {
217 pub sdf_id: u32,
219 pub query_count: u32,
221 pub queries: Vec<GpuSdfQuery>,
223}
224
225pub struct GpuSdfMultiDispatch {
243 pub batches: Vec<GpuSdfInstancedBatch>,
245}
246
247impl GpuSdfMultiDispatch {
248 #[must_use]
250 pub const fn new() -> Self {
251 Self {
252 batches: Vec::new(),
253 }
254 }
255
256 pub fn add_batch(&mut self, sdf_id: u32, mut queries: Vec<GpuSdfQuery>) {
263 if let Some(b) = self.batches.iter_mut().find(|b| b.sdf_id == sdf_id) {
264 b.queries.append(&mut queries);
265 b.query_count = b.queries.len() as u32;
266 return;
267 }
268 self.batches.push(GpuSdfInstancedBatch {
269 sdf_id,
270 query_count: queries.len() as u32,
271 queries,
272 });
273 }
274
275 #[must_use]
277 pub fn total_queries(&self) -> usize {
278 self.batches.iter().map(|b| b.queries.len()).sum()
279 }
280
281 #[must_use]
284 pub fn total_dispatches(&self) -> usize {
285 self.batches.len()
286 }
287}
288
289impl Default for GpuSdfMultiDispatch {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295#[inline(always)]
316#[must_use]
317pub const fn batch_size() -> usize {
318 crate::math::SIMD_WIDTH
319}
320
321#[derive(Clone, Copy, Debug, PartialEq)]
323pub struct GpuSdfContact {
324 pub body_index: usize,
326 pub penetration: f32,
328 pub normal: (f32, f32, f32),
330 pub distance: f32,
332}
333
334#[cfg(feature = "std")]
340pub fn execute_batch_cpu(batch: &mut GpuSdfBatch, sdf: &dyn crate::sdf_collider::SdfField) {
341 batch.prepare_output();
342
343 for (i, query) in batch.queries.iter().enumerate() {
344 let dist = sdf.distance(query.x, query.y, query.z);
345
346 let (nx, ny, nz) = if batch.config.compute_normals {
347 sdf.normal(query.x, query.y, query.z)
348 } else {
349 (0.0, 0.0, 0.0)
350 };
351
352 batch.results[i] = GpuSdfResult {
353 distance: dist,
354 normal_x: nx,
355 normal_y: ny,
356 normal_z: nz,
357 };
358 }
359}
360
361pub const SDF_EVAL_WGSL: &str = r"
370struct Query {
371 x: f32,
372 y: f32,
373 z: f32,
374 radius: f32,
375};
376
377struct Result {
378 distance: f32,
379 normal_x: f32,
380 normal_y: f32,
381 normal_z: f32,
382};
383
384@group(0) @binding(0) var<storage, read> queries: array<Query>;
385@group(0) @binding(1) var<storage, read_write> results: array<Result>;
386
387// SDF evaluation function (generated per-SDF)
388fn sdf_distance(x: f32, y: f32, z: f32) -> f32 {
389 // Placeholder - replaced by ALICE-SDF code generation
390 return length(vec3(x, y, z)) - 1.0;
391}
392
393fn sdf_normal(x: f32, y: f32, z: f32) -> vec3<f32> {
394 let eps = 0.001;
395 let dx = sdf_distance(x + eps, y, z) - sdf_distance(x - eps, y, z);
396 let dy = sdf_distance(x, y + eps, z) - sdf_distance(x, y - eps, z);
397 let dz = sdf_distance(x, y, z + eps) - sdf_distance(x, y, z - eps);
398 return normalize(vec3(dx, dy, dz));
399}
400
401@compute @workgroup_size(64)
402fn main(@builtin(global_invocation_id) id: vec3<u32>) {
403 let idx = id.x;
404 if idx >= arrayLength(&queries) {
405 return;
406 }
407 let q = queries[idx];
408 let d = sdf_distance(q.x, q.y, q.z);
409 let n = sdf_normal(q.x, q.y, q.z);
410 results[idx] = Result(d, n.x, n.y, n.z);
411}
412";
413
414#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::sdf_collider::ClosureSdf;
422
423 #[test]
424 fn test_gpu_batch_creation() {
425 let mut batch = GpuSdfBatch::new(GpuDispatchConfig::default());
426
427 batch.add_query(0, Vec3Fix::from_f32(1.0, 0.0, 0.0), Fix128::from_f32(0.5));
428 batch.add_query(1, Vec3Fix::from_f32(0.0, 2.0, 0.0), Fix128::from_f32(0.5));
429
430 assert_eq!(batch.query_count(), 2);
431 assert_eq!(batch.num_workgroups(), 1);
432 }
433
434 #[test]
435 fn test_cpu_fallback() {
436 let sphere = ClosureSdf::new(
437 |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
438 |x, y, z| {
439 let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
440 if len < 1e-10 {
441 (0.0, 1.0, 0.0)
442 } else {
443 (x / len, y / len, z / len)
444 }
445 },
446 );
447
448 let mut batch = GpuSdfBatch::new(GpuDispatchConfig::default());
449 batch.add_query(0, Vec3Fix::from_f32(2.0, 0.0, 0.0), Fix128::from_f32(0.5));
450 batch.add_query(1, Vec3Fix::from_f32(0.5, 0.0, 0.0), Fix128::from_f32(0.5));
451
452 execute_batch_cpu(&mut batch, &sphere);
453
454 assert!(batch.results[0].distance > 0.0, "Should be outside sphere");
456 assert!(batch.results[1].distance < 0.0, "Should be inside sphere");
458 }
459
460 #[test]
461 fn test_extract_contacts() {
462 let sphere = ClosureSdf::new(
463 |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
464 |x, y, z| {
465 let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
466 if len < 1e-10 {
467 (0.0, 1.0, 0.0)
468 } else {
469 (x / len, y / len, z / len)
470 }
471 },
472 );
473
474 let mut batch = GpuSdfBatch::new(GpuDispatchConfig::default());
475 batch.add_query(0, Vec3Fix::from_f32(0.5, 0.0, 0.0), Fix128::from_f32(0.5));
476
477 execute_batch_cpu(&mut batch, &sphere);
478
479 let contacts = batch.extract_contacts(0.5);
480 assert!(
481 !contacts.is_empty(),
482 "Should detect contact for body inside sphere"
483 );
484 }
485
486 #[test]
487 fn test_buffer_layout() {
488 assert_eq!(core::mem::size_of::<GpuSdfQuery>(), 16);
489 assert_eq!(core::mem::size_of::<GpuSdfResult>(), 16);
490 }
491
492 fn q(x: f32, radius: f32) -> GpuSdfQuery {
493 GpuSdfQuery {
494 x,
495 y: 0.0,
496 z: 0.0,
497 radius,
498 }
499 }
500
501 #[test]
502 fn multi_dispatch_merges_batches_by_sdf_id_in_call_order() {
503 let mut d = GpuSdfMultiDispatch::default();
504 assert_eq!(d.total_queries(), 0);
505 assert_eq!(d.total_dispatches(), 0);
506 d.add_batch(7, vec![q(1.0, 0.0)]);
507 d.add_batch(3, vec![q(2.0, 0.0), q(3.0, 0.0)]);
508 d.add_batch(7, vec![q(4.0, 0.5), q(5.0, 0.0)]);
509 assert_eq!(d.total_dispatches(), 2);
511 assert_eq!(d.total_queries(), 5);
512 assert_eq!(d.batches[0].sdf_id, 7);
513 assert_eq!(d.batches[1].sdf_id, 3);
514 assert_eq!(d.batches[0].query_count, 3);
516 assert_eq!(
517 d.batches[0].queries,
518 vec![q(1.0, 0.0), q(4.0, 0.5), q(5.0, 0.0)]
519 );
520 assert_eq!(d.batches[1].query_count, 2);
521 }
522
523 #[test]
524 fn byte_views_cover_every_query_and_result() {
525 let mut batch = GpuSdfBatch::new(GpuDispatchConfig::default());
526 batch.add_query(4, Vec3Fix::from_f32(1.5, -2.0, 0.25), Fix128::from_f32(0.5));
527 batch.add_query(9, Vec3Fix::from_f32(0.0, 3.0, 0.0), Fix128::ZERO);
528 let bytes = batch.query_bytes();
529 assert_eq!(bytes.len(), 2 * 16);
530 let f = |k: usize| f32::from_ne_bytes([bytes[k], bytes[k + 1], bytes[k + 2], bytes[k + 3]]);
531 assert_eq!([f(0), f(4), f(8), f(12)], [1.5, -2.0, 0.25, 0.5]);
532 assert_eq!([f(16), f(20), f(24), f(28)], [0.0, 3.0, 0.0, 0.0]);
533
534 assert!(batch.result_bytes_mut().is_empty());
536 batch.prepare_output();
537 let out = batch.result_bytes_mut();
538 assert_eq!(out.len(), 2 * 16);
539 out[16..20].copy_from_slice(&(-0.75f32).to_ne_bytes());
540 out[24..28].copy_from_slice(&1.0f32.to_ne_bytes());
541 assert_eq!(batch.results[1].distance, -0.75);
542 assert_eq!(batch.results[1].normal_y, 1.0);
543 assert_eq!(batch.results[0].distance, 0.0);
544
545 batch.clear();
546 assert_eq!(batch.query_count(), 0);
547 assert!(batch.results.is_empty());
548 assert!(batch.body_indices.is_empty());
549 assert!(batch.query_bytes().is_empty());
550 assert_eq!(batch.num_workgroups(), 0);
551 }
552
553 #[test]
554 fn cpu_fallback_without_normals_and_contact_depth() {
555 let plane = ClosureSdf::new(|_x, y, _z| y, |_x, _y, _z| (0.0, 1.0, 0.0));
557 let mut batch = GpuSdfBatch::new(GpuDispatchConfig {
558 compute_normals: false,
559 ..GpuDispatchConfig::default()
560 });
561 batch.add_query(2, Vec3Fix::from_f32(0.0, 0.25, 0.0), Fix128::from_f32(0.5));
562 batch.add_query(5, Vec3Fix::from_f32(0.0, 2.0, 0.0), Fix128::ZERO);
563 execute_batch_cpu(&mut batch, &plane);
564 assert_eq!(batch.results[0].distance, 0.25);
565 assert_eq!(
566 (
567 batch.results[0].normal_x,
568 batch.results[0].normal_y,
569 batch.results[0].normal_z
570 ),
571 (0.0, 0.0, 0.0)
572 );
573 let contacts = batch.extract_contacts(0.25);
575 assert_eq!(contacts.len(), 1);
576 assert_eq!(contacts[0].body_index, 2);
577 assert_eq!(contacts[0].penetration, 0.5);
578 assert_eq!(contacts[0].distance, 0.25);
579 }
580
581 #[test]
582 fn workgroup_count_rounds_up() {
583 let mut batch = GpuSdfBatch::new(GpuDispatchConfig {
584 workgroup_size: 4,
585 ..GpuDispatchConfig::default()
586 });
587 for i in 0..9 {
588 batch.add_query(i, Vec3Fix::ZERO, Fix128::ZERO);
589 }
590 assert_eq!(batch.num_workgroups(), 3);
591 assert_eq!(batch_size(), crate::math::SIMD_WIDTH);
592 }
593}