1.3.77
 
Loading...
Searching...
No Matches
CollisionDetection_RayTracing.cpp
Go to the documentation of this file.
1
16#include <chrono>
17#include <functional>
18#include <limits>
19#include <queue>
20#include <stack>
21#include <thread>
22#include "CollisionDetection.h"
23
24// SIMD headers
25#ifdef __AVX2__
26#include <immintrin.h>
27#elif defined(__SSE4_1__)
28#include <smmintrin.h>
29#elif defined(__SSE2__)
30#include <emmintrin.h>
31#endif
32
33// MSVC prefetch support
34#ifdef _MSC_VER
35#include <intrin.h>
36#endif
37#include <algorithm>
38
39#ifdef HELIOS_CUDA_AVAILABLE
40#include <cuda_runtime.h>
41#endif
42
43using namespace helios;
44
45#ifdef HELIOS_CUDA_AVAILABLE
46// GPU BVH node structure (must match the one in .cu file)
47struct GPUBVHNode {
48 float3 aabb_min, aabb_max;
49 unsigned int left_child, right_child;
50 unsigned int primitive_start, primitive_count;
51 unsigned int is_leaf, padding;
52};
53
54// External CUDA functions
55extern "C" {
56void launchBVHTraversal(void *h_nodes, int node_count, unsigned int *h_primitive_indices, int primitive_count, float *h_primitive_aabb_min, float *h_primitive_aabb_max, float *h_query_aabb_min, float *h_query_aabb_max, int num_queries,
57 unsigned int *h_results, unsigned int *h_result_counts, int max_results_per_query);
58bool launchVoxelRayPathLengths(int num_rays, float *h_ray_origins, float *h_ray_directions, float grid_center_x, float grid_center_y, float grid_center_z, float grid_size_x, float grid_size_y, float grid_size_z, int grid_divisions_x,
59 int grid_divisions_y, int grid_divisions_z, int primitive_count, int *h_voxel_ray_counts, float *h_voxel_path_lengths, int *h_voxel_transmitted, int *h_voxel_hit_before, int *h_voxel_hit_after, int *h_voxel_hit_inside);
60// Warp-efficient GPU kernels
61void launchWarpEfficientBVH(void *h_bvh_soa_gpu, unsigned int *h_primitive_indices, int primitive_count, float *h_primitive_aabb_min, float *h_primitive_aabb_max, float *h_ray_origins, float *h_ray_directions, float *h_ray_max_distances,
62 int num_rays, unsigned int *h_results, unsigned int *h_result_counts, int max_results_per_ray);
63// High-performance ray-primitive intersection kernel against device-resident scene geometry. Only the per-call ray and
64// result buffers are uploaded/downloaded here; BVH/primitive geometry is passed in as resident device pointers.
65void launchRaysOnResidentScene(void *d_bvh_nodes, int node_count, unsigned int *d_primitive_indices, int primitive_count, int *d_primitive_types, float3 *d_primitive_vertices, unsigned int *d_vertex_offsets, const unsigned char *d_mask_data,
66 const unsigned int *d_mask_offsets, const int *d_mask_sizes, const int *d_mask_IDs, const float *d_uv_data, const int *d_uv_IDs, int total_vertex_count, const float *h_ray_origins, const float *h_ray_directions,
67 const float *h_ray_max_distances, float uniform_max_distance, int num_rays, float *h_hit_distances, unsigned int *h_hit_primitive_ids, unsigned int *h_hit_counts, float *h_hit_normals, bool find_closest_hit);
68}
69
70// Helper function to convert helios::vec3 to float3
71inline float3 heliosVecToFloat3(const helios::vec3 &v) {
72 return make_float3(v.x, v.y, v.z);
73}
74#endif
75
76// -------- GENERIC RAY-TRACING IMPLEMENTATIONS --------
77
79 return castRay(ray_query.origin, ray_query.direction, ray_query.max_distance, ray_query.target_UUIDs);
80}
81
82CollisionDetection::HitResult CollisionDetection::castRay(const vec3 &origin, const vec3 &direction, float max_distance, const std::vector<uint> &target_UUIDs) {
83 HitResult result;
84
85 // Normalize direction vector
86 vec3 ray_direction = direction;
87 if (ray_direction.magnitude() < 1e-8f) {
88 return result; // Invalid direction
89 }
90 ray_direction = ray_direction / ray_direction.magnitude();
91
92 // Ensure BVH is current before ray casting (handles automatic rebuilds)
93 const_cast<CollisionDetection *>(this)->ensureBVHCurrent();
94
95 // CRITICAL FIX: Use BVH traversal when available for performance
96 // This fixes the 10x+ performance regression by replacing brute-force primitive testing
97 if (!bvh_nodes.empty()) {
98 // Build primitive cache for thread-safe access if not already built (keeps the dense cache in sync too)
99 ensurePrimitiveCacheCurrent();
100
101 // Use BVH traversal for both filtered and unfiltered queries - BVH supports UUID filtering
102 RayQuery query(origin, ray_direction, max_distance, target_UUIDs);
103 return castRayBVHTraversal(query);
104 }
105
106 // FALLBACK: Brute-force primitive testing (only when no BVH available or target UUIDs specified)
107 // This is the slow path that was causing the performance regression
108 std::vector<uint> search_primitives;
109 if (target_UUIDs.empty()) {
110 // Use cached primitives if available (thread-safe), otherwise use context (thread-unsafe)
111 if (!primitive_cache.empty()) {
112 // Use cached primitive IDs for thread-safe operation
113 search_primitives.reserve(primitive_cache.size());
114 for (const auto &cached_pair: primitive_cache) {
115 search_primitives.push_back(cached_pair.first);
116 }
117 } else {
118 // Fall back to context call (thread-unsafe)
119 search_primitives = context->getAllUUIDs();
120 }
121 } else {
122 search_primitives = target_UUIDs;
123 }
124
125 // Find nearest intersection
126 float nearest_distance = std::numeric_limits<float>::max();
127 if (max_distance > 0) {
128 nearest_distance = max_distance;
129 }
130
131 uint hit_primitive = 0;
132 bool found_intersection = false;
133
134 // Test each primitive for intersection
135 for (uint candidate_uuid: search_primitives) {
136 float intersection_distance;
137 bool hit = false;
138
139 // Use thread-safe cached intersection if available
140 if (!primitive_cache.empty()) {
141 HitResult primitive_result = intersectPrimitiveThreadSafe(origin, ray_direction, candidate_uuid, max_distance);
142 if (primitive_result.hit) {
143 hit = true;
144 intersection_distance = primitive_result.distance;
145 }
146 } else {
147 // Fall back to thread-unsafe context call
148 if (!context->doesPrimitiveExist(candidate_uuid)) {
149 continue;
150 }
151 hit = rayPrimitiveIntersection(origin, ray_direction, candidate_uuid, intersection_distance);
152
153 // Reject hits on transparent texels so the ray passes through to geometry behind.
154 if (hit && context->primitiveTextureHasTransparencyChannel(candidate_uuid)) {
155 CachedPrimitive temp_cached(context->getPrimitiveType(candidate_uuid), context->getPrimitiveVertices(candidate_uuid));
156 temp_cached.transparency_mask = context->getPrimitiveTextureTransparencyData(candidate_uuid);
157 temp_cached.texture_size = context->getPrimitiveTextureSize(candidate_uuid);
158 temp_cached.uv = context->getPrimitiveTextureUV(candidate_uuid);
159 if (!isHitTexelOpaque(temp_cached, origin + ray_direction * intersection_distance)) {
160 hit = false;
161 }
162 }
163 }
164
165 if (hit) {
166 // Check distance constraints
167 if (intersection_distance > 1e-6f && // Avoid self-intersection
168 intersection_distance < nearest_distance) { // Find nearest
169
170 nearest_distance = intersection_distance;
171 hit_primitive = candidate_uuid;
172 found_intersection = true;
173 }
174 }
175 }
176
177 // Fill result
178 if (found_intersection) {
179 result.hit = true;
180 result.distance = nearest_distance;
181 result.primitive_UUID = hit_primitive;
182 result.intersection_point = origin + ray_direction * nearest_distance;
183
184 // Calculate surface normal
185 try {
186 PrimitiveType type = context->getPrimitiveType(hit_primitive);
187 std::vector<vec3> vertices = context->getPrimitiveVertices(hit_primitive);
188
189 if (type == PRIMITIVE_TYPE_TRIANGLE && vertices.size() >= 3) {
190 // Calculate triangle normal
191 vec3 v0 = vertices[0];
192 vec3 v1 = vertices[1];
193 vec3 v2 = vertices[2];
194 vec3 edge1 = v1 - v0;
195 vec3 edge2 = v2 - v0;
196 result.normal = cross(edge1, edge2);
197 result.normal = result.normal / result.normal.magnitude();
198 } else if (type == PRIMITIVE_TYPE_PATCH && vertices.size() >= 4) {
199 // Calculate patch normal (assuming quad)
200 vec3 v0 = vertices[0];
201 vec3 v1 = vertices[1];
202 vec3 v2 = vertices[2];
203 vec3 edge1 = v1 - v0;
204 vec3 edge2 = v2 - v0;
205 result.normal = cross(edge1, edge2);
206 result.normal = result.normal / result.normal.magnitude();
207 } else {
208 // Default normal (pointing back along ray)
209 result.normal = make_vec3(-ray_direction.x, -ray_direction.y, -ray_direction.z);
210 }
211 } catch (const std::exception &e) {
212 // If we can't get surface normal, use default
213 result.normal = make_vec3(-ray_direction.x, -ray_direction.y, -ray_direction.z);
214 }
215 }
216
217 return result;
218}
219
220
221bool CollisionDetection::shouldUseGPU(size_t ray_count) const {
222#ifdef HELIOS_CUDA_AVAILABLE
223 // Use GPU only for large batches (amortize launch + transfer overhead) on a complex, already-resident scene.
224 // CPU is faster for small batches. Mirrors the thresholds the vector dispatch has always used.
225 constexpr size_t GPU_BATCH_THRESHOLD = 1000000; // batches >= 1M rays
226 constexpr size_t MIN_PRIMITIVES_FOR_GPU = 500; // minimum scene complexity
227 return gpu_acceleration_enabled && ray_count >= GPU_BATCH_THRESHOLD && d_bvh_nodes != nullptr && d_primitive_vertices != nullptr && !primitive_indices.empty() && primitive_indices.size() >= MIN_PRIMITIVES_FOR_GPU;
228#else
229 (void) ray_count;
230 return false;
231#endif
232}
233
234std::vector<CollisionDetection::HitResult> CollisionDetection::castRays(const std::vector<RayQuery> &ray_queries, RayTracingStats *stats) {
235 std::vector<HitResult> results;
236 results.reserve(ray_queries.size());
237
238 // Initialize statistics
239 RayTracingStats local_stats;
240 local_stats.total_rays_cast = ray_queries.size();
241
242 // Smart CPU/GPU selection via the shared predicate (see shouldUseGPU): GPU only pays off for large batches on a
243 // sufficiently complex, GPU-resident scene. Keeping the predicate in one place ensures castRays and castRaysSoA
244 // never diverge in how they choose GPU vs CPU.
245#ifdef HELIOS_CUDA_AVAILABLE
246 if (shouldUseGPU(ray_queries.size())) {
247 castRaysGPU(ray_queries, results, local_stats);
248 } else {
249 castRaysCPU(ray_queries, results, local_stats);
250 }
251#else
252 // Use CPU implementation when GPU not available
253 castRaysCPU(ray_queries, results, local_stats);
254#endif
255
256 // Copy statistics to output parameter if provided
257 if (stats != nullptr) {
258 *stats = local_stats;
259 }
260
261 return results;
262}
263
264void CollisionDetection::castRaysCPU(const std::vector<RayQuery> &ray_queries, std::vector<HitResult> &results, RayTracingStats &stats) {
265 // Use optimized batch processing - fail explicitly if there are issues
266 results = castRaysOptimized(ray_queries, &stats);
267}
268
269#ifdef HELIOS_CUDA_AVAILABLE
270void CollisionDetection::castRaysGPU(const std::vector<RayQuery> &ray_queries, std::vector<HitResult> &results, RayTracingStats &stats) {
271 // Use the high-performance GPU ray-triangle intersection implementation
272 results = castRaysGPU(ray_queries, stats);
273}
274#endif
275
276std::vector<std::vector<std::vector<std::vector<CollisionDetection::HitResult>>>> CollisionDetection::performGridRayIntersection(const vec3 &grid_center, const vec3 &grid_size, const helios::int3 &grid_divisions,
277 const std::vector<RayQuery> &ray_queries) {
278
279 // Initialize result grid
280 std::vector<std::vector<std::vector<std::vector<HitResult>>>> grid_results;
281 grid_results.resize(grid_divisions.x);
282 for (int i = 0; i < grid_divisions.x; i++) {
283 grid_results[i].resize(grid_divisions.y);
284 for (int j = 0; j < grid_divisions.y; j++) {
285 grid_results[i][j].resize(grid_divisions.z);
286 }
287 }
288
289 // Calculate voxel size
290 vec3 voxel_size = make_vec3(grid_size.x / float(grid_divisions.x), grid_size.y / float(grid_divisions.y), grid_size.z / float(grid_divisions.z));
291
292 // Process each ray
293 for (const auto &query: ray_queries) {
294 HitResult hit_result = castRay(query);
295
296 if (hit_result.hit) {
297 // Determine which voxel the hit point falls into
298 vec3 relative_pos = hit_result.intersection_point - (grid_center - grid_size * 0.5f);
299
300 int voxel_i = int(relative_pos.x / voxel_size.x);
301 int voxel_j = int(relative_pos.y / voxel_size.y);
302 int voxel_k = int(relative_pos.z / voxel_size.z);
303
304 // Check bounds
305 if (voxel_i >= 0 && voxel_i < grid_divisions.x && voxel_j >= 0 && voxel_j < grid_divisions.y && voxel_k >= 0 && voxel_k < grid_divisions.z) {
306
307 grid_results[voxel_i][voxel_j][voxel_k].push_back(hit_result);
308 }
309 }
310 }
311
312 return grid_results;
313}
314
315std::vector<std::vector<CollisionDetection::HitResult>> CollisionDetection::calculateVoxelPathLengths(const vec3 &scan_origin, const std::vector<vec3> &ray_directions, const std::vector<vec3> &voxel_centers, const std::vector<vec3> &voxel_sizes) {
316
317 if (ray_directions.empty()) {
318 if (printmessages) {
319 std::cout << "WARNING (CollisionDetection::calculateVoxelPathLengths): No rays provided" << std::endl;
320 }
321 return std::vector<std::vector<HitResult>>();
322 }
323
324 if (voxel_centers.size() != voxel_sizes.size()) {
325 helios_runtime_error("ERROR (CollisionDetection::calculateVoxelPathLengths): voxel_centers and voxel_sizes vectors must have same size");
326 }
327
328 if (voxel_centers.empty()) {
329 if (printmessages) {
330 std::cout << "WARNING (CollisionDetection::calculateVoxelPathLengths): No voxels provided" << std::endl;
331 }
332 return std::vector<std::vector<HitResult>>();
333 }
334
335 const size_t num_rays = ray_directions.size();
336 const size_t num_voxels = voxel_centers.size();
337
338 if (printmessages) {
339 std::cout << "Calculating voxel path lengths for " << num_rays << " rays through " << num_voxels << " voxels..." << std::endl;
340 }
341
342 // Initialize result structure - one vector of HitResults per voxel
343 std::vector<std::vector<HitResult>> result(num_voxels);
344
345// OpenMP parallel loop over rays for performance
346#pragma omp parallel for schedule(dynamic, 1000)
347 for (int ray_idx = 0; ray_idx < static_cast<int>(num_rays); ++ray_idx) {
348 const vec3 &ray_direction = ray_directions[ray_idx];
349
350 // Process each voxel for this ray
351 for (size_t voxel_idx = 0; voxel_idx < num_voxels; ++voxel_idx) {
352 const vec3 &voxel_center = voxel_centers[voxel_idx];
353 const vec3 &voxel_size = voxel_sizes[voxel_idx];
354
355 // Calculate voxel AABB from center and size
356 const vec3 half_size = voxel_size * 0.5f;
357 const vec3 voxel_min = voxel_center - half_size;
358 const vec3 voxel_max = voxel_center + half_size;
359
360 // Perform ray-AABB intersection test
361 float t_min, t_max;
362 if (rayAABBIntersect(scan_origin, ray_direction, voxel_min, voxel_max, t_min, t_max)) {
363 // Calculate path length: t_max - max(0, t_min)
364 const float path_length = t_max - std::max(0.0f, t_min);
365
366 if (path_length > 1e-6f) { // Only count meaningful intersections
367 // Create HitResult with path length information
368 HitResult hit_result;
369 hit_result.hit = false; // No actual primitive hit, just voxel traversal
370 hit_result.distance = -1.0f; // Not applicable for voxel traversal
371 hit_result.primitive_UUID = 0; // No primitive
372 hit_result.intersection_point = make_vec3(0, 0, 0); // Not applicable
373 hit_result.normal = make_vec3(0, 0, 0); // Not applicable
374 hit_result.path_length = path_length; // This is what we want!
375
376// Thread-safe update of results
377#pragma omp critical
378 {
379 result[voxel_idx].push_back(hit_result);
380 }
381 }
382 }
383 }
384 }
385
386 if (printmessages) {
387 size_t total_intersections = 0;
388 for (size_t i = 0; i < num_voxels; ++i) {
389 total_intersections += result[i].size();
390 }
391 std::cout << "Completed voxel path length calculations. Total ray-voxel intersections: " << total_intersections << std::endl;
392 }
393
394 return result;
395}
396
397void CollisionDetection::calculateRayPathLengthsDetailed(const vec3 &grid_center, const vec3 &grid_size, const helios::int3 &grid_divisions, const std::vector<vec3> &ray_origins, const std::vector<vec3> &ray_directions,
398 std::vector<HitResult> &hit_results) {
399
400 hit_results.clear();
401 hit_results.reserve(ray_origins.size());
402
403 if (ray_origins.size() != ray_directions.size()) {
404 helios_runtime_error("ERROR (CollisionDetection::calculateRayPathLengthsDetailed): ray_origins and ray_directions must have the same size");
405 return;
406 }
407
408 // Also update the existing voxel data structures
409 calculateVoxelRayPathLengths(grid_center, grid_size, grid_divisions, ray_origins, ray_directions);
410
411 // Cast each ray and collect detailed results
412 for (size_t i = 0; i < ray_origins.size(); i++) {
413 RayQuery query(ray_origins[i], ray_directions[i]);
414 HitResult result = castRay(query);
415 hit_results.push_back(result);
416 }
417}
418
419// ================================================================
420// PHASE 2 OPTIMIZATION METHODS: Structure-of-Arrays & Quantization
421// ================================================================
422
424 if (mode == bvh_optimization_mode) {
425 return; // No change needed
426 }
427
428 BVHOptimizationMode old_mode = bvh_optimization_mode;
429 bvh_optimization_mode = mode;
430
431 // Build optimized structures when mode changes
432 if (old_mode != mode && !bvh_nodes.empty()) {
433 if (printmessages) {
434 std::cout << "CollisionDetection: Converting BVH from mode " << static_cast<int>(old_mode) << " to mode " << static_cast<int>(mode) << std::endl;
435 }
436
437 // Build the optimized structures immediately
438 ensureOptimizedBVH();
439
440 if (printmessages) {
441 auto memory_stats = getBVHMemoryUsage();
442 std::cout << "CollisionDetection: Memory usage - SoA: " << memory_stats.soa_memory_bytes << " bytes, Quantized: " << memory_stats.quantized_memory_bytes << " bytes (" << memory_stats.quantized_reduction_percent << "% reduction)"
443 << std::endl;
444 }
445 }
446}
447
451
452void CollisionDetection::convertBVHLayout(BVHOptimizationMode from_mode, BVHOptimizationMode to_mode) {
453 // With only SOA_UNCOMPRESSED mode remaining, no conversion needed
454 return;
455}
456
457
458std::vector<CollisionDetection::HitResult> CollisionDetection::castRaysOptimized(const std::vector<RayQuery> &ray_queries, RayTracingStats *stats) {
459 if (ray_queries.empty()) {
460 return {};
461 }
462
463 // Ensure BVH is current and optimized structures are available
464 ensureBVHCurrent();
465 ensureOptimizedBVH();
466
467 // Build primitive cache for high-performance thread-safe primitive intersection (dense cache kept in sync)
468 ensurePrimitiveCacheCurrent();
469
470 RayTracingStats local_stats;
471 std::vector<HitResult> results;
472 results.reserve(ray_queries.size());
473
474 auto start_time = std::chrono::high_resolution_clock::now();
475
476 // Dispatch to appropriate optimized method based on current mode
477 switch (bvh_optimization_mode) {
479 results = castRaysSoA(ray_queries, local_stats);
480 break;
481 }
482
483 auto end_time = std::chrono::high_resolution_clock::now();
484 auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
485
486 if (stats) {
487 *stats = local_stats;
488 }
489
490 return results;
491}
492
494 if (ray_stream.packets.empty()) {
495 return true;
496 }
497
498 RayTracingStats combined_stats;
499 bool success = true;
500
501 for (auto &packet: ray_stream.packets) {
502 // Convert packet to ray queries
503 auto queries = packet.toRayQueries();
504
505 // Process the packet using optimized ray casting
506 RayTracingStats packet_stats;
507 auto results = castRaysOptimized(queries, &packet_stats);
508
509 if (results.size() != queries.size()) {
510 success = false;
511 continue;
512 }
513
514 // Store results back in packet
515 packet.results = std::move(results);
516
517 // Accumulate statistics
518 combined_stats.total_rays_cast += packet_stats.total_rays_cast;
519 combined_stats.total_hits += packet_stats.total_hits;
520 combined_stats.bvh_nodes_visited += packet_stats.bvh_nodes_visited;
521 combined_stats.average_ray_distance = (combined_stats.average_ray_distance * (combined_stats.total_rays_cast - packet_stats.total_rays_cast) + packet_stats.average_ray_distance * packet_stats.total_rays_cast) / combined_stats.total_rays_cast;
522 }
523
524 if (stats) {
525 *stats = combined_stats;
526 }
527
528 if (printmessages && success) {
529 std::cout << "CollisionDetection: Processed " << ray_stream.packets.size() << " ray packets (" << ray_stream.total_rays << " total rays)" << std::endl;
530 }
531
532 return success;
533}
534
535CollisionDetection::MemoryUsageStats CollisionDetection::getBVHMemoryUsage() const {
536 // Ensure optimized structures are built before calculating memory usage
537 const_cast<CollisionDetection *>(this)->ensureOptimizedBVH();
538
539 MemoryUsageStats stats;
540
541 // Calculate SoA memory usage
542 stats.soa_memory_bytes = bvh_nodes_soa.getMemoryUsage();
543
544 // With quantized mode removed, set quantized values to 0
545 stats.quantized_memory_bytes = 0;
546 stats.quantized_reduction_percent = 0.0f;
547
548 return stats;
549}
550
551void CollisionDetection::castRaysSoA(const helios::vec3 *origins, const helios::vec3 *directions, size_t count, float max_distance, float *out_distance, helios::vec3 *out_normal, uint *out_primitive_UUID, RayTracingStats *stats) {
552
553 // Low-memory SoA batch cast: no intermediate RayQuery/HitResult vectors. Results are written straight into the
554 // caller-owned output arrays; a miss is signalled by out_primitive_UUID == MISS_UUID (out_distance/out_normal are
555 // then unspecified). Mirrors the parallel traversal of the vector-based castRaysSoA() but reuses the per-ray kernel
556 // castRaySoATraversal() directly.
557 constexpr uint MISS_UUID = 0xFFFFFFFFu;
558
559 RayTracingStats local_stats;
560 local_stats.total_rays_cast = count;
561
562 if (count == 0) {
563 if (stats != nullptr) {
564 *stats = local_stats;
565 }
566 return;
567 }
568
569 // Snapshot the external cancellation flag (see the vector-based castRaysSoA overload). When it is already set, or
570 // flips mid-trace, the batch short-circuits: the GPU launch is skipped entirely and the CPU loop drains its
571 // remaining indices cheaply, marking every un-traced ray as a miss so the caller frees the batch and stops.
572 volatile int *const cancel = cancel_flag;
573 if (cancel != nullptr && *cancel != 0) {
574 for (size_t i = 0; i < count; i++) {
575 out_primitive_UUID[i] = MISS_UUID;
576 }
577 if (stats != nullptr) {
578 *stats = local_stats;
579 }
580 return;
581 }
582
583 // Ensure the BVH and primitive cache are current (same prerequisites as castRaysOptimized()). Automatic BVH rebuilds
584 // are intentionally NOT toggled here — a caller issuing many batched calls over static geometry should disable
585 // automatic rebuilds and buildBVH() once around the whole batch.
586 ensureBVHCurrent();
587
588#ifdef HELIOS_CUDA_AVAILABLE
589 // GPU fast path: for large batches on a GPU-resident scene, trace on the device and write results straight into the
590 // caller arrays. The scene geometry is uploaded once per scan by buildBVH()/transferBVHToGPU() (driven by the
591 // LiDAR prepare/finish bracket), so only this batch's ray + result buffers move across the bus here — keeping a
592 // chunked synthetic scan from re-uploading the whole scene per chunk. shouldUseGPU() also guards that the scene is
593 // actually resident (d_primitive_vertices != nullptr); otherwise we fall through to the CPU traversal below.
594 if (shouldUseGPU(count)) {
595 // The CPU convention "max_distance <= 0 => unbounded" maps to a large finite kernel cutoff, broadcast to every
596 // ray (no per-ray distance array needed).
597 const float kernel_max_distance = (max_distance > 0) ? max_distance : std::numeric_limits<float>::max();
598
599 // helios::vec3 is three contiguous floats == float3, so the caller's origins/directions are passed straight to
600 // the device with no host repack (directions are normalized in the kernel). Results are written directly into the
601 // caller's out_distance / out_primitive_UUID / out_normal — no intermediate host staging. The kernel reports a
602 // miss as out_primitive_UUID == 0xFFFFFFFF, which is exactly the SoA MISS_UUID sentinel, and hit_counts is not
603 // needed (passed null) since the UUID already distinguishes hit from miss.
604 launchRaysOnResidentScene(d_bvh_nodes, d_gpu_node_count, d_primitive_indices, d_gpu_primitive_count, d_primitive_types, (float3 *) d_primitive_vertices, d_vertex_offsets, (const unsigned char *) d_mask_data, d_mask_offsets, d_mask_sizes,
605 d_mask_IDs, (const float *) d_uv_data, d_uv_IDs, d_gpu_total_vertex_count, reinterpret_cast<const float *>(origins), reinterpret_cast<const float *>(directions), /*h_ray_max_distances=*/nullptr, kernel_max_distance,
606 static_cast<int>(count), out_distance, out_primitive_UUID, /*h_hit_counts=*/nullptr, reinterpret_cast<float *>(out_normal), true);
607
608 // Stats only: out_distance / out_primitive_UUID / out_normal are already populated by the launch above. The kernel
609 // bounds every recorded hit by kernel_max_distance, so a non-sentinel UUID is exactly an in-range hit.
610 size_t total_hits = 0;
611 double dist_sum = 0.0;
612 for (size_t i = 0; i < count; i++) {
613 if (out_primitive_UUID[i] != MISS_UUID) {
614 total_hits++;
615 dist_sum += out_distance[i];
616 }
617 }
618 local_stats.total_hits = total_hits;
619 local_stats.average_ray_distance = (total_hits > 0) ? (dist_sum / static_cast<double>(total_hits)) : 0.0;
620 if (stats != nullptr) {
621 *stats = local_stats;
622 }
623 return;
624 }
625#endif
626
627 ensureOptimizedBVH();
628 ensurePrimitiveCacheCurrent();
629
630 if (bvh_nodes_soa.node_count == 0) {
631 // No SoA BVH: every ray misses.
632 for (size_t i = 0; i < count; i++) {
633 out_primitive_UUID[i] = MISS_UUID;
634 }
635 if (stats != nullptr) {
636 *stats = local_stats;
637 }
638 return;
639 }
640
641#pragma omp parallel
642 {
643 RayTracingStats thread_stats = {}; // thread-local statistics
644
645#pragma omp for schedule(guided, 32)
646 for (long long i = 0; i < static_cast<long long>(count); ++i) {
647 if (cancel != nullptr && *cancel != 0) {
648 out_primitive_UUID[i] = MISS_UUID; // run cancelled — drain remaining indices cheaply as misses
649 continue;
650 }
651 RayQuery query(origins[i], directions[i], max_distance); // stack-local; empty target_UUIDs => all primitives
652 HitResult result = castRaySoATraversal(query, thread_stats);
653
654 if (result.hit) {
655 out_primitive_UUID[i] = result.primitive_UUID;
656 out_distance[i] = result.distance;
657 out_normal[i] = result.normal;
658 thread_stats.total_hits++;
659 thread_stats.average_ray_distance += result.distance;
660 } else {
661 out_primitive_UUID[i] = MISS_UUID;
662 }
663 }
664
665#pragma omp atomic
666 local_stats.total_hits += thread_stats.total_hits;
667#pragma omp atomic
668 local_stats.average_ray_distance += thread_stats.average_ray_distance;
669#pragma omp atomic
670 local_stats.bvh_nodes_visited += thread_stats.bvh_nodes_visited;
671 }
672
673 if (local_stats.total_hits > 0) {
674 local_stats.average_ray_distance /= local_stats.total_hits;
675 }
676
677 if (stats != nullptr) {
678 *stats = local_stats;
679 }
680}
681
682void CollisionDetection::castRaysSoA_packets(const helios::vec3 *origins, const helios::vec3 *directions, size_t count, size_t packet_size, float max_distance, float *out_distance, helios::vec3 *out_normal, uint *out_primitive_UUID,
683 RayTracingStats *stats) {
684 constexpr uint MISS_UUID = 0xFFFFFFFFu;
685
686 RayTracingStats local_stats;
687 local_stats.total_rays_cast = count;
688
689 if (count == 0) {
690 if (stats != nullptr) {
691 *stats = local_stats;
692 }
693 return;
694 }
695
696 // packet_size 0 or 1 has no coherence to exploit: defer to the per-ray path (which also covers the GPU fast path).
697 if (packet_size <= 1) {
698 castRaysSoA(origins, directions, count, max_distance, out_distance, out_normal, out_primitive_UUID, stats);
699 return;
700 }
701
702 volatile int *const cancel = cancel_flag;
703 if (cancel != nullptr && *cancel != 0) {
704 for (size_t i = 0; i < count; i++) {
705 out_primitive_UUID[i] = MISS_UUID;
706 }
707 if (stats != nullptr) {
708 *stats = local_stats;
709 }
710 return;
711 }
712
713 ensureBVHCurrent();
714
715 // The packet traversal is a CPU-only optimization. On a GPU-resident scene large enough to favor the device, defer
716 // to the per-ray path which dispatches to the GPU kernel (per-ray on the GPU is already massively parallel and the
717 // packet sharing would not map onto it). shouldUseGPU is false when no GPU build / scene is resident.
718 if (shouldUseGPU(count)) {
719 castRaysSoA(origins, directions, count, max_distance, out_distance, out_normal, out_primitive_UUID, stats);
720 return;
721 }
722
723 ensureOptimizedBVH();
724 ensurePrimitiveCacheCurrent();
725
726 if (bvh_nodes_soa.node_count == 0) {
727 for (size_t i = 0; i < count; i++) {
728 out_primitive_UUID[i] = MISS_UUID;
729 }
730 if (stats != nullptr) {
731 *stats = local_stats;
732 }
733 return;
734 }
735
736 const size_t num_packets = (count + packet_size - 1) / packet_size;
737
738#pragma omp parallel
739 {
740 RayTracingStats thread_stats = {};
741
742#pragma omp for schedule(guided, 8)
743 for (long long pkt = 0; pkt < static_cast<long long>(num_packets); ++pkt) {
744 const size_t begin = size_t(pkt) * packet_size;
745 const size_t end = std::min(begin + packet_size, count);
746
747 if (cancel != nullptr && *cancel != 0) {
748 for (size_t i = begin; i < end; i++) {
749 out_primitive_UUID[i] = MISS_UUID;
750 }
751 continue;
752 }
753
754 castPacketSoATraversal(origins, directions, begin, end, max_distance, out_distance, out_normal, out_primitive_UUID, thread_stats);
755 }
756
757#pragma omp atomic
758 local_stats.total_hits += thread_stats.total_hits;
759#pragma omp atomic
760 local_stats.average_ray_distance += thread_stats.average_ray_distance;
761#pragma omp atomic
762 local_stats.bvh_nodes_visited += thread_stats.bvh_nodes_visited;
763 }
764
765 if (local_stats.total_hits > 0) {
766 local_stats.average_ray_distance /= local_stats.total_hits;
767 }
768 if (stats != nullptr) {
769 *stats = local_stats;
770 }
771}
772
773void CollisionDetection::castPacketSoATraversal(const helios::vec3 *origins, const helios::vec3 *directions, size_t begin, size_t end, float max_distance, float *out_distance, helios::vec3 *out_normal, uint *out_primitive_UUID,
774 RayTracingStats &stats) {
775 constexpr uint MISS_UUID = 0xFFFFFFFFu;
776 const size_t n = end - begin;
777
778 // Per-ray traversal state for this packet (small, stack-allocated). Packets larger than MAX_PACKET_RAYS are split
779 // into sub-packets so the fixed-size buffers never overflow; LiDAR pulses are ~10-200 sub-rays so this is rare.
780 constexpr size_t MAX_PACKET_RAYS = 256;
781 if (n > MAX_PACKET_RAYS) {
782 for (size_t sub = begin; sub < end; sub += MAX_PACKET_RAYS) {
783 castPacketSoATraversal(origins, directions, sub, std::min(sub + MAX_PACKET_RAYS, end), max_distance, out_distance, out_normal, out_primitive_UUID, stats);
784 }
785 return;
786 }
787
788 const bool use_dense_cache = (primitive_cache_dense.size() == primitive_indices.size());
789
790 // Per-ray running closest-hit distance and best hit so far.
791 float closest[MAX_PACKET_RAYS];
792 HitResult best[MAX_PACKET_RAYS];
793 vec3 ray_origin[MAX_PACKET_RAYS];
794 vec3 ray_dir[MAX_PACKET_RAYS];
795 const float init_far = (max_distance > 0) ? max_distance : std::numeric_limits<float>::max();
796 for (size_t r = 0; r < n; r++) {
797 ray_origin[r] = origins[begin + r];
798 ray_dir[r] = directions[begin + r];
799 closest[r] = init_far;
800 // best[r] default-constructed: hit == false
801 }
802
803 // Fixed-size shared traversal stack (BVH depth bounded by buildBVHRecursive MAX_DEPTH).
804 constexpr int STACK_CAPACITY = 128;
805 uint32_t node_stack[STACK_CAPACITY];
806 int stack_size = 0;
807 node_stack[stack_size++] = 0; // root
808
809 while (stack_size > 0) {
810 uint32_t node_idx = node_stack[--stack_size];
811 if (node_idx >= bvh_nodes_soa.node_count) {
812 continue;
813 }
814
815 if (bvh_nodes_soa.is_leaf_flags[node_idx]) {
816 // Active-ray mask: only rays whose AABB (with their own running closest distance) intersects this leaf's node
817 // can possibly hit a primitive inside it (the node AABB bounds all its primitives). Test the primitive against
818 // just those rays, not all n. For a coherent LiDAR pulse only ~1/3 of the sub-rays reach a given leaf, so this
819 // avoids ~2/3 of the (expensive) ray-primitive intersections while remaining identical to the per-ray result.
820 int active_idx[MAX_PACKET_RAYS];
821 int active_count = 0;
822 for (size_t r = 0; r < n; r++) {
823 if (aabbIntersectSoA(ray_origin[r], ray_dir[r], closest[r], node_idx)) {
824 active_idx[active_count++] = int(r);
825 }
826 }
827 if (active_count == 0) {
828 continue;
829 }
830 stats.bvh_nodes_visited++;
831
832 const uint32_t primitive_start = bvh_nodes_soa.primitive_starts[node_idx];
833 const uint32_t primitive_count = bvh_nodes_soa.primitive_counts[node_idx];
834
835 for (uint32_t i = 0; i < primitive_count; ++i) {
836 const uint32_t slot = primitive_start + i;
837 const uint primitive_id = primitive_indices[slot];
838
839 // Fetch the primitive once for the packet, then test it against the active rays only.
840 for (int a = 0; a < active_count; ++a) {
841 const int r = active_idx[a];
842 HitResult pr = use_dense_cache ? intersectCachedPrimitive(ray_origin[r], ray_dir[r], primitive_cache_dense[slot], closest[r]) : intersectPrimitiveThreadSafe(ray_origin[r], ray_dir[r], primitive_id, closest[r]);
843 if (pr.hit && pr.distance < closest[r]) {
844 best[r] = pr;
845 closest[r] = pr.distance;
846 }
847 }
848 }
849 } else {
850 // Internal node: descend if ANY ray (using its own closest distance) intersects the node AABB. Early-break —
851 // the full active set is only needed at leaves.
852 bool any_hit_box = false;
853 for (size_t r = 0; r < n; r++) {
854 if (aabbIntersectSoA(ray_origin[r], ray_dir[r], closest[r], node_idx)) {
855 any_hit_box = true;
856 break;
857 }
858 }
859 if (!any_hit_box) {
860 continue;
861 }
862 stats.bvh_nodes_visited++;
863
864 const uint32_t left_child = bvh_nodes_soa.left_children[node_idx];
865 const uint32_t right_child = bvh_nodes_soa.right_children[node_idx];
866 const bool left_valid = (left_child != 0xFFFFFFFF && left_child < bvh_nodes_soa.node_count);
867 const bool right_valid = (right_child != 0xFFFFFFFF && right_child < bvh_nodes_soa.node_count);
868
869 if (left_valid && right_valid) {
870 // Near-first ordering using the packet's central ray (sub-ray 0 = the beam nominal axis): push the farther
871 // child first so the nearer is popped first, tightening the running closest sooner (which then lets the
872 // active-ray test above prune more leaves). Affects only traversal order, not the result.
873 const float t_left = aabbEntryDistanceSoA(ray_origin[0], ray_dir[0], left_child);
874 const float t_right = aabbEntryDistanceSoA(ray_origin[0], ray_dir[0], right_child);
875 uint32_t first = left_child, second = right_child;
876 if (t_right < t_left) {
877 first = right_child;
878 second = left_child;
879 }
880 if (stack_size < STACK_CAPACITY)
881 node_stack[stack_size++] = second; // farther pushed first (popped last)
882 if (stack_size < STACK_CAPACITY)
883 node_stack[stack_size++] = first; // nearer pushed last (popped first)
884 } else if (left_valid) {
885 if (stack_size < STACK_CAPACITY)
886 node_stack[stack_size++] = left_child;
887 } else if (right_valid) {
888 if (stack_size < STACK_CAPACITY)
889 node_stack[stack_size++] = right_child;
890 }
891 }
892 }
893
894 // Write packet results.
895 for (size_t r = 0; r < n; r++) {
896 if (best[r].hit) {
897 out_primitive_UUID[begin + r] = best[r].primitive_UUID;
898 out_distance[begin + r] = best[r].distance;
899 out_normal[begin + r] = best[r].normal;
900 stats.total_hits++;
901 stats.average_ray_distance += best[r].distance;
902 } else {
903 out_primitive_UUID[begin + r] = MISS_UUID;
904 }
905 }
906}
907
908std::vector<CollisionDetection::HitResult> CollisionDetection::castRaysSoA(const std::vector<RayQuery> &ray_queries, RayTracingStats &stats) {
909 std::vector<HitResult> results;
910 results.reserve(ray_queries.size());
911
912 if (bvh_nodes_soa.node_count == 0) {
913 // Return empty results if no SoA BVH, but still set statistics
914 results.resize(ray_queries.size());
915 stats.total_rays_cast = ray_queries.size();
916 stats.total_hits = 0;
917 stats.bvh_nodes_visited = 0;
918 stats.average_ray_distance = 0.0f;
919 return results;
920 }
921
922 stats.total_rays_cast = ray_queries.size();
923 stats.total_hits = 0;
924 stats.average_ray_distance = 0.0;
925
926 // Resize results vector for parallel access
927 results.resize(ray_queries.size());
928
929 // Snapshot the external cancellation flag so every thread reads the same
930 // pointer. When it flips non-zero mid-trace, each thread short-circuits its
931 // remaining loop indices (OpenMP forbids breaking out of a worksharing loop,
932 // so we skip-cheaply instead). The volatile load is re-read each iteration
933 // and is effectively free next to a BVH traversal; left-over results[i] for
934 // skipped rays stay default-constructed (no hit), which the caller discards.
935 volatile int *const cancel = cancel_flag;
936
937// OpenMP parallel ray processing for high-performance SoA traversal
938#pragma omp parallel
939 {
940 RayTracingStats local_stats = {}; // Thread-local statistics
941
942#pragma omp for schedule(guided, 32)
943 for (int i = 0; i < static_cast<int>(ray_queries.size()); ++i) {
944 if (cancel != nullptr && *cancel != 0) {
945 continue; // run cancelled — drain remaining indices cheaply
946 }
947 HitResult result = castRaySoATraversal(ray_queries[i], local_stats);
948 results[i] = result;
949
950 if (result.hit) {
951 local_stats.total_hits++;
952 local_stats.average_ray_distance += result.distance;
953 }
954 }
955
956// Combine thread-local statistics atomically
957#pragma omp atomic
958 stats.total_hits += local_stats.total_hits;
959
960#pragma omp atomic
961 stats.average_ray_distance += local_stats.average_ray_distance;
962
963#pragma omp atomic
964 stats.bvh_nodes_visited += local_stats.bvh_nodes_visited;
965 }
966
967 if (stats.total_hits > 0) {
968 stats.average_ray_distance /= stats.total_hits;
969 }
970
971 return results;
972}
973
974// Optimized BVH traversal methods using Structure-of-Arrays layout
975
976#include "CollisionDetection.h"
977
978using namespace helios;
979
980CollisionDetection::HitResult CollisionDetection::castRaySoATraversal(const RayQuery &query, RayTracingStats &stats) {
981 HitResult result;
982
983 if (bvh_nodes_soa.node_count == 0 || bvh_nodes_soa.aabb_mins.empty()) {
984 return result; // No BVH built
985 }
986
987 // Fixed-size traversal stack (no per-ray heap allocation). An iterative DFS that pushes both children holds at most
988 // ~one node per tree level, so the capacity must exceed buildBVHRecursive's MAX_DEPTH (64). 128 leaves generous
989 // headroom; the guarded push below is a defensive backstop that must never actually trigger for a valid tree.
990 constexpr int STACK_CAPACITY = 128;
991 uint32_t node_stack[STACK_CAPACITY];
992 int stack_size = 0;
993 node_stack[stack_size++] = 0; // Start from root
994
995 // Dense, BVH-leaf-ordered cache must be in lockstep with the SoA leaves' primitive ranges; fall back to the
996 // UUID-keyed path if it has not been built (e.g. legacy callers that bypass buildPrimitiveCache()).
997 const bool use_dense_cache = (primitive_cache_dense.size() == primitive_indices.size());
998
999 float closest_distance = (query.max_distance > 0) ? query.max_distance : std::numeric_limits<float>::max();
1000
1001 while (stack_size > 0) {
1002 uint32_t node_idx = node_stack[--stack_size];
1003 stats.bvh_nodes_visited++;
1004
1005 // Bounds check for node index
1006 if (node_idx >= bvh_nodes_soa.node_count) {
1007 continue;
1008 }
1009
1010 // AABB intersection test using SoA layout
1011 if (!aabbIntersectSoA(query.origin, query.direction, closest_distance, node_idx)) {
1012 continue;
1013 }
1014
1015 // Check if leaf node
1016 if (bvh_nodes_soa.is_leaf_flags[node_idx]) {
1017 // Process primitives in this leaf
1018 uint32_t primitive_start = bvh_nodes_soa.primitive_starts[node_idx];
1019 uint32_t primitive_count = bvh_nodes_soa.primitive_counts[node_idx];
1020
1021 for (uint32_t i = 0; i < primitive_count; ++i) {
1022 const uint32_t slot = primitive_start + i;
1023 uint primitive_id = primitive_indices[slot];
1024
1025 // Skip if not in target list (if specified)
1026 if (!query.target_UUIDs.empty()) {
1027 bool found = false;
1028 for (uint target: query.target_UUIDs) {
1029 if (primitive_id == target) {
1030 found = true;
1031 break;
1032 }
1033 }
1034 if (!found)
1035 continue;
1036 }
1037
1038 // Hot path: dense-cache slot lookup (no unordered_map find). Falls back to the UUID-keyed
1039 // intersection when the dense cache is unavailable.
1040 HitResult primitive_result = use_dense_cache ? intersectCachedPrimitive(query.origin, query.direction, primitive_cache_dense[slot], closest_distance)
1041 : intersectPrimitiveThreadSafe(query.origin, query.direction, primitive_id, closest_distance);
1042 if (primitive_result.hit && primitive_result.distance < closest_distance) {
1043 result = primitive_result;
1044 closest_distance = primitive_result.distance;
1045 }
1046 }
1047 } else {
1048 // Internal node - push children. Descend the nearer child first (pushed last, popped first) so the
1049 // farther subtree can be pruned by closest_distance once a near hit is found.
1050 uint32_t left_child = bvh_nodes_soa.left_children[node_idx];
1051 uint32_t right_child = bvh_nodes_soa.right_children[node_idx];
1052
1053 const bool left_valid = (left_child != 0xFFFFFFFF && left_child < bvh_nodes_soa.node_count);
1054 const bool right_valid = (right_child != 0xFFFFFFFF && right_child < bvh_nodes_soa.node_count);
1055
1056 if (left_valid && right_valid) {
1057 // Order by entry distance into each child's AABB so the nearer child is popped first.
1058 const float t_left = aabbEntryDistanceSoA(query.origin, query.direction, left_child);
1059 const float t_right = aabbEntryDistanceSoA(query.origin, query.direction, right_child);
1060 uint32_t first = left_child, second = right_child;
1061 if (t_right < t_left) {
1062 first = right_child;
1063 second = left_child;
1064 }
1065 if (stack_size < STACK_CAPACITY)
1066 node_stack[stack_size++] = second; // farther child pushed first (popped last)
1067 if (stack_size < STACK_CAPACITY)
1068 node_stack[stack_size++] = first; // nearer child pushed last (popped first)
1069 } else if (left_valid) {
1070 if (stack_size < STACK_CAPACITY)
1071 node_stack[stack_size++] = left_child;
1072 } else if (right_valid) {
1073 if (stack_size < STACK_CAPACITY)
1074 node_stack[stack_size++] = right_child;
1075 }
1076 }
1077 }
1078
1079 return result;
1080}
1081
1082
1083bool CollisionDetection::aabbIntersectSoA(const helios::vec3 &ray_origin, const helios::vec3 &ray_direction, float max_distance, size_t node_index) const {
1084 // Direct access to SoA arrays for optimal memory usage
1085 const vec3 &aabb_min = bvh_nodes_soa.aabb_mins[node_index];
1086 const vec3 &aabb_max = bvh_nodes_soa.aabb_maxs[node_index];
1087
1088#ifdef __SSE4_1__
1089 // SIMD-optimized ray-AABB intersection for better performance
1090 __m128 ray_orig = _mm_set_ps(0.0f, ray_origin.z, ray_origin.y, ray_origin.x);
1091 __m128 ray_dir = _mm_set_ps(0.0f, ray_direction.z, ray_direction.y, ray_direction.x);
1092 __m128 aabb_min_vec = _mm_set_ps(0.0f, aabb_min.z, aabb_min.y, aabb_min.x);
1093 __m128 aabb_max_vec = _mm_set_ps(0.0f, aabb_max.z, aabb_max.y, aabb_max.x);
1094
1095 // Compute inverse ray direction. Clamp near-zero direction components away from zero before the reciprocal so an
1096 // axis-aligned ray lying exactly on a box face computes (bound-origin)*huge == 0 (finite) instead of
1097 // (bound-origin==0)*inf == NaN, which would corrupt the min/max slab test and spuriously reject the box.
1098 constexpr float PARALLEL_EPS = 1e-8f;
1099 __m128 abs_dir = _mm_andnot_ps(_mm_set1_ps(-0.0f), ray_dir); // |ray_dir|
1100 __m128 too_small = _mm_cmplt_ps(abs_dir, _mm_set1_ps(PARALLEL_EPS));
1101 // Replace near-zero components with +/-PARALLEL_EPS preserving sign (sign bit of the original component).
1102 __m128 sign = _mm_and_ps(ray_dir, _mm_set1_ps(-0.0f));
1103 __m128 clamped = _mm_or_ps(sign, _mm_set1_ps(PARALLEL_EPS));
1104 __m128 safe_dir = _mm_or_ps(_mm_and_ps(too_small, clamped), _mm_andnot_ps(too_small, ray_dir));
1105 __m128 inv_dir = _mm_div_ps(_mm_set1_ps(1.0f), safe_dir);
1106
1107 // Compute t1 and t2 for all axes
1108 __m128 t1 = _mm_mul_ps(_mm_sub_ps(aabb_min_vec, ray_orig), inv_dir);
1109 __m128 t2 = _mm_mul_ps(_mm_sub_ps(aabb_max_vec, ray_orig), inv_dir);
1110
1111 // Get min and max for each axis
1112 __m128 tmin = _mm_min_ps(t1, t2);
1113 __m128 tmax = _mm_max_ps(t1, t2);
1114
1115 // Extract components
1116 float tmin_vals[4], tmax_vals[4];
1117 _mm_store_ps(tmin_vals, tmin);
1118 _mm_store_ps(tmax_vals, tmax);
1119
1120 float t_near = std::max({tmin_vals[0], tmin_vals[1], tmin_vals[2], 0.0f});
1121 float t_far = std::min({tmax_vals[0], tmax_vals[1], tmax_vals[2], max_distance});
1122
1123 return t_near <= t_far;
1124#else
1125 // Fallback scalar implementation. Each axis is handled explicitly so that an axis-aligned ray lying exactly on a
1126 // box face (direction component == 0) does not produce a 0*inf == NaN t-value that would corrupt the slab test
1127 // and spuriously reject the box. A ray parallel to an axis simply imposes no near/far constraint on that axis as
1128 // long as its origin is within the slab; if the origin is outside the slab it misses outright.
1129 float t_near = 0.0f;
1130 float t_far = max_distance;
1131
1132 const float origin_xyz[3] = {ray_origin.x, ray_origin.y, ray_origin.z};
1133 const float dir_xyz[3] = {ray_direction.x, ray_direction.y, ray_direction.z};
1134 const float min_xyz[3] = {aabb_min.x, aabb_min.y, aabb_min.z};
1135 const float max_xyz[3] = {aabb_max.x, aabb_max.y, aabb_max.z};
1136
1137 constexpr float PARALLEL_EPS = 1e-8f;
1138 for (int axis = 0; axis < 3; axis++) {
1139 if (std::abs(dir_xyz[axis]) < PARALLEL_EPS) {
1140 // Ray parallel to this slab: it can only hit the box if its origin lies within the slab bounds.
1141 if (origin_xyz[axis] < min_xyz[axis] || origin_xyz[axis] > max_xyz[axis]) {
1142 return false;
1143 }
1144 continue; // no t constraint from this axis
1145 }
1146 const float inv = 1.0f / dir_xyz[axis];
1147 float t1 = (min_xyz[axis] - origin_xyz[axis]) * inv;
1148 float t2 = (max_xyz[axis] - origin_xyz[axis]) * inv;
1149 if (t1 > t2) {
1150 std::swap(t1, t2);
1151 }
1152 t_near = std::max(t_near, t1);
1153 t_far = std::min(t_far, t2);
1154 if (t_near > t_far) {
1155 return false;
1156 }
1157 }
1158
1159 return t_near <= t_far;
1160#endif
1161}
1162
1163float CollisionDetection::aabbEntryDistanceSoA(const helios::vec3 &ray_origin, const helios::vec3 &ray_direction, size_t node_index) const {
1164 // Entry distance (t_near, clamped to >=0) of the ray into the node's AABB, used only to order the two
1165 // children for near-first traversal. Returns +inf when the ray misses the box so a missed child sorts last.
1166 // Handles axis-aligned (parallel) rays explicitly to avoid 0*inf == NaN corrupting the comparison.
1167 const vec3 &aabb_min = bvh_nodes_soa.aabb_mins[node_index];
1168 const vec3 &aabb_max = bvh_nodes_soa.aabb_maxs[node_index];
1169
1170 const float origin_xyz[3] = {ray_origin.x, ray_origin.y, ray_origin.z};
1171 const float dir_xyz[3] = {ray_direction.x, ray_direction.y, ray_direction.z};
1172 const float min_xyz[3] = {aabb_min.x, aabb_min.y, aabb_min.z};
1173 const float max_xyz[3] = {aabb_max.x, aabb_max.y, aabb_max.z};
1174
1175 constexpr float PARALLEL_EPS = 1e-8f;
1176 float t_near = 0.0f;
1177 float t_far = std::numeric_limits<float>::max();
1178 for (int axis = 0; axis < 3; axis++) {
1179 if (std::abs(dir_xyz[axis]) < PARALLEL_EPS) {
1180 if (origin_xyz[axis] < min_xyz[axis] || origin_xyz[axis] > max_xyz[axis]) {
1181 return std::numeric_limits<float>::max();
1182 }
1183 continue;
1184 }
1185 const float inv = 1.0f / dir_xyz[axis];
1186 float t1 = (min_xyz[axis] - origin_xyz[axis]) * inv;
1187 float t2 = (max_xyz[axis] - origin_xyz[axis]) * inv;
1188 if (t1 > t2) {
1189 std::swap(t1, t2);
1190 }
1191 t_near = std::max(t_near, t1);
1192 t_far = std::min(t_far, t2);
1193 }
1194
1195 return (t_near <= t_far) ? t_near : std::numeric_limits<float>::max();
1196}
1197
1198
1199// Basic BVH traversal using standard node structure
1200CollisionDetection::HitResult CollisionDetection::castRayBVHTraversal(const RayQuery &query) {
1201 HitResult result;
1202
1203 if (bvh_nodes.empty()) {
1204 return result; // No BVH built
1205 }
1206
1207 // Stack-based traversal using standard BVH nodes
1208 std::stack<size_t> node_stack;
1209 node_stack.push(0); // Start from root
1210
1211 float closest_distance = (query.max_distance > 0) ? query.max_distance : std::numeric_limits<float>::max();
1212
1213 while (!node_stack.empty()) {
1214 size_t node_idx = node_stack.top();
1215 node_stack.pop();
1216
1217 if (node_idx >= bvh_nodes.size()) {
1218 if (printmessages) {
1219 std::cout << "ERROR: Invalid BVH node index " << node_idx << " >= " << bvh_nodes.size() << " nodes" << std::endl;
1220 }
1221 result.hit = false;
1222 return result;
1223 }
1224
1225 const BVHNode &node = bvh_nodes[node_idx];
1226
1227 // AABB intersection test - this provides the early miss detection that was missing
1228 if (!rayAABBIntersect(query.origin, query.direction, node.aabb_min, node.aabb_max)) {
1229 continue; // Ray misses this node's bounding box - skip entire subtree
1230 }
1231
1232 if (node.is_leaf) {
1233 // Process primitives in this leaf
1234 for (uint32_t i = 0; i < node.primitive_count; ++i) {
1235 if (node.primitive_start + i >= primitive_indices.size()) {
1236 if (printmessages) {
1237 std::cout << "ERROR: Invalid BVH primitive index " << (node.primitive_start + i) << " >= " << primitive_indices.size() << " primitives" << std::endl;
1238 }
1239 result.hit = false;
1240 return result;
1241 }
1242
1243 uint primitive_id = primitive_indices[node.primitive_start + i];
1244
1245 // Skip if not in target list (if specified)
1246 if (!query.target_UUIDs.empty()) {
1247 bool found = false;
1248 for (uint target: query.target_UUIDs) {
1249 if (primitive_id == target) {
1250 found = true;
1251 break;
1252 }
1253 }
1254 if (!found)
1255 continue;
1256 }
1257
1258 // Perform primitive intersection test
1259 HitResult primitive_hit = intersectPrimitive(query, primitive_id);
1260 if (primitive_hit.hit && primitive_hit.distance < closest_distance) {
1261 result = primitive_hit;
1262 closest_distance = primitive_hit.distance;
1263 }
1264 }
1265 } else {
1266 // Internal node - add children to stack for traversal
1267 if (node.left_child != 0xFFFFFFFF && node.left_child < bvh_nodes.size()) {
1268 node_stack.push(node.left_child);
1269 }
1270 if (node.right_child != 0xFFFFFFFF && node.right_child < bvh_nodes.size()) {
1271 node_stack.push(node.right_child);
1272 }
1273 }
1274 }
1275
1276 return result;
1277}
1278
1279// Helper method to test ray-AABB intersection
1280bool CollisionDetection::rayAABBIntersect(const vec3 &ray_origin, const vec3 &ray_direction, const vec3 &aabb_min, const vec3 &aabb_max) const {
1281 // Robust ray-AABB intersection using slab method with proper axis-aligned ray handling
1282 const float EPSILON = 1e-8f;
1283
1284 float tmin = 0.0f; // Ray starts at origin
1285 float tmax = std::numeric_limits<float>::max(); // No maximum distance limit
1286
1287 // Handle X slab
1288 if (std::abs(ray_direction.x) > EPSILON) {
1289 float inv_dir_x = 1.0f / ray_direction.x;
1290 float t1 = (aabb_min.x - ray_origin.x) * inv_dir_x;
1291 float t2 = (aabb_max.x - ray_origin.x) * inv_dir_x;
1292
1293 float slab_tmin = std::min(t1, t2);
1294 float slab_tmax = std::max(t1, t2);
1295
1296 tmin = std::max(tmin, slab_tmin);
1297 tmax = std::min(tmax, slab_tmax);
1298
1299 if (tmin > tmax)
1300 return false; // Early exit if no intersection
1301 } else {
1302 // Ray is parallel to X slab - check if ray origin is within X bounds
1303 if (ray_origin.x < aabb_min.x || ray_origin.x > aabb_max.x) {
1304 return false;
1305 }
1306 }
1307
1308 // Handle Y slab
1309 if (std::abs(ray_direction.y) > EPSILON) {
1310 float inv_dir_y = 1.0f / ray_direction.y;
1311 float t1 = (aabb_min.y - ray_origin.y) * inv_dir_y;
1312 float t2 = (aabb_max.y - ray_origin.y) * inv_dir_y;
1313
1314 float slab_tmin = std::min(t1, t2);
1315 float slab_tmax = std::max(t1, t2);
1316
1317 tmin = std::max(tmin, slab_tmin);
1318 tmax = std::min(tmax, slab_tmax);
1319
1320 if (tmin > tmax)
1321 return false; // Early exit if no intersection
1322 } else {
1323 // Ray is parallel to Y slab - check if ray origin is within Y bounds
1324 if (ray_origin.y < aabb_min.y || ray_origin.y > aabb_max.y) {
1325 return false;
1326 }
1327 }
1328
1329 // Handle Z slab
1330 if (std::abs(ray_direction.z) > EPSILON) {
1331 float inv_dir_z = 1.0f / ray_direction.z;
1332 float t1 = (aabb_min.z - ray_origin.z) * inv_dir_z;
1333 float t2 = (aabb_max.z - ray_origin.z) * inv_dir_z;
1334
1335 float slab_tmin = std::min(t1, t2);
1336 float slab_tmax = std::max(t1, t2);
1337
1338 tmin = std::max(tmin, slab_tmin);
1339 tmax = std::min(tmax, slab_tmax);
1340
1341 if (tmin > tmax)
1342 return false; // Early exit if no intersection
1343 } else {
1344 // Ray is parallel to Z slab - check if ray origin is within Z bounds
1345 if (ray_origin.z < aabb_min.z || ray_origin.z > aabb_max.z) {
1346 return false;
1347 }
1348 }
1349
1350 return tmin <= tmax;
1351}
1352
1353// Helper method to intersect with individual primitive (reuses existing logic)
1354CollisionDetection::HitResult CollisionDetection::intersectPrimitive(const RayQuery &query, uint primitive_id) {
1355 // PERFORMANCE FIX: Use direct context call instead of expensive cached primitive data
1356 // This avoids the need to build and maintain a primitive cache
1357 HitResult result;
1358
1359 float distance;
1360 if (rayPrimitiveIntersection(query.origin, query.direction, primitive_id, distance)) {
1361 vec3 intersection_point = query.origin + query.direction * distance;
1362
1363 // Calculate surface normal directly from context (minimal overhead)
1364 PrimitiveType type = context->getPrimitiveType(primitive_id);
1365 std::vector<vec3> vertices = context->getPrimitiveVertices(primitive_id);
1366
1367 // Reject hits on transparent texels so the ray passes through to geometry behind.
1368 if (context->primitiveTextureHasTransparencyChannel(primitive_id)) {
1369 CachedPrimitive temp_cached(type, vertices);
1370 temp_cached.transparency_mask = context->getPrimitiveTextureTransparencyData(primitive_id);
1371 temp_cached.texture_size = context->getPrimitiveTextureSize(primitive_id);
1372 temp_cached.uv = context->getPrimitiveTextureUV(primitive_id);
1373 if (!isHitTexelOpaque(temp_cached, intersection_point)) {
1374 return result; // result.hit is still false
1375 }
1376 }
1377
1378 result.hit = true;
1379 result.distance = distance;
1380 result.primitive_UUID = primitive_id;
1381 result.intersection_point = intersection_point;
1382
1383 if (type == PRIMITIVE_TYPE_TRIANGLE && vertices.size() >= 3) {
1384 vec3 edge1 = vertices[1] - vertices[0];
1385 vec3 edge2 = vertices[2] - vertices[0];
1386 result.normal = cross(edge1, edge2);
1387 if (result.normal.magnitude() > 1e-8f) {
1388 result.normal = result.normal / result.normal.magnitude();
1389 } else {
1390 result.normal = make_vec3(-query.direction.x, -query.direction.y, -query.direction.z);
1391 }
1392 } else if (type == PRIMITIVE_TYPE_PATCH && vertices.size() >= 3) {
1393 vec3 edge1 = vertices[1] - vertices[0];
1394 vec3 edge2 = vertices[2] - vertices[0];
1395 result.normal = cross(edge1, edge2);
1396 if (result.normal.magnitude() > 1e-8f) {
1397 result.normal = result.normal / result.normal.magnitude();
1398 } else {
1399 result.normal = make_vec3(-query.direction.x, -query.direction.y, -query.direction.z);
1400 }
1401 } else {
1402 // Default normal (opposite to ray direction)
1403 result.normal = make_vec3(-query.direction.x, -query.direction.y, -query.direction.z);
1404 }
1405 }
1406
1407 return result;
1408}
1409
1410bool CollisionDetection::isHitTexelOpaque(const CachedPrimitive &cached, const vec3 &hit_point) const {
1411
1412 // No transparency mask -> primitive is fully solid.
1413 if (cached.transparency_mask == nullptr || cached.texture_size.x <= 0 || cached.texture_size.y <= 0) {
1414 return true;
1415 }
1416
1417 const std::vector<vec2> &uvs = cached.uv;
1418
1419 // Compute the (u,v) texture coordinate at the hit point. The interpolation mirrors the
1420 // verified logic in LiDARcloud::syntheticScan (sample_hit_color) so that the texel selected
1421 // here matches the texel later used for hit-point coloring.
1422 vec2 uv;
1423 if (cached.type == PRIMITIVE_TYPE_PATCH && cached.vertices.size() >= 4) {
1424 // Patch corners are (BL, BR, TR, TL); project the hit onto the (BL->BR, BL->TL) basis.
1425 const vec3 e1 = cached.vertices[1] - cached.vertices[0];
1426 const vec3 e2 = cached.vertices[3] - cached.vertices[0];
1427 const vec3 d = hit_point - cached.vertices[0];
1428 const float e1_sq = e1 * e1;
1429 const float e2_sq = e2 * e2;
1430 float s_param = (e1_sq > 0.f) ? (d * e1) / e1_sq : 0.f;
1431 float t_param = (e2_sq > 0.f) ? (d * e2) / e2_sq : 0.f;
1432 s_param = std::min(std::max(s_param, 0.f), 1.f);
1433 t_param = std::min(std::max(t_param, 0.f), 1.f);
1434 if (uvs.size() == 4) {
1435 uv = (1.f - s_param) * (1.f - t_param) * uvs[0] + s_param * (1.f - t_param) * uvs[1] + s_param * t_param * uvs[2] + (1.f - s_param) * t_param * uvs[3];
1436 } else {
1437 uv = make_vec2(s_param, t_param);
1438 }
1439 } else if (cached.type == PRIMITIVE_TYPE_TRIANGLE && cached.vertices.size() >= 3 && uvs.size() == 3) {
1440 const vec3 e1 = cached.vertices[1] - cached.vertices[0];
1441 const vec3 e2 = cached.vertices[2] - cached.vertices[0];
1442 const vec3 d = hit_point - cached.vertices[0];
1443 const float dot11 = e1 * e1;
1444 const float dot12 = e1 * e2;
1445 const float dot22 = e2 * e2;
1446 const float dot1d = e1 * d;
1447 const float dot2d = e2 * d;
1448 const float denom = dot11 * dot22 - dot12 * dot12;
1449 if (std::fabs(denom) < 1e-20f) {
1450 return true; // degenerate triangle - cannot map UV, treat as solid
1451 }
1452 const float inv_denom = 1.f / denom;
1453 const float beta = (dot22 * dot1d - dot12 * dot2d) * inv_denom;
1454 const float gamma = (dot11 * dot2d - dot12 * dot1d) * inv_denom;
1455 uv = uvs[0] + beta * (uvs[1] - uvs[0]) + gamma * (uvs[2] - uvs[0]);
1456 } else {
1457 // Unsupported configuration (e.g. missing UVs) - cannot map the texel, treat as solid.
1458 return true;
1459 }
1460
1461 // Wrap UV into [0,1) so repeat-style mappings sample correctly.
1462 uv.x -= std::floor(uv.x);
1463 uv.y -= std::floor(uv.y);
1464
1465 int px = static_cast<int>(uv.x * static_cast<float>(cached.texture_size.x));
1466 px = std::min(std::max(px, 0), cached.texture_size.x - 1);
1467 // Mask rows are stored top-to-bottom (row 0 = top of image); UV y=0 is the bottom.
1468 int py = static_cast<int>((1.f - uv.y) * static_cast<float>(cached.texture_size.y));
1469 py = std::min(std::max(py, 0), cached.texture_size.y - 1);
1470
1471 const std::vector<std::vector<bool>> &mask = *cached.transparency_mask;
1472 if (py >= static_cast<int>(mask.size()) || px >= static_cast<int>(mask[py].size())) {
1473 return true; // out-of-bounds guard - treat as solid rather than dropping the hit
1474 }
1475 return mask[py][px]; // true => opaque/solid texel
1476}
1477
1478CollisionDetection::HitResult CollisionDetection::intersectPrimitiveThreadSafe(const vec3 &origin, const vec3 &direction, uint primitive_id, float max_distance) {
1479 HitResult result;
1480
1481 // Check if we have cached primitive data for this primitive
1482 auto it = primitive_cache.find(primitive_id);
1483 if (it == primitive_cache.end()) {
1484 // Primitive not in cache - this shouldn't happen in optimized paths
1485 // This indicates that the context was modified after the cache was built
1486 // Fall back to thread-unsafe context call (only safe for sequential code)
1487 // For parallel regions, this could cause issues, but it's better than crashing
1488 float distance;
1489 if (rayPrimitiveIntersection(origin, direction, primitive_id, distance)) {
1490 vec3 intersection_point = origin + direction * distance;
1491
1492 // Calculate surface normal for uncached primitive
1493 PrimitiveType type = context->getPrimitiveType(primitive_id);
1494 std::vector<vec3> vertices = context->getPrimitiveVertices(primitive_id);
1495
1496 // Reject hits on transparent texels (build a temporary cache entry from the Context;
1497 // this fallback path is already thread-unsafe so the extra Context reads are acceptable).
1498 if (context->primitiveTextureHasTransparencyChannel(primitive_id)) {
1499 CachedPrimitive temp_cached(type, vertices);
1500 temp_cached.transparency_mask = context->getPrimitiveTextureTransparencyData(primitive_id);
1501 temp_cached.texture_size = context->getPrimitiveTextureSize(primitive_id);
1502 temp_cached.uv = context->getPrimitiveTextureUV(primitive_id);
1503 if (!isHitTexelOpaque(temp_cached, intersection_point)) {
1504 return result; // result.hit is still false
1505 }
1506 }
1507
1508 result.hit = true;
1509 result.distance = distance;
1510 result.primitive_UUID = primitive_id;
1511 result.intersection_point = intersection_point;
1512
1513 if (type == PRIMITIVE_TYPE_TRIANGLE && vertices.size() >= 3) {
1514 vec3 edge1 = vertices[1] - vertices[0];
1515 vec3 edge2 = vertices[2] - vertices[0];
1516 result.normal = cross(edge1, edge2);
1517 if (result.normal.magnitude() > 1e-8f) {
1518 result.normal = result.normal / result.normal.magnitude();
1519 // Ensure normal points towards ray origin (for LiDAR compatibility)
1520 vec3 to_origin = origin - result.intersection_point;
1521 if (result.normal * to_origin < 0) {
1522 result.normal = result.normal * -1.0f;
1523 }
1524 } else {
1525 result.normal = make_vec3(-direction.x, -direction.y, -direction.z);
1526 result.normal = result.normal / result.normal.magnitude();
1527 }
1528 } else if (type == PRIMITIVE_TYPE_PATCH && vertices.size() >= 4) {
1529 vec3 edge1 = vertices[1] - vertices[0];
1530 vec3 edge2 = vertices[2] - vertices[0];
1531 result.normal = cross(edge1, edge2);
1532 if (result.normal.magnitude() > 1e-8f) {
1533 result.normal = result.normal / result.normal.magnitude();
1534 // Ensure normal points towards ray origin (for LiDAR compatibility)
1535 vec3 to_origin = origin - result.intersection_point;
1536 if (result.normal * to_origin < 0) {
1537 result.normal = result.normal * -1.0f;
1538 }
1539 } else {
1540 result.normal = make_vec3(-direction.x, -direction.y, -direction.z);
1541 result.normal = result.normal / result.normal.magnitude();
1542 }
1543 } else {
1544 result.normal = make_vec3(-direction.x, -direction.y, -direction.z);
1545 result.normal = result.normal / result.normal.magnitude();
1546 }
1547 }
1548 return result;
1549 }
1550
1551 // Cache hit: delegate to the shared cached-primitive intersection (same logic as the dense-cache hot path).
1552 return intersectCachedPrimitive(origin, direction, it->second, max_distance);
1553}
1554
1555CollisionDetection::HitResult CollisionDetection::intersectCachedPrimitive(const vec3 &origin, const vec3 &direction, const CachedPrimitive &cached, float max_distance) const {
1556 HitResult result;
1557
1558 // Perform intersection test based on primitive type
1559 if (cached.type == PRIMITIVE_TYPE_TRIANGLE && cached.vertices.size() >= 3) {
1560 float distance;
1561 if (triangleIntersect(origin, direction, cached.vertices[0], cached.vertices[1], cached.vertices[2], distance)) {
1562 if (distance > 1e-6f && (max_distance <= 0 || distance < max_distance)) {
1563 vec3 intersection_point = origin + direction * distance;
1564
1565 // Reject hits on transparent texels so the ray passes through to geometry behind.
1566 if (!isHitTexelOpaque(cached, intersection_point)) {
1567 return result; // result.hit is still false
1568 }
1569
1570 result.hit = true;
1571 result.distance = distance;
1572 result.primitive_UUID = cached.UUID;
1573 result.intersection_point = intersection_point;
1574
1575 // Calculate triangle normal
1576 vec3 edge1 = cached.vertices[1] - cached.vertices[0];
1577 vec3 edge2 = cached.vertices[2] - cached.vertices[0];
1578 result.normal = cross(edge1, edge2);
1579 if (result.normal.magnitude() > 1e-8f) {
1580 result.normal = result.normal / result.normal.magnitude();
1581 // Ensure normal points towards ray origin (for LiDAR compatibility)
1582 vec3 to_origin = origin - result.intersection_point;
1583 if (result.normal * to_origin < 0) {
1584 result.normal = result.normal * -1.0f;
1585 }
1586 } else {
1587 result.normal = make_vec3(-direction.x, -direction.y, -direction.z);
1588 result.normal = result.normal / result.normal.magnitude();
1589 }
1590 }
1591 }
1592 } else if (cached.type == PRIMITIVE_TYPE_PATCH && cached.vertices.size() >= 4) {
1593 float distance;
1594 if (patchIntersect(origin, direction, cached.vertices[0], cached.vertices[1], cached.vertices[2], cached.vertices[3], distance)) {
1595 if (distance > 1e-6f && (max_distance <= 0 || distance < max_distance)) {
1596 vec3 intersection_point = origin + direction * distance;
1597
1598 // Reject hits on transparent texels so the ray passes through to geometry behind.
1599 if (!isHitTexelOpaque(cached, intersection_point)) {
1600 return result; // result.hit is still false
1601 }
1602
1603 result.hit = true;
1604 result.distance = distance;
1605 result.primitive_UUID = cached.UUID;
1606 result.intersection_point = intersection_point;
1607
1608 // Calculate patch normal (use v0, v1, v2 like the original code)
1609 vec3 edge1 = cached.vertices[1] - cached.vertices[0];
1610 vec3 edge2 = cached.vertices[2] - cached.vertices[0];
1611 result.normal = cross(edge1, edge2);
1612 if (result.normal.magnitude() > 1e-8f) {
1613 result.normal = result.normal / result.normal.magnitude();
1614 // Ensure normal points towards ray origin (for LiDAR compatibility)
1615 vec3 to_origin = origin - result.intersection_point;
1616 if (result.normal * to_origin < 0) {
1617 result.normal = result.normal * -1.0f;
1618 }
1619 } else {
1620 result.normal = make_vec3(-direction.x, -direction.y, -direction.z);
1621 result.normal = result.normal / result.normal.magnitude();
1622 }
1623 }
1624 }
1625 } else if (cached.type == PRIMITIVE_TYPE_VOXEL && cached.vertices.size() == 8) {
1626 // Voxel (AABB) intersection using slab method
1627 // Calculate AABB from 8 vertices
1628 vec3 aabb_min = cached.vertices[0];
1629 vec3 aabb_max = cached.vertices[0];
1630
1631 for (int i = 1; i < 8; i++) {
1632 aabb_min.x = std::min(aabb_min.x, cached.vertices[i].x);
1633 aabb_min.y = std::min(aabb_min.y, cached.vertices[i].y);
1634 aabb_min.z = std::min(aabb_min.z, cached.vertices[i].z);
1635 aabb_max.x = std::max(aabb_max.x, cached.vertices[i].x);
1636 aabb_max.y = std::max(aabb_max.y, cached.vertices[i].y);
1637 aabb_max.z = std::max(aabb_max.z, cached.vertices[i].z);
1638 }
1639
1640 // Ray-AABB intersection using slab method
1641 float t_near = -std::numeric_limits<float>::max();
1642 float t_far = std::numeric_limits<float>::max();
1643
1644 // Check intersection with each slab (X, Y, Z)
1645 for (int axis = 0; axis < 3; axis++) {
1646 float ray_dir_component = (axis == 0) ? direction.x : (axis == 1) ? direction.y : direction.z;
1647 float ray_orig_component = (axis == 0) ? origin.x : (axis == 1) ? origin.y : origin.z;
1648 float aabb_min_component = (axis == 0) ? aabb_min.x : (axis == 1) ? aabb_min.y : aabb_min.z;
1649 float aabb_max_component = (axis == 0) ? aabb_max.x : (axis == 1) ? aabb_max.y : aabb_max.z;
1650
1651 if (std::abs(ray_dir_component) < 1e-8f) {
1652 // Ray is parallel to slab
1653 if (ray_orig_component < aabb_min_component || ray_orig_component > aabb_max_component) {
1654 return result; // Ray is outside slab and parallel - no intersection
1655 }
1656 } else {
1657 // Calculate intersection distances for this slab
1658 float t1 = (aabb_min_component - ray_orig_component) / ray_dir_component;
1659 float t2 = (aabb_max_component - ray_orig_component) / ray_dir_component;
1660
1661 // Ensure t1 <= t2
1662 if (t1 > t2) {
1663 std::swap(t1, t2);
1664 }
1665
1666 // Update near and far intersection distances
1667 t_near = std::max(t_near, t1);
1668 t_far = std::min(t_far, t2);
1669
1670 // Early exit if no intersection possible
1671 if (t_near > t_far) {
1672 return result;
1673 }
1674 }
1675 }
1676
1677 // Check if intersection is in front of ray origin and within max distance
1678 if (t_far >= 0.0f) {
1679 // Use t_near if it's positive (ray starts outside box), otherwise t_far (ray starts inside box)
1680 float intersection_distance = (t_near >= 1e-6f) ? t_near : t_far;
1681 if (intersection_distance >= 1e-6f && (max_distance <= 0 || intersection_distance < max_distance)) {
1682 result.hit = true;
1683 result.distance = intersection_distance;
1684 result.primitive_UUID = cached.UUID;
1685 result.intersection_point = origin + direction * intersection_distance;
1686
1687 // Calculate normal based on which face was hit
1688 // Determine which face of the voxel was hit by examining intersection point
1689 vec3 hit_point = result.intersection_point;
1690 vec3 box_center = (aabb_min + aabb_max) * 0.5f;
1691 vec3 box_extent = (aabb_max - aabb_min) * 0.5f;
1692
1693 // Find which face the hit point is closest to
1694 vec3 local_hit = hit_point - box_center;
1695 vec3 abs_local_hit = make_vec3(std::abs(local_hit.x), std::abs(local_hit.y), std::abs(local_hit.z));
1696
1697 // Determine which axis has the largest relative coordinate (closest to face)
1698 float rel_x = abs_local_hit.x / box_extent.x;
1699 float rel_y = abs_local_hit.y / box_extent.y;
1700 float rel_z = abs_local_hit.z / box_extent.z;
1701
1702 if (rel_x >= rel_y && rel_x >= rel_z) {
1703 // Hit X face
1704 result.normal = make_vec3((local_hit.x > 0) ? 1.0f : -1.0f, 0.0f, 0.0f);
1705 } else if (rel_y >= rel_z) {
1706 // Hit Y face
1707 result.normal = make_vec3(0.0f, (local_hit.y > 0) ? 1.0f : -1.0f, 0.0f);
1708 } else {
1709 // Hit Z face
1710 result.normal = make_vec3(0.0f, 0.0f, (local_hit.z > 0) ? 1.0f : -1.0f);
1711 }
1712 }
1713 }
1714 }
1715 // Add other primitive types as needed (DISK, etc.)
1716
1717 return result;
1718}
1719
1720void CollisionDetection::buildPrimitiveCache() {
1721 primitive_cache.clear();
1722
1723 // Get all primitive UUIDs from context
1724 std::vector<uint> all_primitives = context->getAllUUIDs();
1725
1726 // Cache primitive data for thread-safe access
1727 for (uint primitive_id: all_primitives) {
1728 if (context->doesPrimitiveExist(primitive_id)) {
1729 try {
1730 PrimitiveType type = context->getPrimitiveType(primitive_id);
1731 std::vector<vec3> vertices = context->getPrimitiveVertices(primitive_id);
1732
1733 CachedPrimitive cached(type, vertices);
1734 cached.UUID = primitive_id;
1735
1736 // Cache texture transparency data so that ray hits on transparent texels can be
1737 // rejected during traversal (mirrors the OptiX rtIgnoreIntersection behavior).
1738 // Reading the Context here keeps the parallel traversal thread-safe. Primitives
1739 // without a transparency channel (no texture, or e.g. JPEG) keep a null mask and
1740 // are treated as fully solid, preserving the original behavior.
1741 if (context->primitiveTextureHasTransparencyChannel(primitive_id)) {
1742 cached.transparency_mask = context->getPrimitiveTextureTransparencyData(primitive_id);
1743 cached.texture_size = context->getPrimitiveTextureSize(primitive_id);
1744 cached.uv = context->getPrimitiveTextureUV(primitive_id);
1745 }
1746
1747 primitive_cache[primitive_id] = std::move(cached);
1748 } catch (const std::exception &e) {
1749 // Skip this primitive if it no longer exists or can't be accessed
1750 // This can happen when UUIDs from previous contexts persist
1751 if (printmessages) {
1752 std::cout << "Warning: Skipping primitive " << primitive_id << " in cache build (not accessible: " << e.what() << ")" << std::endl;
1753 }
1754 continue;
1755 }
1756 }
1757 }
1758
1759 // Build the dense, BVH-leaf-ordered cache to match the current primitive_indices ordering.
1760 rebuildDensePrimitiveCache();
1761}
1762
1763void CollisionDetection::rebuildDensePrimitiveCache() {
1764 // Build the dense, BVH-leaf-ordered cache (slot i <-> primitive_indices[i]) from the UUID-keyed cache so the
1765 // hot traversal loop indexes it directly without an unordered_map lookup. Must be called whenever
1766 // primitive_indices is (re)ordered — buildBVH() reorders it in place even when the primitive set is unchanged.
1767 // Any primitive_indices entry with no cache entry gets a default CachedPrimitive whose empty vertex list makes
1768 // every intersection test fail safely — the same outcome as the previous find()==end() miss.
1769 primitive_cache_dense.assign(primitive_indices.size(), CachedPrimitive());
1770 for (size_t i = 0; i < primitive_indices.size(); i++) {
1771 auto it = primitive_cache.find(primitive_indices[i]);
1772 if (it != primitive_cache.end()) {
1773 primitive_cache_dense[i] = it->second;
1774 }
1775 }
1776}
1777
1778void CollisionDetection::ensurePrimitiveCacheCurrent() {
1779 // Build the UUID-keyed cache the first time (or after a primitive-set change clears it). buildPrimitiveCache()
1780 // also (re)builds the dense cache in the correct order.
1781 if (primitive_cache.empty()) {
1782 buildPrimitiveCache();
1783 return;
1784 }
1785 // The UUID-keyed cache is valid but the dense cache may be out of sync with the current primitive_indices order
1786 // (a BVH rebuild over an unchanged primitive set reorders primitive_indices and clears the dense cache). Rebuild
1787 // just the cheap dense ordering from the existing map in that case.
1788 if (primitive_cache_dense.size() != primitive_indices.size()) {
1789 rebuildDensePrimitiveCache();
1790 }
1791}
1792
1793bool CollisionDetection::triangleIntersect(const vec3 &origin, const vec3 &direction, const vec3 &v0, const vec3 &v1, const vec3 &v2, float &distance) const {
1794 // Möller-Trumbore triangle intersection algorithm (optimized - no vec3 temporaries)
1795 // Note: Using 1e-5f to match LiDAR CUDA kernel tolerance for edge-case rays
1796 const float EPSILON = 1e-5f;
1797
1798 // Compute triangle edges directly as components (avoid vec3 constructors)
1799 float edge1_x = v1.x - v0.x, edge1_y = v1.y - v0.y, edge1_z = v1.z - v0.z;
1800 float edge2_x = v2.x - v0.x, edge2_y = v2.y - v0.y, edge2_z = v2.z - v0.z;
1801
1802 // Cross product: h = direction × edge2 (computed directly)
1803 float h_x = direction.y * edge2_z - direction.z * edge2_y;
1804 float h_y = direction.z * edge2_x - direction.x * edge2_z;
1805 float h_z = direction.x * edge2_y - direction.y * edge2_x;
1806
1807 // Dot product: a = edge1 · h
1808 float a = edge1_x * h_x + edge1_y * h_y + edge1_z * h_z;
1809
1810 if (a > -EPSILON && a < EPSILON) {
1811 return false; // Ray is parallel to triangle
1812 }
1813
1814 float f = 1.0f / a;
1815
1816 // Vector s = origin - v0 (computed as components)
1817 float s_x = origin.x - v0.x, s_y = origin.y - v0.y, s_z = origin.z - v0.z;
1818
1819 // u = f * (s · h)
1820 float u = f * (s_x * h_x + s_y * h_y + s_z * h_z);
1821
1822 if (u < -EPSILON || u > 1.0f + EPSILON) {
1823 return false;
1824 }
1825
1826 // Cross product: q = s × edge1 (computed directly)
1827 float q_x = s_y * edge1_z - s_z * edge1_y;
1828 float q_y = s_z * edge1_x - s_x * edge1_z;
1829 float q_z = s_x * edge1_y - s_y * edge1_x;
1830
1831 // v = f * (direction · q)
1832 float v = f * (direction.x * q_x + direction.y * q_y + direction.z * q_z);
1833
1834 if (v < -EPSILON || u + v > 1.0f + EPSILON) {
1835 return false;
1836 }
1837
1838 // t = f * (edge2 · q) - computed directly as dot product
1839 float t = f * (edge2_x * q_x + edge2_y * q_y + edge2_z * q_z);
1840
1841 if (t > EPSILON) {
1842 distance = t;
1843 return true;
1844 }
1845
1846 return false; // Line intersection but not ray intersection
1847}
1848
1849bool CollisionDetection::patchIntersect(const vec3 &origin, const vec3 &direction, const vec3 &v0, const vec3 &v1, const vec3 &v2, const vec3 &v3, float &distance) const {
1850 // Patch (quadrilateral) intersection using radiation model algorithm
1851 const float EPSILON = 1e-8f;
1852
1853 // Calculate patch vectors and normal (same as radiation model)
1854 vec3 anchor = v0;
1855 vec3 normal = cross(v1 - v0, v2 - v0);
1856 normal.normalize();
1857
1858 vec3 a = v1 - v0; // First edge vector
1859 vec3 b = v3 - v0; // Second edge vector
1860
1861 // Ray-plane intersection
1862 float denom = direction * normal;
1863 if (std::abs(denom) > EPSILON) { // Not parallel to plane
1864 float t = (anchor - origin) * normal / denom;
1865
1866 if (t > EPSILON && t < 1e8f) { // Valid intersection distance
1867 // Find intersection point
1868 vec3 p = origin + direction * t;
1869 vec3 d = p - anchor;
1870
1871 // Project onto patch coordinate system
1872 float ddota = d * a;
1873 float ddotb = d * b;
1874
1875 // Check if point is within patch bounds (with epsilon tolerance for edge cases)
1876 if (ddota >= -EPSILON && ddota <= (a * a) + EPSILON && ddotb >= -EPSILON && ddotb <= (b * b) + EPSILON) {
1877 distance = t;
1878 return true;
1879 }
1880 }
1881 }
1882
1883 return false;
1884}
1885
1886#ifdef HELIOS_CUDA_AVAILABLE
1887#include <vector_types.h> // For make_float3
1888
1895std::vector<CollisionDetection::HitResult> CollisionDetection::castRaysGPU(const std::vector<RayQuery> &ray_queries, RayTracingStats &stats) {
1896 std::vector<HitResult> results;
1897 results.resize(ray_queries.size());
1898
1899 if (ray_queries.empty() || !gpu_acceleration_enabled) {
1900 return castRaysSoA(ray_queries, stats); // Use CPU implementation
1901 }
1902
1903 // Ensure the BVH is built. With GPU acceleration enabled, buildBVH() also uploads the scene geometry to the device
1904 // (transferBVHToGPU) so it stays resident across many ray batches.
1905 if (bvh_nodes.empty()) {
1906 buildBVH();
1907 if (bvh_nodes.empty()) {
1908 helios_runtime_error("ERROR: BVH construction failed - no geometry available for ray tracing. Ensure primitives are properly added to the collision detection system.");
1909 }
1910 }
1911
1912 // If the caller enabled GPU after the BVH was already built, the scene may not be resident yet; upload it now.
1913 if (d_bvh_nodes == nullptr || d_primitive_vertices == nullptr) {
1914 transferBVHToGPU();
1915 }
1916 // If the scene still could not be made resident (e.g. GPU disabled at runtime), fall back to the CPU path.
1917 if (!gpu_acceleration_enabled || d_bvh_nodes == nullptr || d_primitive_vertices == nullptr || primitive_indices.empty()) {
1918 return castRaysSoA(ray_queries, stats);
1919 }
1920
1921 // Prepare host ray data with normalized directions (matching CPU RayQuery semantics). A non-positive max_distance
1922 // means "unbounded" on the CPU path; translate it to a large finite value so the GPU AABB/closest-hit logic agrees.
1923 const size_t num_rays = ray_queries.size();
1924 std::vector<float> ray_origins(num_rays * 3);
1925 std::vector<float> ray_directions(num_rays * 3);
1926 std::vector<float> ray_max_distances(num_rays);
1927 for (size_t i = 0; i < num_rays; i++) {
1928 vec3 dir = ray_queries[i].direction;
1929 float mag = dir.magnitude();
1930 if (mag > 1e-8f) {
1931 dir = dir / mag;
1932 }
1933 ray_origins[i * 3] = ray_queries[i].origin.x;
1934 ray_origins[i * 3 + 1] = ray_queries[i].origin.y;
1935 ray_origins[i * 3 + 2] = ray_queries[i].origin.z;
1936 ray_directions[i * 3] = dir.x;
1937 ray_directions[i * 3 + 1] = dir.y;
1938 ray_directions[i * 3 + 2] = dir.z;
1939 ray_max_distances[i] = (ray_queries[i].max_distance > 0) ? ray_queries[i].max_distance : std::numeric_limits<float>::max();
1940 }
1941
1942 std::vector<float> hit_distances(num_rays);
1943 std::vector<unsigned int> hit_primitive_ids(num_rays);
1944 std::vector<unsigned int> hit_counts(num_rays);
1945 std::vector<float> hit_normals(num_rays * 3);
1946
1947 // Launch against the resident scene: only the ray + result buffers are uploaded/downloaded here. The vector path
1948 // carries a per-ray max-distance array (RayQuery.max_distance may differ per ray), so uniform_max_distance is unused.
1949 launchRaysOnResidentScene(d_bvh_nodes, d_gpu_node_count, d_primitive_indices, d_gpu_primitive_count, d_primitive_types, (float3 *) d_primitive_vertices, d_vertex_offsets, (const unsigned char *) d_mask_data, d_mask_offsets, d_mask_sizes,
1950 d_mask_IDs, (const float *) d_uv_data, d_uv_IDs, d_gpu_total_vertex_count, ray_origins.data(), ray_directions.data(), ray_max_distances.data(), /*uniform_max_distance=*/0.0f, static_cast<int>(num_rays),
1951 hit_distances.data(), hit_primitive_ids.data(), hit_counts.data(), hit_normals.data(), true);
1952
1953 size_t hit_count = 0;
1954 for (size_t i = 0; i < num_rays; i++) {
1955 const float max_d = (ray_queries[i].max_distance > 0) ? ray_queries[i].max_distance : std::numeric_limits<float>::max();
1956 if (hit_counts[i] > 0 && hit_distances[i] <= max_d) {
1957 results[i].hit = true;
1958 results[i].primitive_UUID = hit_primitive_ids[i];
1959 results[i].distance = hit_distances[i];
1960 results[i].intersection_point = ray_queries[i].origin + ray_queries[i].direction * hit_distances[i];
1961 results[i].normal = make_vec3(hit_normals[i * 3], hit_normals[i * 3 + 1], hit_normals[i * 3 + 2]);
1962 hit_count++;
1963 } else {
1964 results[i].hit = false;
1965 results[i].primitive_UUID = 0;
1966 results[i].distance = std::numeric_limits<float>::max();
1967 }
1968 }
1969
1970 stats.total_rays_cast = num_rays;
1971 stats.total_hits = hit_count;
1972
1973 return results;
1974}
1975
1976// ================================================================
1977// SIMD-OPTIMIZED RAY TRACING METHODS
1978// ================================================================
1979
1980uint32_t CollisionDetection::rayAABBIntersectSIMD(const vec3 *ray_origins, const vec3 *ray_directions, const vec3 *aabb_mins, const vec3 *aabb_maxs, float *t_mins, float *t_maxs, int count) {
1981#ifdef __AVX2__
1982 if (count == 8) {
1983 // AVX2 implementation for 8 rays at once
1984 uint32_t hit_mask = 0;
1985
1986 for (int i = 0; i < 8; i += 8) {
1987 // Load 8 ray origins
1988 __m256 orig_x = _mm256_set_ps(ray_origins[i + 7].x, ray_origins[i + 6].x, ray_origins[i + 5].x, ray_origins[i + 4].x, ray_origins[i + 3].x, ray_origins[i + 2].x, ray_origins[i + 1].x, ray_origins[i + 0].x);
1989 __m256 orig_y = _mm256_set_ps(ray_origins[i + 7].y, ray_origins[i + 6].y, ray_origins[i + 5].y, ray_origins[i + 4].y, ray_origins[i + 3].y, ray_origins[i + 2].y, ray_origins[i + 1].y, ray_origins[i + 0].y);
1990 __m256 orig_z = _mm256_set_ps(ray_origins[i + 7].z, ray_origins[i + 6].z, ray_origins[i + 5].z, ray_origins[i + 4].z, ray_origins[i + 3].z, ray_origins[i + 2].z, ray_origins[i + 1].z, ray_origins[i + 0].z);
1991
1992 // Load 8 ray directions
1993 __m256 dir_x = _mm256_set_ps(ray_directions[i + 7].x, ray_directions[i + 6].x, ray_directions[i + 5].x, ray_directions[i + 4].x, ray_directions[i + 3].x, ray_directions[i + 2].x, ray_directions[i + 1].x, ray_directions[i + 0].x);
1994 __m256 dir_y = _mm256_set_ps(ray_directions[i + 7].y, ray_directions[i + 6].y, ray_directions[i + 5].y, ray_directions[i + 4].y, ray_directions[i + 3].y, ray_directions[i + 2].y, ray_directions[i + 1].y, ray_directions[i + 0].y);
1995 __m256 dir_z = _mm256_set_ps(ray_directions[i + 7].z, ray_directions[i + 6].z, ray_directions[i + 5].z, ray_directions[i + 4].z, ray_directions[i + 3].z, ray_directions[i + 2].z, ray_directions[i + 1].z, ray_directions[i + 0].z);
1996
1997 // Load 8 AABB mins
1998 __m256 aabb_min_x = _mm256_set_ps(aabb_mins[i + 7].x, aabb_mins[i + 6].x, aabb_mins[i + 5].x, aabb_mins[i + 4].x, aabb_mins[i + 3].x, aabb_mins[i + 2].x, aabb_mins[i + 1].x, aabb_mins[i + 0].x);
1999 __m256 aabb_min_y = _mm256_set_ps(aabb_mins[i + 7].y, aabb_mins[i + 6].y, aabb_mins[i + 5].y, aabb_mins[i + 4].y, aabb_mins[i + 3].y, aabb_mins[i + 2].y, aabb_mins[i + 1].y, aabb_mins[i + 0].y);
2000 __m256 aabb_min_z = _mm256_set_ps(aabb_mins[i + 7].z, aabb_mins[i + 6].z, aabb_mins[i + 5].z, aabb_mins[i + 4].z, aabb_mins[i + 3].z, aabb_mins[i + 2].z, aabb_mins[i + 1].z, aabb_mins[i + 0].z);
2001
2002 // Load 8 AABB maxs
2003 __m256 aabb_max_x = _mm256_set_ps(aabb_maxs[i + 7].x, aabb_maxs[i + 6].x, aabb_maxs[i + 5].x, aabb_maxs[i + 4].x, aabb_maxs[i + 3].x, aabb_maxs[i + 2].x, aabb_maxs[i + 1].x, aabb_maxs[i + 0].x);
2004 __m256 aabb_max_y = _mm256_set_ps(aabb_maxs[i + 7].y, aabb_maxs[i + 6].y, aabb_maxs[i + 5].y, aabb_maxs[i + 4].y, aabb_maxs[i + 3].y, aabb_maxs[i + 2].y, aabb_maxs[i + 1].y, aabb_maxs[i + 0].y);
2005 __m256 aabb_max_z = _mm256_set_ps(aabb_maxs[i + 7].z, aabb_maxs[i + 6].z, aabb_maxs[i + 5].z, aabb_maxs[i + 4].z, aabb_maxs[i + 3].z, aabb_maxs[i + 2].z, aabb_maxs[i + 1].z, aabb_maxs[i + 0].z);
2006
2007 // Calculate inverse directions
2008 __m256 inv_dir_x = _mm256_div_ps(_mm256_set1_ps(1.0f), dir_x);
2009 __m256 inv_dir_y = _mm256_div_ps(_mm256_set1_ps(1.0f), dir_y);
2010 __m256 inv_dir_z = _mm256_div_ps(_mm256_set1_ps(1.0f), dir_z);
2011
2012 // Calculate intersection distances for X axis
2013 __m256 t1_x = _mm256_mul_ps(_mm256_sub_ps(aabb_min_x, orig_x), inv_dir_x);
2014 __m256 t2_x = _mm256_mul_ps(_mm256_sub_ps(aabb_max_x, orig_x), inv_dir_x);
2015 __m256 tmin_x = _mm256_min_ps(t1_x, t2_x);
2016 __m256 tmax_x = _mm256_max_ps(t1_x, t2_x);
2017
2018 // Calculate intersection distances for Y axis
2019 __m256 t1_y = _mm256_mul_ps(_mm256_sub_ps(aabb_min_y, orig_y), inv_dir_y);
2020 __m256 t2_y = _mm256_mul_ps(_mm256_sub_ps(aabb_max_y, orig_y), inv_dir_y);
2021 __m256 tmin_y = _mm256_min_ps(t1_y, t2_y);
2022 __m256 tmax_y = _mm256_max_ps(t1_y, t2_y);
2023
2024 // Calculate intersection distances for Z axis
2025 __m256 t1_z = _mm256_mul_ps(_mm256_sub_ps(aabb_min_z, orig_z), inv_dir_z);
2026 __m256 t2_z = _mm256_mul_ps(_mm256_sub_ps(aabb_max_z, orig_z), inv_dir_z);
2027 __m256 tmin_z = _mm256_min_ps(t1_z, t2_z);
2028 __m256 tmax_z = _mm256_max_ps(t1_z, t2_z);
2029
2030 // Find intersection interval
2031 __m256 t_min_final = _mm256_max_ps(_mm256_max_ps(tmin_x, tmin_y), tmin_z);
2032 __m256 t_max_final = _mm256_min_ps(_mm256_min_ps(tmax_x, tmax_y), tmax_z);
2033
2034 // Store results
2035 _mm256_store_ps(&t_mins[i], t_min_final);
2036 _mm256_store_ps(&t_maxs[i], t_max_final);
2037
2038 // Check for intersection: t_max >= 0 && t_min <= t_max
2039 __m256 zero = _mm256_set1_ps(0.0f);
2040 __m256 hits = _mm256_and_ps(_mm256_cmp_ps(t_max_final, zero, _CMP_GE_OS), _mm256_cmp_ps(t_min_final, t_max_final, _CMP_LE_OS));
2041
2042 // Convert to bitmask
2043 hit_mask |= _mm256_movemask_ps(hits);
2044 }
2045
2046 return hit_mask;
2047 }
2048#endif
2049
2050#ifdef __SSE4_1__
2051 if (count == 4) {
2052 // SSE implementation for 4 rays at once
2053 uint32_t hit_mask = 0;
2054
2055 for (int i = 0; i < 4; i += 4) {
2056 // Load 4 ray origins
2057 __m128 orig_x = _mm_set_ps(ray_origins[i + 3].x, ray_origins[i + 2].x, ray_origins[i + 1].x, ray_origins[i + 0].x);
2058 __m128 orig_y = _mm_set_ps(ray_origins[i + 3].y, ray_origins[i + 2].y, ray_origins[i + 1].y, ray_origins[i + 0].y);
2059 __m128 orig_z = _mm_set_ps(ray_origins[i + 3].z, ray_origins[i + 2].z, ray_origins[i + 1].z, ray_origins[i + 0].z);
2060
2061 // Load 4 ray directions
2062 __m128 dir_x = _mm_set_ps(ray_directions[i + 3].x, ray_directions[i + 2].x, ray_directions[i + 1].x, ray_directions[i + 0].x);
2063 __m128 dir_y = _mm_set_ps(ray_directions[i + 3].y, ray_directions[i + 2].y, ray_directions[i + 1].y, ray_directions[i + 0].y);
2064 __m128 dir_z = _mm_set_ps(ray_directions[i + 3].z, ray_directions[i + 2].z, ray_directions[i + 1].z, ray_directions[i + 0].z);
2065
2066 // Load 4 AABB mins
2067 __m128 aabb_min_x = _mm_set_ps(aabb_mins[i + 3].x, aabb_mins[i + 2].x, aabb_mins[i + 1].x, aabb_mins[i + 0].x);
2068 __m128 aabb_min_y = _mm_set_ps(aabb_mins[i + 3].y, aabb_mins[i + 2].y, aabb_mins[i + 1].y, aabb_mins[i + 0].y);
2069 __m128 aabb_min_z = _mm_set_ps(aabb_mins[i + 3].z, aabb_mins[i + 2].z, aabb_mins[i + 1].z, aabb_mins[i + 0].z);
2070
2071 // Load 4 AABB maxs
2072 __m128 aabb_max_x = _mm_set_ps(aabb_maxs[i + 3].x, aabb_maxs[i + 2].x, aabb_maxs[i + 1].x, aabb_maxs[i + 0].x);
2073 __m128 aabb_max_y = _mm_set_ps(aabb_maxs[i + 3].y, aabb_maxs[i + 2].y, aabb_maxs[i + 1].y, aabb_maxs[i + 0].y);
2074 __m128 aabb_max_z = _mm_set_ps(aabb_maxs[i + 3].z, aabb_maxs[i + 2].z, aabb_maxs[i + 1].z, aabb_maxs[i + 0].z);
2075
2076 // Calculate inverse directions
2077 __m128 inv_dir_x = _mm_div_ps(_mm_set1_ps(1.0f), dir_x);
2078 __m128 inv_dir_y = _mm_div_ps(_mm_set1_ps(1.0f), dir_y);
2079 __m128 inv_dir_z = _mm_div_ps(_mm_set1_ps(1.0f), dir_z);
2080
2081 // Calculate intersection distances for X axis
2082 __m128 t1_x = _mm_mul_ps(_mm_sub_ps(aabb_min_x, orig_x), inv_dir_x);
2083 __m128 t2_x = _mm_mul_ps(_mm_sub_ps(aabb_max_x, orig_x), inv_dir_x);
2084 __m128 tmin_x = _mm_min_ps(t1_x, t2_x);
2085 __m128 tmax_x = _mm_max_ps(t1_x, t2_x);
2086
2087 // Calculate intersection distances for Y axis
2088 __m128 t1_y = _mm_mul_ps(_mm_sub_ps(aabb_min_y, orig_y), inv_dir_y);
2089 __m128 t2_y = _mm_mul_ps(_mm_sub_ps(aabb_max_y, orig_y), inv_dir_y);
2090 __m128 tmin_y = _mm_min_ps(t1_y, t2_y);
2091 __m128 tmax_y = _mm_max_ps(t1_y, t2_y);
2092
2093 // Calculate intersection distances for Z axis
2094 __m128 t1_z = _mm_mul_ps(_mm_sub_ps(aabb_min_z, orig_z), inv_dir_z);
2095 __m128 t2_z = _mm_mul_ps(_mm_sub_ps(aabb_max_z, orig_z), inv_dir_z);
2096 __m128 tmin_z = _mm_min_ps(t1_z, t2_z);
2097 __m128 tmax_z = _mm_max_ps(t1_z, t2_z);
2098
2099 // Find intersection interval
2100 __m128 t_min_final = _mm_max_ps(_mm_max_ps(tmin_x, tmin_y), tmin_z);
2101 __m128 t_max_final = _mm_min_ps(_mm_min_ps(tmax_x, tmax_y), tmax_z);
2102
2103 // Store results
2104 _mm_store_ps(&t_mins[i], t_min_final);
2105 _mm_store_ps(&t_maxs[i], t_max_final);
2106
2107 // Check for intersection: t_max >= 0 && t_min <= t_max
2108 __m128 zero = _mm_set1_ps(0.0f);
2109 __m128 hits = _mm_and_ps(_mm_cmpge_ps(t_max_final, zero), _mm_cmple_ps(t_min_final, t_max_final));
2110
2111 // Convert to bitmask
2112 hit_mask |= _mm_movemask_ps(hits);
2113 }
2114
2115 return hit_mask;
2116 }
2117#endif
2118
2119 // Fallback to scalar implementation
2120 uint32_t hit_mask = 0;
2121 for (int i = 0; i < count; ++i) {
2122 if (rayAABBIntersect(ray_origins[i], ray_directions[i], aabb_mins[i], aabb_maxs[i], t_mins[i], t_maxs[i])) {
2123 hit_mask |= (1 << i);
2124 }
2125 }
2126 return hit_mask;
2127}
2128
2129void CollisionDetection::traverseBVHSIMD(const vec3 *ray_origins, const vec3 *ray_directions, int count, HitResult *results) {
2130 if (bvh_nodes.empty()) {
2131 // Initialize all results as misses
2132 for (int i = 0; i < count; ++i) {
2133 results[i] = HitResult();
2134 }
2135 return;
2136 }
2137
2138 // Determine SIMD batch size based on available instructions
2139 int simd_batch_size = 1; // Default to scalar
2140#ifdef __AVX2__
2141 simd_batch_size = 8;
2142#elif defined(__SSE4_1__)
2143 simd_batch_size = 4;
2144#endif
2145
2146 // Process rays in SIMD batches
2147 for (int batch_start = 0; batch_start < count; batch_start += simd_batch_size) {
2148 int batch_count = std::min(simd_batch_size, count - batch_start);
2149
2150 // Initialize batch results
2151 for (int i = 0; i < batch_count; ++i) {
2152 results[batch_start + i] = HitResult();
2153 }
2154
2155 if (batch_count >= simd_batch_size && simd_batch_size > 1) {
2156 // SIMD-optimized batch processing
2157 traverseBVHSIMDImpl(&ray_origins[batch_start], &ray_directions[batch_start], batch_count, &results[batch_start]);
2158 } else {
2159 // Process remaining rays individually
2160 for (int i = 0; i < batch_count; ++i) {
2161 int ray_idx = batch_start + i;
2162 results[ray_idx] = castRay(RayQuery(ray_origins[ray_idx], ray_directions[ray_idx]));
2163 }
2164 }
2165 }
2166}
2167
2168void CollisionDetection::traverseBVHSIMDImpl(const vec3 *ray_origins, const vec3 *ray_directions, int count, HitResult *results) {
2169 const size_t MAX_STACK_SIZE = 64;
2170
2171 // Per-ray data structures - using aligned arrays for SIMD efficiency
2172 alignas(32) uint32_t node_stacks[8][MAX_STACK_SIZE]; // 8 stacks for up to 8 rays
2173 alignas(32) uint32_t stack_tops[8] = {0}; // Current stack positions
2174 alignas(32) float closest_distances[8]; // Closest hit distances per ray
2175 alignas(32) bool ray_active[8]; // Which rays are still active
2176
2177 // Initialize per-ray state
2178 for (int i = 0; i < count; ++i) {
2179 node_stacks[i][0] = 0; // Start with root node
2180 stack_tops[i] = 1;
2181 closest_distances[i] = std::numeric_limits<float>::max();
2182 ray_active[i] = true;
2183 results[i] = HitResult(); // Initialize as miss
2184 }
2185
2186 // Main traversal loop - continue while any ray is active
2187 while (true) {
2188 bool any_active = false;
2189 for (int i = 0; i < count; ++i) {
2190 if (ray_active[i] && stack_tops[i] > 0) {
2191 any_active = true;
2192 break;
2193 }
2194 }
2195 if (!any_active)
2196 break;
2197
2198 // Collect next nodes to test for active rays
2199 alignas(32) vec3 test_aabb_mins[8];
2200 alignas(32) vec3 test_aabb_maxs[8];
2201 alignas(32) uint32_t test_node_indices[8];
2202 alignas(32) int test_ray_indices[8];
2203 int test_count = 0;
2204
2205 for (int i = 0; i < count; ++i) {
2206 if (ray_active[i] && stack_tops[i] > 0) {
2207 uint32_t node_idx = node_stacks[i][--stack_tops[i]];
2208 const BVHNode &node = bvh_nodes[node_idx];
2209
2210 test_aabb_mins[test_count] = node.aabb_min;
2211 test_aabb_maxs[test_count] = node.aabb_max;
2212 test_node_indices[test_count] = node_idx;
2213 test_ray_indices[test_count] = i;
2214 test_count++;
2215
2216 if (test_count == count)
2217 break; // Batch is full
2218 }
2219 }
2220
2221 if (test_count == 0)
2222 break;
2223
2224 // Prepare ray data for SIMD intersection test
2225 alignas(32) vec3 batch_origins[8];
2226 alignas(32) vec3 batch_directions[8];
2227 alignas(32) float t_mins[8];
2228 alignas(32) float t_maxs[8];
2229
2230 for (int i = 0; i < test_count; ++i) {
2231 int ray_idx = test_ray_indices[i];
2232 batch_origins[i] = ray_origins[ray_idx];
2233 batch_directions[i] = ray_directions[ray_idx];
2234 }
2235
2236 // Perform SIMD AABB intersection test
2237 uint32_t hit_mask = rayAABBIntersectSIMD(batch_origins, batch_directions, test_aabb_mins, test_aabb_maxs, t_mins, t_maxs, test_count);
2238
2239 // Process intersection results
2240 for (int i = 0; i < test_count; ++i) {
2241 if (!(hit_mask & (1 << i)))
2242 continue; // Ray missed this AABB
2243
2244 int ray_idx = test_ray_indices[i];
2245 uint32_t node_idx = test_node_indices[i];
2246 const BVHNode &node = bvh_nodes[node_idx];
2247
2248 if (t_mins[i] > closest_distances[ray_idx])
2249 continue; // Beyond closest hit
2250
2251 if (node.is_leaf) {
2252 // Test primitives in leaf node
2253 for (uint32_t prim_idx = node.primitive_start; prim_idx < node.primitive_start + node.primitive_count; ++prim_idx) {
2254
2255 uint32_t primitive_id = primitive_indices[prim_idx];
2256
2257 // Use thread-safe primitive intersection
2258 HitResult prim_result = intersectPrimitiveThreadSafe(batch_origins[i], batch_directions[i], primitive_id, closest_distances[ray_idx]);
2259 if (prim_result.hit && prim_result.distance < closest_distances[ray_idx]) {
2260 closest_distances[ray_idx] = prim_result.distance;
2261 results[ray_idx] = prim_result;
2262 }
2263 }
2264 } else {
2265 // Add child nodes to stack (if space available)
2266 if (stack_tops[ray_idx] < MAX_STACK_SIZE - 2) {
2267 node_stacks[ray_idx][stack_tops[ray_idx]++] = node.left_child;
2268 node_stacks[ray_idx][stack_tops[ray_idx]++] = node.right_child;
2269 } else {
2270 // Stack overflow - mark ray as inactive to prevent infinite loop
2271 ray_active[ray_idx] = false;
2272 }
2273 }
2274 }
2275 }
2276}
2277#endif
2278
2279
2280bool CollisionDetection::rayAABBIntersectPrimitive(const helios::vec3 &origin, const helios::vec3 &direction, const helios::vec3 &aabb_min, const helios::vec3 &aabb_max, float &distance) {
2281 // Ray-AABB intersection using slab method
2282 // Optimized version with early termination
2283
2284 const float EPSILON = 1e-8f;
2285
2286 // Calculate t values for each slab
2287 float t_min_x = (aabb_min.x - origin.x) / direction.x;
2288 float t_max_x = (aabb_max.x - origin.x) / direction.x;
2289
2290 // Handle negative direction components
2291 if (direction.x < 0.0f) {
2292 float temp = t_min_x;
2293 t_min_x = t_max_x;
2294 t_max_x = temp;
2295 }
2296
2297 float t_min_y = (aabb_min.y - origin.y) / direction.y;
2298 float t_max_y = (aabb_max.y - origin.y) / direction.y;
2299
2300 if (direction.y < 0.0f) {
2301 float temp = t_min_y;
2302 t_min_y = t_max_y;
2303 t_max_y = temp;
2304 }
2305
2306 // Check for early termination in X-Y
2307 float t_min = std::max(t_min_x, t_min_y);
2308 float t_max = std::min(t_max_x, t_max_y);
2309
2310 if (t_min > t_max) {
2311 return false; // No intersection
2312 }
2313
2314 float t_min_z = (aabb_min.z - origin.z) / direction.z;
2315 float t_max_z = (aabb_max.z - origin.z) / direction.z;
2316
2317 if (direction.z < 0.0f) {
2318 float temp = t_min_z;
2319 t_min_z = t_max_z;
2320 t_max_z = temp;
2321 }
2322
2323 // Final intersection test
2324 t_min = std::max(t_min, t_min_z);
2325 t_max = std::min(t_max, t_max_z);
2326
2327 if (t_min > t_max || t_max < EPSILON) {
2328 return false; // No intersection or behind ray
2329 }
2330
2331 // Set distance to closest intersection point
2332 distance = (t_min > EPSILON) ? t_min : t_max;
2333
2334 return distance > EPSILON;
2335}