use super::eval_simd::eval_compiled_simd;
use super::simd::Vec3x8;
use super::CompiledSdf;
use crate::soa::{SoADistances, SoAPoints, SIMD_WIDTH};
use rayon::prelude::*;
use wide::f32x8;
pub fn eval_compiled_batch_soa(sdf: &CompiledSdf, points: &SoAPoints) -> Vec<f32> {
if points.is_empty() {
return Vec::new();
}
let len = points.len();
let padded_len = points.padded_len();
let mut results = vec![0.0f32; padded_len];
for chunk_start in (0..padded_len).step_by(SIMD_WIDTH) {
let (x, y, z) = points.load_simd(chunk_start).unwrap_or_else(|| {
(f32x8::splat(0.0), f32x8::splat(0.0), f32x8::splat(0.0))
});
let p = Vec3x8 { x, y, z };
let distances = eval_compiled_simd(sdf, p);
let dist_arr: [f32; 8] = distances.into();
results[chunk_start..chunk_start + 8].copy_from_slice(&dist_arr);
}
results.truncate(len);
results
}
pub fn eval_compiled_batch_soa_parallel(sdf: &CompiledSdf, points: &SoAPoints) -> Vec<f32> {
if points.is_empty() {
return Vec::new();
}
let len = points.len();
let padded_len = points.padded_len();
const PARALLEL_THRESHOLD: usize = 256;
if padded_len < PARALLEL_THRESHOLD {
return eval_compiled_batch_soa(sdf, points);
}
let mut results = SoADistances::with_capacity(len);
let (x_slice, y_slice, z_slice) = points.as_slices();
let chunk_count = padded_len / SIMD_WIDTH;
let chunk_results: Vec<[f32; 8]> = (0..chunk_count)
.into_par_iter()
.map(|chunk_idx| {
let offset = chunk_idx * SIMD_WIDTH;
let x = f32x8::from(&x_slice[offset..offset + 8]);
let y = f32x8::from(&y_slice[offset..offset + 8]);
let z = f32x8::from(&z_slice[offset..offset + 8]);
let p = Vec3x8 { x, y, z };
let distances = eval_compiled_simd(sdf, p);
distances.into()
})
.collect();
for (chunk_idx, chunk_result) in chunk_results.into_iter().enumerate() {
let offset = chunk_idx * SIMD_WIDTH;
let copy_len = (len - offset).min(SIMD_WIDTH);
results.distances[offset..offset + copy_len].copy_from_slice(&chunk_result[..copy_len]);
}
results.to_vec()
}
pub fn eval_compiled_batch_soa_into(
sdf: &CompiledSdf,
points: &SoAPoints,
output: &mut SoADistances,
) {
let padded_len = points.padded_len();
if output.distances.len() < padded_len {
output.distances.resize(padded_len, 0.0);
}
let (x_slice, y_slice, z_slice) = points.as_slices();
for chunk_start in (0..padded_len).step_by(SIMD_WIDTH) {
let x = f32x8::from(&x_slice[chunk_start..chunk_start + 8]);
let y = f32x8::from(&y_slice[chunk_start..chunk_start + 8]);
let z = f32x8::from(&z_slice[chunk_start..chunk_start + 8]);
let p = Vec3x8 { x, y, z };
let distances = eval_compiled_simd(sdf, p);
unsafe {
output.store_simd_unchecked(chunk_start, distances);
}
}
}
#[inline(never)]
pub unsafe fn eval_compiled_batch_soa_raw(
sdf: &CompiledSdf,
x_ptr: *const f32,
y_ptr: *const f32,
z_ptr: *const f32,
out_ptr: *mut f32,
count: usize,
) {
let simd_count = count & !7;
for i in (0..simd_count).step_by(8) {
let x = f32x8::from(std::slice::from_raw_parts(x_ptr.add(i), 8));
let y = f32x8::from(std::slice::from_raw_parts(y_ptr.add(i), 8));
let z = f32x8::from(std::slice::from_raw_parts(z_ptr.add(i), 8));
let p = Vec3x8 { x, y, z };
let distances = eval_compiled_simd(sdf, p);
let arr: [f32; 8] = distances.into();
std::ptr::copy_nonoverlapping(arr.as_ptr(), out_ptr.add(i), 8);
}
for i in simd_count..count {
let p = glam::Vec3::new(*x_ptr.add(i), *y_ptr.add(i), *z_ptr.add(i));
*out_ptr.add(i) = crate::compiled::eval_compiled(sdf, p);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::eval;
use crate::types::SdfNode;
use glam::Vec3;
fn approx_eq(a: f32, b: f32, epsilon: f32) -> bool {
(a - b).abs() < epsilon
}
#[test]
fn test_soa_batch_eval_sphere() {
let sphere = SdfNode::Sphere { radius: 1.0 };
let compiled = CompiledSdf::compile(&sphere);
let points = vec![
Vec3::ZERO,
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(0.5, 0.5, 0.5),
];
let soa = SoAPoints::from_vec3_slice(&points);
let results = eval_compiled_batch_soa(&compiled, &soa);
assert_eq!(results.len(), points.len());
for (i, p) in points.iter().enumerate() {
let expected = eval(&sphere, *p);
assert!(
approx_eq(results[i], expected, 0.001),
"Mismatch at {}: got {}, expected {}",
i,
results[i],
expected
);
}
}
#[test]
fn test_soa_batch_eval_parallel() {
let shape = SdfNode::Sphere { radius: 1.0 }.smooth_union(
SdfNode::Box3d {
half_extents: Vec3::splat(0.5),
}
.translate(2.0, 0.0, 0.0),
0.2,
);
let compiled = CompiledSdf::compile(&shape);
let points: Vec<Vec3> = (0..1000)
.map(|i| Vec3::new((i as f32).mul_add(0.01, -5.0), 0.0, 0.0))
.collect();
let soa = SoAPoints::from_vec3_slice(&points);
let results_st = eval_compiled_batch_soa(&compiled, &soa);
let results_mt = eval_compiled_batch_soa_parallel(&compiled, &soa);
assert_eq!(results_st.len(), results_mt.len());
for i in 0..points.len() {
assert!(
approx_eq(results_st[i], results_mt[i], 0.0001),
"Mismatch at {}: st={}, mt={}",
i,
results_st[i],
results_mt[i]
);
}
}
#[test]
fn test_soa_batch_eval_into() {
let sphere = SdfNode::Sphere { radius: 1.0 };
let compiled = CompiledSdf::compile(&sphere);
let points = vec![Vec3::ZERO, Vec3::new(2.0, 0.0, 0.0)];
let soa = SoAPoints::from_vec3_slice(&points);
let mut output = SoADistances::with_capacity(points.len());
eval_compiled_batch_soa_into(&compiled, &soa, &mut output);
let results = output.to_vec();
assert!(approx_eq(results[0], -1.0, 0.001)); assert!(approx_eq(results[1], 1.0, 0.001)); }
#[test]
fn test_soa_vs_aos_consistency() {
let shape = SdfNode::Sphere { radius: 1.0 }
.union(SdfNode::Box3d {
half_extents: Vec3::splat(0.5),
})
.translate(1.0, 0.0, 0.0);
let compiled = CompiledSdf::compile(&shape);
let points: Vec<Vec3> = (0..100)
.map(|i| {
let t = i as f32 / 100.0 * std::f32::consts::TAU;
Vec3::new(t.cos() * 2.0, t.sin() * 2.0, 0.0)
})
.collect();
let soa = SoAPoints::from_vec3_slice(&points);
let soa_results = eval_compiled_batch_soa(&compiled, &soa);
for (i, p) in points.iter().enumerate() {
let expected = eval(&shape, *p);
assert!(
approx_eq(soa_results[i], expected, 0.01),
"SoA/CPU mismatch at {}: soa={}, cpu={}",
i,
soa_results[i],
expected
);
}
}
#[test]
fn test_soa_large_batch() {
let sphere = SdfNode::Sphere { radius: 1.0 };
let compiled = CompiledSdf::compile(&sphere);
let points: Vec<Vec3> = (0..10000)
.map(|i| {
let t = i as f32 / 10000.0;
Vec3::new(t * 4.0 - 2.0, (t * 10.0).sin(), (t * 10.0).cos())
})
.collect();
let soa = SoAPoints::from_vec3_slice(&points);
let results = eval_compiled_batch_soa_parallel(&compiled, &soa);
assert_eq!(results.len(), 10000);
for i in [0, 1000, 5000, 9999] {
let expected = eval(&sphere, points[i]);
assert!(
approx_eq(results[i], expected, 0.01),
"Mismatch at {}: got {}, expected {}",
i,
results[i],
expected
);
}
}
#[test]
fn test_soa_raw_eval() {
let sphere = SdfNode::Sphere { radius: 1.0 };
let compiled = CompiledSdf::compile(&sphere);
let points = vec![
Vec3::ZERO,
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
Vec3::new(0.5, 0.5, 0.5),
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(0.0, -1.0, 0.0),
];
let soa = SoAPoints::from_vec3_slice(&points);
let (px, py, pz) = soa.as_ptrs();
let mut output = vec![0.0f32; 8];
unsafe {
eval_compiled_batch_soa_raw(&compiled, px, py, pz, output.as_mut_ptr(), 8);
}
for (i, p) in points.iter().enumerate() {
let expected = eval(&sphere, *p);
assert!(
approx_eq(output[i], expected, 0.001),
"Raw mismatch at {}: got {}, expected {}",
i,
output[i],
expected
);
}
}
}