1.3.77
 
Loading...
Searching...
No Matches
OptiX8Backend.cpp
Go to the documentation of this file.
1
16#include "OptiX8Backend.h"
17
18// OptiX function table definition (must be in exactly one .cpp)
19#include <optix_function_table_definition.h>
20
21#include "Context.h"
22
23#include <algorithm>
24#include <cfloat>
25#include <fstream>
26
27namespace helios {
28
29// ---------------------------------------------------------------------------
30// Construction / destruction
31// ---------------------------------------------------------------------------
32
33OptiX8Backend::OptiX8Backend() = default;
34
35bool OptiX8Backend::probe() noexcept {
36 try {
37 int device_count = 0;
38 cudaError_t rc = cudaGetDeviceCount(&device_count);
39 if (rc != cudaSuccess || device_count == 0) {
40 return false;
41 }
42 OptixResult optix_rc = optixInit();
43 return (optix_rc == OPTIX_SUCCESS);
44 } catch (...) {
45 return false;
46 }
47}
48
49OptiX8Backend::~OptiX8Backend() {
50 if (is_initialized) {
51 shutdown();
52 }
53}
54
55// ---------------------------------------------------------------------------
56// Lifecycle
57// ---------------------------------------------------------------------------
58
60 // Initialize CUDA
61 CUDA_CHECK(cudaFree(nullptr)); // Force CUDA context initialization
62
63 // Create CUDA stream
64 CUDA_CHECK(cudaStreamCreate(&cuda_stream));
65
66 // Initialize OptiX function table from the loaded driver
67 OPTIX_CHECK(optixInit());
68
69 // Create OptiX device context
70 CUcontext cuda_context = nullptr; // use current context
71 OptixDeviceContextOptions ctx_options = {};
72 ctx_options.logCallbackFunction = [](unsigned int level, const char *tag, const char *message, void *) {
73 if (level <= 2) {
74 std::cerr << "[OptiX][" << tag << "] " << message << "\n";
75 }
76 };
77 ctx_options.logCallbackLevel = 2;
78 OPTIX_CHECK(optixDeviceContextCreate(cuda_context, &ctx_options, &optix_context));
79
80 // Load device code (PTX or OptixIR) and compile module
81 const std::string device_code_path = findDeviceCodeFile();
82 std::ifstream file(device_code_path, std::ios::binary | std::ios::ate);
83 if (!file.is_open()) {
84 helios_runtime_error("ERROR (OptiX8Backend::initialize): Could not open device code file: " + device_code_path);
85 }
86 const std::streamsize file_size = file.tellg();
87 file.seekg(0, std::ios::beg);
88 std::vector<char> device_code(file_size);
89 if (!file.read(device_code.data(), file_size)) {
90 helios_runtime_error("ERROR (OptiX8Backend::initialize): Could not read device code file: " + device_code_path);
91 }
92
93 OptixModuleCompileOptions module_options = {};
94 module_options.maxRegisterCount = OPTIX_COMPILE_DEFAULT_MAX_REGISTER_COUNT;
95#ifndef NDEBUG
96 module_options.optLevel = OPTIX_COMPILE_OPTIMIZATION_LEVEL_0;
97 module_options.debugLevel = OPTIX_COMPILE_DEBUG_LEVEL_FULL;
98#else
99 module_options.optLevel = OPTIX_COMPILE_OPTIMIZATION_LEVEL_3;
100 module_options.debugLevel = OPTIX_COMPILE_DEBUG_LEVEL_NONE;
101#endif
102
103 OptixPipelineCompileOptions pipeline_options = {};
104 pipeline_options.usesMotionBlur = 0;
105 pipeline_options.traversableGraphFlags = OPTIX_TRAVERSABLE_GRAPH_FLAG_ALLOW_SINGLE_GAS;
106 pipeline_options.numPayloadValues = 2; // two uint32 for pointer-in-registers
107 pipeline_options.numAttributeValues = 2; // UUID + face in attributes
108 pipeline_options.exceptionFlags = OPTIX_EXCEPTION_FLAG_NONE;
109 pipeline_options.pipelineLaunchParamsVariableName = "params";
110 pipeline_options.usesPrimitiveTypeFlags = OPTIX_PRIMITIVE_TYPE_FLAGS_CUSTOM;
111
112 char log[4096];
113 size_t log_size = sizeof(log);
114
115 OPTIX_CHECK(optixModuleCreate(
116 optix_context,
117 &module_options,
118 &pipeline_options,
119 device_code.data(),
120 static_cast<size_t>(file_size),
121 log, &log_size,
122 &optix_module));
123
124 // ---- Create program groups ----
125 OptixProgramGroupOptions pg_options = {};
126
127 // Raygen programs
128 auto createRaygen = [&](const char *entry, OptixProgramGroup &pg) {
129 OptixProgramGroupDesc desc = {};
130 desc.kind = OPTIX_PROGRAM_GROUP_KIND_RAYGEN;
131 desc.raygen.module = optix_module;
132 desc.raygen.entryFunctionName = entry;
133 log_size = sizeof(log);
134 OPTIX_CHECK(optixProgramGroupCreate(optix_context, &desc, 1, &pg_options, log, &log_size, &pg));
135 (void)log_size;
136 };
137
138 createRaygen("__raygen__direct", pg_raygen_direct);
139 createRaygen("__raygen__diffuse", pg_raygen_diffuse);
140 createRaygen("__raygen__camera", pg_raygen_camera);
141 createRaygen("__raygen__pixel_label", pg_raygen_pixel_label);
142
143 // Miss programs
144 auto createMiss = [&](const char *entry, OptixProgramGroup &pg) {
145 OptixProgramGroupDesc desc = {};
146 desc.kind = OPTIX_PROGRAM_GROUP_KIND_MISS;
147 desc.miss.module = optix_module;
148 desc.miss.entryFunctionName = entry;
149 log_size = sizeof(log);
150 OPTIX_CHECK(optixProgramGroupCreate(optix_context, &desc, 1, &pg_options, log, &log_size, &pg));
151 (void)log_size;
152 };
153
154 createMiss("__miss__direct", pg_miss_direct);
155 createMiss("__miss__diffuse", pg_miss_diffuse);
156 createMiss("__miss__camera", pg_miss_camera);
157 createMiss("__miss__pixel_label", pg_miss_pixel_label);
158
159 // Hit groups: one per ray type (4 total). All geometry uses __intersection__patch,
160 // which dispatches internally on primitive type. With a single GAS and numSbtRecords=1,
161 // the SBT hit record index = sbt_offset from optixTrace (stride=0 for all ray types).
162
163 // ah_entry may be nullptr (no any-hit). The any-hit programs implement translucent-cover
164 // (glass/plastic) pass-through with attenuation for the direct and diffuse ray types.
165 auto createHitGroup = [&](const char *ch_entry, const char *is_entry, const char *ah_entry, OptixProgramGroup &pg) {
166 OptixProgramGroupDesc desc = {};
167 desc.kind = OPTIX_PROGRAM_GROUP_KIND_HITGROUP;
168 desc.hitgroup.moduleCH = optix_module;
169 desc.hitgroup.entryFunctionNameCH = ch_entry;
170 desc.hitgroup.moduleIS = optix_module;
171 desc.hitgroup.entryFunctionNameIS = is_entry;
172 desc.hitgroup.moduleAH = ah_entry ? optix_module : nullptr;
173 desc.hitgroup.entryFunctionNameAH = ah_entry;
174 log_size = sizeof(log);
175 OPTIX_CHECK(optixProgramGroupCreate(optix_context, &desc, 1, &pg_options, log, &log_size, &pg));
176 (void)log_size;
177 };
178
179 createHitGroup("__closesthit__direct", "__intersection__patch", "__anyhit__direct", pg_hit_direct);
180 createHitGroup("__closesthit__diffuse", "__intersection__patch", "__anyhit__diffuse", pg_hit_diffuse);
181 createHitGroup("__closesthit__camera", "__intersection__patch", nullptr, pg_hit_camera);
182 createHitGroup("__closesthit__pixel_label", "__intersection__patch", nullptr, pg_hit_pixel_label);
183
184 // ---- Create pipeline ----
185 OptixProgramGroup all_groups[] = {
186 pg_raygen_direct, pg_raygen_diffuse, pg_raygen_camera, pg_raygen_pixel_label,
187 pg_miss_direct, pg_miss_diffuse, pg_miss_camera, pg_miss_pixel_label,
188 pg_hit_direct, pg_hit_diffuse, pg_hit_camera, pg_hit_pixel_label
189 };
190
191 OptixPipelineLinkOptions link_options = {};
192 link_options.maxTraceDepth = 1;
193
194 log_size = sizeof(log);
195 OPTIX_CHECK(optixPipelineCreate(
196 optix_context,
197 &pipeline_options,
198 &link_options,
199 all_groups,
200 sizeof(all_groups) / sizeof(all_groups[0]),
201 log, &log_size,
202 &optix_pipeline));
203
204 // Set pipeline stack sizes using OptiX utilities
205 OptixStackSizes stack_sizes = {};
206 for (auto &pg : all_groups) {
207 OPTIX_CHECK(optixUtilAccumulateStackSizes(pg, &stack_sizes, optix_pipeline));
208 }
209 uint32_t max_trace_depth = 1;
210 uint32_t direct_callable_stack_size_from_traversal = 0;
211 uint32_t direct_callable_stack_size_from_state = 0;
212 uint32_t continuation_stack_size = 0;
213 OPTIX_CHECK(optixUtilComputeStackSizes(
214 &stack_sizes,
215 max_trace_depth,
216 0, // maxCCDepth (no continuation callables)
217 0, // maxDCDepth (no direct callables)
218 &direct_callable_stack_size_from_traversal,
219 &direct_callable_stack_size_from_state,
220 &continuation_stack_size));
221 OPTIX_CHECK(optixPipelineSetStackSize(
222 optix_pipeline,
223 direct_callable_stack_size_from_traversal,
224 direct_callable_stack_size_from_state,
225 continuation_stack_size,
226 1 /* maxTraversableGraphDepth */));
227
228 // Allocate device-side launch params buffer
229 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_params), sizeof(OptiX8LaunchParams)));
230 memset(&h_params, 0, sizeof(h_params));
231
232 is_initialized = true;
233}
234
236 if (!is_initialized) {
237 return;
238 }
239
240 // Synchronize before cleanup
241 if (cuda_stream) {
242 cudaStreamSynchronize(cuda_stream);
243 }
244
245 // Free device buffers
246 freeGeometryBuffers();
247 freeMaterialBuffers();
248
249 auto freePtr = [](CUdeviceptr &ptr) {
250 if (ptr) { cudaFree(reinterpret_cast<void *>(ptr)); ptr = 0; }
251 };
252
253 freePtr(d_radiation_in);
254 freePtr(d_radiation_out_top);
255 freePtr(d_radiation_out_bottom);
256 freePtr(d_scatter_buff_top);
257 freePtr(d_scatter_buff_bottom);
258 freePtr(d_radiation_in_camera);
259 freePtr(d_scatter_buff_top_cam);
260 freePtr(d_scatter_buff_bottom_cam);
261 freePtr(d_radiation_specular);
262 freePtr(d_Rsky);
263 freePtr(d_camera_pixel_label);
264 freePtr(d_camera_pixel_depth);
265 freePtr(d_source_positions);
266 freePtr(d_source_rotations);
267 freePtr(d_source_widths);
268 freePtr(d_source_types);
269 freePtr(d_source_fluxes);
270 freePtr(d_source_fluxes_cam);
271 freePtr(d_diffuse_flux);
272 freePtr(d_diffuse_extinction);
273 freePtr(d_diffuse_peak_dir);
274 freePtr(d_diffuse_dist_norm);
275 freePtr(d_sky_radiance_params);
276 freePtr(d_camera_sky_radiance);
277 freePtr(d_solar_disk_radiance);
278 freePtr(d_camera_diffuse_flux);
279 freePtr(d_band_emission_flag);
280 freePtr(d_band_launch_flag);
281 freePtr(d_mask_data);
282 freePtr(d_mask_sizes);
283 freePtr(d_mask_IDs);
284 freePtr(d_uv_data);
285 freePtr(d_uv_IDs);
286 freePtr(d_params);
287
288 // Free SBT device memory
289 if (d_raygen_records) { cudaFree(reinterpret_cast<void *>(d_raygen_records)); d_raygen_records = 0; }
290 if (d_miss_records) { cudaFree(reinterpret_cast<void *>(d_miss_records)); d_miss_records = 0; }
291 if (d_hitgroup_records) { cudaFree(reinterpret_cast<void *>(d_hitgroup_records)); d_hitgroup_records = 0; }
292
293 // Free GAS
294 if (d_gas_output) { cudaFree(reinterpret_cast<void *>(d_gas_output)); d_gas_output = 0; }
295
296 // Destroy program groups
297 auto destroyPG = [](OptixProgramGroup &pg) {
298 if (pg) { optixProgramGroupDestroy(pg); pg = nullptr; }
299 };
300 destroyPG(pg_raygen_direct); destroyPG(pg_raygen_diffuse);
301 destroyPG(pg_raygen_camera); destroyPG(pg_raygen_pixel_label);
302 destroyPG(pg_miss_direct); destroyPG(pg_miss_diffuse);
303 destroyPG(pg_miss_camera); destroyPG(pg_miss_pixel_label);
304 destroyPG(pg_hit_direct); destroyPG(pg_hit_diffuse);
305 destroyPG(pg_hit_camera); destroyPG(pg_hit_pixel_label);
306
307 if (optix_pipeline) { optixPipelineDestroy(optix_pipeline); optix_pipeline = nullptr; }
308 if (optix_module) { optixModuleDestroy(optix_module); optix_module = nullptr; }
309 if (optix_context) { optixDeviceContextDestroy(optix_context); optix_context = nullptr; }
310 if (cuda_stream) { cudaStreamDestroy(cuda_stream); cuda_stream = nullptr; }
311
312 is_initialized = false;
313}
314
315// ---------------------------------------------------------------------------
316// Geometry management
317// ---------------------------------------------------------------------------
318
320 validateGeometryBeforeUpload(geometry);
321
322 // Validate that all primitive types are supported by the OptiX 8.1 backend.
323 // Supported: patch (0), triangle (1), tile (3). Disk (2), voxel (4), and
324 // bbox (5) intersection programs are not yet implemented.
325 if (geometry.disk_count > 0) {
326 helios_runtime_error("ERROR (OptiX8Backend::updateGeometry): Scene contains disk primitives, "
327 "which are not yet supported by the OptiX 8.1 backend.");
328 }
329 if (geometry.voxel_count > 0) {
330 helios_runtime_error("ERROR (OptiX8Backend::updateGeometry): Scene contains voxel primitives, "
331 "which are not yet supported by the OptiX 8.1 backend.");
332 }
333
334 freeGeometryBuffers();
335
336 auto upload = [this](CUdeviceptr &d_ptr, const void *src, size_t bytes) {
337 if (bytes > 0 && src) {
338 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_ptr), bytes));
339 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_ptr), src, bytes, cudaMemcpyHostToDevice));
340 }
341 };
342
343 upload(d_transform_matrix, geometry.transform_matrices.data(), geometry.transform_matrices.size() * sizeof(float));
344 upload(d_primitive_type, geometry.primitive_types.data(), geometry.primitive_types.size() * sizeof(uint32_t));
345 upload(d_primitive_positions, geometry.primitive_positions.data(), geometry.primitive_positions.size() * sizeof(uint32_t));
346 upload(d_primitive_uuid_arr, geometry.primitive_UUIDs.data(), geometry.primitive_UUIDs.size() * sizeof(uint32_t));
347 upload(d_primitiveID, geometry.primitive_IDs.data(), geometry.primitive_IDs.size() * sizeof(uint32_t));
348 upload(d_objectID, geometry.object_IDs.data(), geometry.object_IDs.size() * sizeof(uint32_t));
349 upload(d_twosided_flag, geometry.twosided_flags.data(), geometry.twosided_flags.size() * sizeof(char));
350 upload(d_primitive_solid_fraction, geometry.solid_fractions.data(), geometry.solid_fractions.size() * sizeof(float));
351
352 // object_subdivisions: vector<helios::int2> → flat int32 array (2 ints per prim)
353 if (!geometry.object_subdivisions.empty()) {
354 const size_t bytes = geometry.object_subdivisions.size() * sizeof(helios::int2);
355 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_object_subdivisions), bytes));
356 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_object_subdivisions),
357 geometry.object_subdivisions.data(), bytes, cudaMemcpyHostToDevice));
358 }
359
360 // Per-type geometry
361 if (geometry.patch_count > 0) {
362 upload(d_patch_vertices, geometry.patches.vertices.data(),
363 geometry.patches.vertices.size() * sizeof(helios::vec3));
364 upload(d_patch_UUIDs, geometry.patches.UUIDs.data(),
365 geometry.patches.UUIDs.size() * sizeof(uint32_t));
366 }
367 if (geometry.triangles.count > 0) {
368 upload(d_triangle_vertices, geometry.triangles.vertices.data(),
369 geometry.triangles.vertices.size() * sizeof(helios::vec3));
370 upload(d_triangle_UUIDs, geometry.triangles.UUIDs.data(),
371 geometry.triangles.UUIDs.size() * sizeof(uint32_t));
372 }
373 if (geometry.disk_count > 0) {
374 upload(d_disk_centers, geometry.disk_centers.data(), geometry.disk_centers.size() * sizeof(helios::vec3));
375 upload(d_disk_radii, geometry.disk_radii.data(), geometry.disk_radii.size() * sizeof(float));
376 upload(d_disk_normals, geometry.disk_normals.data(), geometry.disk_normals.size() * sizeof(helios::vec3));
377 upload(d_disk_UUIDs, geometry.disk_UUIDs.data(), geometry.disk_UUIDs.size() * sizeof(uint32_t));
378 }
379 if (geometry.tiles.count > 0) {
380 upload(d_tile_vertices, geometry.tiles.vertices.data(),
381 geometry.tiles.vertices.size() * sizeof(helios::vec3));
382 upload(d_tile_UUIDs, geometry.tiles.UUIDs.data(),
383 geometry.tiles.UUIDs.size() * sizeof(uint32_t));
384 }
385 if (geometry.voxels.count > 0) {
386 upload(d_voxel_vertices, geometry.voxels.vertices.data(),
387 geometry.voxels.vertices.size() * sizeof(helios::vec3));
388 upload(d_voxel_UUIDs, geometry.voxels.UUIDs.data(),
389 geometry.voxels.UUIDs.size() * sizeof(uint32_t));
390 }
391 if (geometry.bbox_count > 0) {
392 upload(d_bbox_vertices, geometry.bboxes.vertices.data(),
393 geometry.bboxes.vertices.size() * sizeof(helios::vec3));
394 upload(d_bbox_UUIDs, geometry.bboxes.UUIDs.data(),
395 geometry.bboxes.UUIDs.size() * sizeof(uint32_t));
396 }
397
398 // Extend primitive_type and primitive_uuid arrays to include bbox entries (type=5).
399 // OptiX AABB indices are global: indices [0, Nprims) are real primitives,
400 // indices [Nprims, Nprims+Nbboxes) are bbox faces. The intersection dispatch
401 // program reads params.primitive_type[optixGetPrimitiveIndex()] and needs
402 // type-5 entries at those positions.
403 if (geometry.bbox_count > 0) {
404 const size_t Nprims = geometry.primitive_count;
405 const size_t Nbboxes = geometry.bbox_count;
406
407 freeCUdeviceptr(d_primitive_type);
408 std::vector<uint32_t> ext_types(geometry.primitive_types);
409 ext_types.resize(Nprims + Nbboxes, 5u);
410 upload(d_primitive_type, ext_types.data(), ext_types.size() * sizeof(uint32_t));
411
412 freeCUdeviceptr(d_primitive_uuid_arr);
413 std::vector<uint32_t> ext_uuids(geometry.primitive_UUIDs);
414 ext_uuids.insert(ext_uuids.end(),
415 geometry.bboxes.UUIDs.begin(), geometry.bboxes.UUIDs.end());
416 upload(d_primitive_uuid_arr, ext_uuids.data(), ext_uuids.size() * sizeof(uint32_t));
417 }
418
419 // ---- Texture mask and UV data ----
420 {
421 // Compute per-mask offsets (cumulative start index into mask_data)
422 std::vector<uint32_t> mask_offsets;
423 uint32_t cumulative = 0;
424 for (const auto &sz : geometry.mask_sizes) {
425 mask_offsets.push_back(cumulative);
426 cumulative += static_cast<uint32_t>(sz.x) * static_cast<uint32_t>(sz.y);
427 }
428
429 // Convert vector<bool> to flat uint8 array (1=opaque, 0=transparent)
430 std::vector<uint8_t> mask_data_u8(cumulative);
431 for (uint32_t i = 0; i < cumulative; ++i) {
432 mask_data_u8[i] = geometry.mask_data[i] ? 1u : 0u;
433 }
434
435 // Reformat UV data to flat [Nprims * 4] array (4 UV vertices per primitive).
436 // uv_IDs[p] >= 0 flags whether primitive p has custom UV data; all textured
437 // primitives store exactly 4 UV vertices in uv_data (triangles are padded).
438 const size_t Np = geometry.primitive_count;
439 std::vector<helios::vec2> uv_flat(Np * 4, helios::make_vec2(0.f, 0.f));
440 size_t uv_read = 0;
441 for (size_t p = 0; p < Np; ++p) {
442 if (!geometry.uv_IDs.empty() && geometry.uv_IDs[p] >= 0) {
443 for (int v = 0; v < 4 && uv_read < geometry.uv_data.size(); ++v) {
444 uv_flat[p * 4 + v] = geometry.uv_data[uv_read++];
445 }
446 }
447 }
448
449 freeCUdeviceptr(d_mask_data);
450 freeCUdeviceptr(d_mask_offsets);
451 freeCUdeviceptr(d_mask_sizes);
452 freeCUdeviceptr(d_mask_IDs);
453 freeCUdeviceptr(d_uv_data);
454 freeCUdeviceptr(d_uv_IDs);
455
456 upload(d_mask_data, mask_data_u8.data(), mask_data_u8.size() * sizeof(uint8_t));
457 upload(d_mask_offsets, mask_offsets.data(), mask_offsets.size() * sizeof(uint32_t));
458 upload(d_mask_sizes, geometry.mask_sizes.data(), geometry.mask_sizes.size() * sizeof(helios::int2));
459 upload(d_mask_IDs, geometry.mask_IDs.data(), geometry.mask_IDs.size() * sizeof(int32_t));
460 upload(d_uv_data, uv_flat.data(), uv_flat.size() * sizeof(helios::vec2));
461 upload(d_uv_IDs, geometry.uv_IDs.data(), geometry.uv_IDs.size() * sizeof(int32_t));
462 }
463
464 // Update h_params device pointers
465 const uint32_t Nprims = static_cast<uint32_t>(geometry.primitive_count);
466 h_params.transform_matrix = reinterpret_cast<float *>(d_transform_matrix);
467 h_params.primitive_type = reinterpret_cast<uint32_t *>(d_primitive_type);
468 h_params.primitive_positions = reinterpret_cast<uint32_t *>(d_primitive_positions);
469 h_params.primitive_uuid = reinterpret_cast<uint32_t *>(d_primitive_uuid_arr);
470 h_params.primitiveID = reinterpret_cast<uint32_t *>(d_primitiveID);
471 h_params.objectID = reinterpret_cast<uint32_t *>(d_objectID);
472 h_params.object_subdivisions = reinterpret_cast<int32_t *>(d_object_subdivisions);
473 h_params.twosided_flag = reinterpret_cast<int8_t *>(d_twosided_flag);
474 h_params.primitive_solid_fraction = reinterpret_cast<float *>(d_primitive_solid_fraction);
475 h_params.patch_vertices = reinterpret_cast<float3 *>(d_patch_vertices);
476 h_params.patch_UUIDs = reinterpret_cast<uint32_t *>(d_patch_UUIDs);
477 h_params.triangle_vertices = reinterpret_cast<float3 *>(d_triangle_vertices);
478 h_params.triangle_UUIDs = reinterpret_cast<uint32_t *>(d_triangle_UUIDs);
479 h_params.disk_centers = reinterpret_cast<float3 *>(d_disk_centers);
480 h_params.disk_radii = reinterpret_cast<float *>(d_disk_radii);
481 h_params.disk_normals = reinterpret_cast<float3 *>(d_disk_normals);
482 h_params.disk_UUIDs = reinterpret_cast<uint32_t *>(d_disk_UUIDs);
483 h_params.tile_vertices = reinterpret_cast<float3 *>(d_tile_vertices);
484 h_params.tile_UUIDs = reinterpret_cast<uint32_t *>(d_tile_UUIDs);
485 h_params.voxel_vertices = reinterpret_cast<float3 *>(d_voxel_vertices);
486 h_params.voxel_UUIDs = reinterpret_cast<uint32_t *>(d_voxel_UUIDs);
487 h_params.bbox_vertices = reinterpret_cast<float3 *>(d_bbox_vertices);
488 h_params.bbox_UUIDs = reinterpret_cast<uint32_t *>(d_bbox_UUIDs);
489 h_params.Nprimitives = Nprims;
490 h_params.bbox_UUID_base = geometry.bbox_UUID_base;
491 h_params.periodic_flag = make_float2(geometry.periodic_flag.x, geometry.periodic_flag.y);
492 h_params.mask_data = reinterpret_cast<uint8_t *>(d_mask_data);
493 h_params.mask_offsets = reinterpret_cast<uint32_t *>(d_mask_offsets);
494 h_params.mask_sizes = reinterpret_cast<int32_t *>(d_mask_sizes);
495 h_params.mask_IDs = reinterpret_cast<int32_t *>(d_mask_IDs);
496 h_params.uv_data = reinterpret_cast<float2 *>(d_uv_data);
497 h_params.uv_IDs = reinterpret_cast<int32_t *>(d_uv_IDs);
498
499 // Store counts
500 current_primitive_count = geometry.primitive_count;
501 current_patch_count = geometry.patch_count;
502 current_triangle_count = geometry.triangle_count;
503 current_disk_count = geometry.disk_count;
504 current_tile_count = geometry.tile_count;
505 current_voxel_count = geometry.voxel_count;
506 current_bbox_count = geometry.bbox_count;
507
508 buildAABBs(geometry);
509}
510
512 if (current_primitive_count == 0) {
513 helios_runtime_error("ERROR (OptiX8Backend::buildAccelerationStructure): No geometry uploaded. Call updateGeometry() first.");
514 }
515 buildGAS(static_cast<uint32_t>(current_primitive_count + current_bbox_count));
516 buildSBT();
517}
518
519// ---------------------------------------------------------------------------
520// Materials
521// ---------------------------------------------------------------------------
522
524 freeMaterialBuffers();
525
526 auto upload = [this](CUdeviceptr &d_ptr, const void *src, size_t bytes) {
527 if (bytes > 0 && src) {
528 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_ptr), bytes));
529 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_ptr), src, bytes, cudaMemcpyHostToDevice));
530 }
531 };
532
533 // rho/tau: always allocate at least Nprims*Nbands elements (zeroed) to prevent null pointer
534 // dereference in device code when Nsources==0 (diffuse-only scenarios).
535 {
536 const size_t Nprims = current_primitive_count;
537 const size_t Nbands = materials.num_bands;
538 const size_t alloc = std::max(materials.reflectivity.size(), std::max(Nprims * Nbands, (size_t)1));
539 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_rho), alloc * sizeof(float)));
540 CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_rho), 0, alloc * sizeof(float)));
541 if (!materials.reflectivity.empty()) {
542 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_rho), materials.reflectivity.data(),
543 materials.reflectivity.size() * sizeof(float), cudaMemcpyHostToDevice));
544 }
545 }
546 {
547 const size_t Nprims = current_primitive_count;
548 const size_t Nbands = materials.num_bands;
549 const size_t alloc = std::max(materials.transmissivity.size(), std::max(Nprims * Nbands, (size_t)1));
550 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_tau), alloc * sizeof(float)));
551 CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_tau), 0, alloc * sizeof(float)));
552 if (!materials.transmissivity.empty()) {
553 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_tau), materials.transmissivity.data(),
554 materials.transmissivity.size() * sizeof(float), cudaMemcpyHostToDevice));
555 }
556 }
557 if (!materials.reflectivity_cam.empty())
558 upload(d_rho_cam, materials.reflectivity_cam.data(), materials.reflectivity_cam.size() * sizeof(float));
559 if (!materials.transmissivity_cam.empty())
560 upload(d_tau_cam, materials.transmissivity_cam.data(), materials.transmissivity_cam.size() * sizeof(float));
561 if (!materials.specular_exponent.empty())
562 upload(d_specular_exponent, materials.specular_exponent.data(), materials.specular_exponent.size() * sizeof(float));
563 if (!materials.specular_scale.empty())
564 upload(d_specular_scale, materials.specular_scale.data(), materials.specular_scale.size() * sizeof(float));
565
566 // Translucent cover (glass/plastic) buffers. Only uploaded if any primitive uses the glass model;
567 // otherwise the device pointers stay null and the any-hit programs early-return (no overhead).
568 bool any_glass = false;
569 for (char g : materials.is_glass) {
570 if (g != 0) { any_glass = true; break; }
571 }
572 if (any_glass) {
573 // The device per-band cover-transmittance accumulator is a fixed-size array. Fail fast if the
574 // band count exceeds it rather than silently corrupting energy.
575 if (materials.num_bands > (size_t)HELIOS_MAX_RADIATION_BANDS) {
576 helios_runtime_error("ERROR (OptiX8Backend): translucent cover (glass) materials are in use with " + std::to_string(materials.num_bands) +
577 " radiation bands, which exceeds the compile-time maximum of " + std::to_string(HELIOS_MAX_RADIATION_BANDS) +
578 " (HELIOS_MAX_RADIATION_BANDS). Reduce the number of bands or increase the cap.");
579 }
580 upload(d_glass_n, materials.glass_n.data(), materials.glass_n.size() * sizeof(float));
581 upload(d_glass_KL, materials.glass_KL.data(), materials.glass_KL.size() * sizeof(float));
582 upload(d_is_glass, materials.is_glass.data(), materials.is_glass.size() * sizeof(char));
583 }
584
585 // Allocate radiation energy buffers (Nprims × Nbands_global)
586 const size_t Nprims = current_primitive_count;
587 const size_t Nbands = materials.num_bands;
588 const size_t rad_bytes = Nprims * Nbands * sizeof(float);
589
590 reallocDevice(d_radiation_in, rad_bytes);
591 reallocDevice(d_radiation_out_top, rad_bytes);
592 reallocDevice(d_radiation_out_bottom, rad_bytes);
593 reallocDevice(d_scatter_buff_top, rad_bytes);
594 reallocDevice(d_scatter_buff_bottom, rad_bytes);
595
596 current_band_count = Nbands;
597 current_source_count = materials.num_sources;
598 current_camera_count = materials.num_cameras;
599
600 // Update h_params
601 h_params.rho = reinterpret_cast<float *>(d_rho);
602 h_params.tau = reinterpret_cast<float *>(d_tau);
603 h_params.rho_cam = reinterpret_cast<float *>(d_rho_cam);
604 h_params.tau_cam = reinterpret_cast<float *>(d_tau_cam);
605 h_params.specular_exponent = reinterpret_cast<float *>(d_specular_exponent);
606 h_params.specular_scale = reinterpret_cast<float *>(d_specular_scale);
607 h_params.glass_n = reinterpret_cast<float *>(d_glass_n);
608 h_params.glass_KL = reinterpret_cast<float *>(d_glass_KL);
609 h_params.is_glass = reinterpret_cast<int8_t *>(d_is_glass);
610 h_params.radiation_in = reinterpret_cast<float *>(d_radiation_in);
611 h_params.radiation_out_top = reinterpret_cast<float *>(d_radiation_out_top);
612 h_params.radiation_out_bottom = reinterpret_cast<float *>(d_radiation_out_bottom);
613 h_params.scatter_buff_top = reinterpret_cast<float *>(d_scatter_buff_top);
614 h_params.scatter_buff_bottom = reinterpret_cast<float *>(d_scatter_buff_bottom);
615 h_params.Nsources = static_cast<uint32_t>(materials.num_sources);
616 h_params.Ncameras = static_cast<uint32_t>(materials.num_cameras);
617 h_params.Nbands_global = static_cast<uint32_t>(Nbands);
618}
619
620// ---------------------------------------------------------------------------
621// Sources
622// ---------------------------------------------------------------------------
623
624void OptiX8Backend::updateSources(const std::vector<RayTracingSource> &sources) {
625 auto freePtr = [this](CUdeviceptr &ptr) { freeCUdeviceptr(ptr); };
626 freePtr(d_source_positions);
627 freePtr(d_source_rotations);
628 freePtr(d_source_widths);
629 freePtr(d_source_types);
630 // d_source_fluxes is managed by uploadSourceFluxes()
631
632 const size_t Nsources = sources.size();
633 if (Nsources == 0) {
634 current_source_count = 0;
635 h_params.Nsources = 0;
636 return;
637 }
638
639 std::vector<float3> positions(Nsources);
640 std::vector<float3> rotations(Nsources);
641 std::vector<float2> widths(Nsources);
642 std::vector<uint32_t> types(Nsources);
643
644 for (size_t i = 0; i < Nsources; i++) {
645 positions[i] = make_float3(sources[i].position.x, sources[i].position.y, sources[i].position.z);
646 rotations[i] = make_float3(sources[i].rotation.x, sources[i].rotation.y, sources[i].rotation.z);
647 widths[i] = make_float2(sources[i].width.x, sources[i].width.y);
648 types[i] = sources[i].type;
649 }
650
651 auto upload = [this](CUdeviceptr &d_ptr, const void *src, size_t bytes) {
652 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_ptr), bytes));
653 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_ptr), src, bytes, cudaMemcpyHostToDevice));
654 };
655
656 upload(d_source_positions, positions.data(), Nsources * sizeof(float3));
657 upload(d_source_rotations, rotations.data(), Nsources * sizeof(float3));
658 upload(d_source_widths, widths.data(), Nsources * sizeof(float2));
659 upload(d_source_types, types.data(), Nsources * sizeof(uint32_t));
660
661 // Upload camera-weighted source fluxes (full 3D buffer [source][band][camera])
662 // This is needed during direct ray tracing for specular accumulation in __miss__direct.
663 // Layout matches OptiX 6: flattened as [src0_band0_cam0, src0_band0_cam1, ..., src0_band1_cam0, ...]
664 freeCUdeviceptr(d_source_fluxes_cam);
665 std::vector<float> fluxes_cam;
666 for (size_t i = 0; i < Nsources; i++) {
667 for (float f : sources[i].fluxes_cam) {
668 fluxes_cam.push_back(f);
669 }
670 }
671 if (!fluxes_cam.empty()) {
672 upload(d_source_fluxes_cam, fluxes_cam.data(), fluxes_cam.size() * sizeof(float));
673 h_params.source_fluxes_cam = reinterpret_cast<float *>(d_source_fluxes_cam);
674 }
675
676 current_source_count = Nsources;
677
678 h_params.Nsources = static_cast<uint32_t>(Nsources);
679 h_params.source_positions = reinterpret_cast<float3 *>(d_source_positions);
680 h_params.source_rotations = reinterpret_cast<float3 *>(d_source_rotations);
681 h_params.source_widths = reinterpret_cast<float2 *>(d_source_widths);
682 h_params.source_types = reinterpret_cast<uint32_t *>(d_source_types);
683}
684
685// ---------------------------------------------------------------------------
686// Diffuse / sky
687// ---------------------------------------------------------------------------
688
689void OptiX8Backend::updateDiffuseRadiation(const std::vector<float> &flux, const std::vector<float> &extinction,
690 const std::vector<helios::vec3> &peak_dir,
691 const std::vector<float> &dist_norm,
692 const std::vector<float> &sky_energy) {
693 freeCUdeviceptr(d_diffuse_flux);
694 freeCUdeviceptr(d_diffuse_extinction);
695 freeCUdeviceptr(d_diffuse_peak_dir);
696 freeCUdeviceptr(d_diffuse_dist_norm);
697 freeCUdeviceptr(d_Rsky);
698
699 auto upload_f = [this](CUdeviceptr &ptr, const std::vector<float> &v) {
700 if (!v.empty()) {
701 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&ptr), v.size() * sizeof(float)));
702 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(ptr), v.data(),
703 v.size() * sizeof(float), cudaMemcpyHostToDevice));
704 }
705 };
706 upload_f(d_diffuse_flux, flux);
707 upload_f(d_diffuse_extinction, extinction);
708 upload_f(d_diffuse_dist_norm, dist_norm);
709 upload_f(d_Rsky, sky_energy);
710
711 if (!peak_dir.empty()) {
712 std::vector<float3> pd_f3(peak_dir.size());
713 for (size_t i = 0; i < peak_dir.size(); i++) {
714 pd_f3[i] = make_float3(peak_dir[i].x, peak_dir[i].y, peak_dir[i].z);
715 }
716 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_diffuse_peak_dir),
717 pd_f3.size() * sizeof(float3)));
718 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_diffuse_peak_dir), pd_f3.data(),
719 pd_f3.size() * sizeof(float3), cudaMemcpyHostToDevice));
720 }
721
722 h_params.diffuse_flux = reinterpret_cast<float *>(d_diffuse_flux);
723 h_params.diffuse_extinction = reinterpret_cast<float *>(d_diffuse_extinction);
724 h_params.diffuse_peak_dir = reinterpret_cast<float3 *>(d_diffuse_peak_dir);
725 h_params.diffuse_dist_norm = reinterpret_cast<float *>(d_diffuse_dist_norm);
726 h_params.Rsky = reinterpret_cast<float *>(d_Rsky);
727}
728
729void OptiX8Backend::updateSkyModel(const std::vector<helios::vec4> &sky_radiance_params,
730 const std::vector<float> &camera_sky_radiance,
731 const helios::vec3 &sun_direction,
732 const std::vector<float> &solar_disk_radiance,
733 float solar_disk_cos_angle,
734 const std::vector<float> &camera_diffuse_flux,
735 const std::vector<uint32_t> &band_emission_flag) {
736 // Upload sky_radiance_params (helios::vec4 → float4)
737 freeCUdeviceptr(d_sky_radiance_params);
738 if (!sky_radiance_params.empty()) {
739 std::vector<float4> f4(sky_radiance_params.size());
740 for (size_t i = 0; i < sky_radiance_params.size(); i++) {
741 f4[i] = make_float4(sky_radiance_params[i].x, sky_radiance_params[i].y,
742 sky_radiance_params[i].z, sky_radiance_params[i].w);
743 }
744 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_sky_radiance_params), f4.size() * sizeof(float4)));
745 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_sky_radiance_params), f4.data(),
746 f4.size() * sizeof(float4), cudaMemcpyHostToDevice));
747 }
748
749 freeCUdeviceptr(d_camera_sky_radiance);
750 if (!camera_sky_radiance.empty()) {
751 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_camera_sky_radiance),
752 camera_sky_radiance.size() * sizeof(float)));
753 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_camera_sky_radiance), camera_sky_radiance.data(),
754 camera_sky_radiance.size() * sizeof(float), cudaMemcpyHostToDevice));
755 }
756
757 freeCUdeviceptr(d_solar_disk_radiance);
758 if (!solar_disk_radiance.empty()) {
759 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_solar_disk_radiance),
760 solar_disk_radiance.size() * sizeof(float)));
761 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_solar_disk_radiance), solar_disk_radiance.data(),
762 solar_disk_radiance.size() * sizeof(float), cudaMemcpyHostToDevice));
763 }
764
765 // camera_diffuse_flux: per-band hemispherical sky flux sampled by cameras on a miss when
766 // band_emission_flag[b] is set. We keep this in a dedicated buffer (instead of reusing
767 // h_params.diffuse_flux) because launchCameraRays does not refresh diffuse_flux, so the
768 // global buffer can be stale or null in camera-only dispatches.
769 freeCUdeviceptr(d_camera_diffuse_flux);
770 if (!camera_diffuse_flux.empty()) {
771 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_camera_diffuse_flux),
772 camera_diffuse_flux.size() * sizeof(float)));
773 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_camera_diffuse_flux), camera_diffuse_flux.data(),
774 camera_diffuse_flux.size() * sizeof(float), cudaMemcpyHostToDevice));
775 }
776
777 freeCUdeviceptr(d_band_emission_flag);
778 if (!band_emission_flag.empty()) {
779 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_band_emission_flag),
780 band_emission_flag.size() * sizeof(uint32_t)));
781 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_band_emission_flag), band_emission_flag.data(),
782 band_emission_flag.size() * sizeof(uint32_t), cudaMemcpyHostToDevice));
783 }
784
785 h_params.sky_radiance_params = reinterpret_cast<float4 *>(d_sky_radiance_params);
786 h_params.camera_sky_radiance = reinterpret_cast<float *>(d_camera_sky_radiance);
787 h_params.solar_disk_radiance = reinterpret_cast<float *>(d_solar_disk_radiance);
788 h_params.camera_diffuse_flux = reinterpret_cast<float *>(d_camera_diffuse_flux);
789 h_params.band_emission_flag = reinterpret_cast<uint32_t *>(d_band_emission_flag);
790 h_params.sun_direction = make_float3(sun_direction.x, sun_direction.y, sun_direction.z);
791 h_params.solar_disk_cos_angle = solar_disk_cos_angle;
792}
793
794// ---------------------------------------------------------------------------
795// Ray launching
796// ---------------------------------------------------------------------------
797
799 if (!is_initialized) {
800 helios_runtime_error("ERROR (OptiX8Backend::launchDirectRays): Backend not initialized.");
801 }
802 if (gas_handle == 0) {
803 helios_runtime_error("ERROR (OptiX8Backend::launchDirectRays): No acceleration structure. Call buildAccelerationStructure() first.");
804 }
805
806 applyLaunchParams(launch_params);
807
808 // Upload band_launch_flag (vector<bool> → device bool array)
809 if (d_band_launch_flag) { freeCUdeviceptr(d_band_launch_flag); }
810 const size_t Nbands_g = launch_params.band_launch_flag.size();
811 if (Nbands_g > 0) {
812 std::vector<uint8_t> flags_u8(Nbands_g);
813 for (size_t i = 0; i < Nbands_g; i++) {
814 flags_u8[i] = launch_params.band_launch_flag[i] ? 1u : 0u;
815 }
816 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_band_launch_flag), Nbands_g * sizeof(uint8_t)));
817 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_band_launch_flag), flags_u8.data(),
818 Nbands_g * sizeof(uint8_t), cudaMemcpyHostToDevice));
819 }
820 h_params.band_launch_flag = reinterpret_cast<bool *>(d_band_launch_flag);
821 h_params.traversable = gas_handle;
822
823 // 2D stratification: split rays_per_primitive into dim_x × dim_y grid (same as diffuse)
824 // rays_per_primitive is always n*n (RadiationModel sets it as ceil(sqrt(N))^2)
825 const uint32_t launch_count = launch_params.launch_count;
826 const uint32_t rpp = launch_params.rays_per_primitive;
827 const uint32_t dim_x = static_cast<uint32_t>(sqrtf(static_cast<float>(rpp)));
828 const uint32_t dim_y = (dim_x > 0) ? (rpp / dim_x) : 1u;
829 h_params.launch_dim_x = dim_x;
830 h_params.launch_dim_y = dim_y;
831
832 // prd_pool is unused (PRD allocated on thread stack in raygen)
833 h_params.prd_pool = nullptr;
834
835 // Direct launch uses raygen record 0 (d_raygen_records + 0)
836 OptixShaderBindingTable direct_sbt = sbt;
837 direct_sbt.raygenRecord = d_raygen_records;
838
839 // OptiX depth dimension is limited to 65535; batch if needed
840 const uint32_t MAX_DEPTH = 65535u;
841 uint32_t offset = launch_params.launch_offset;
842 uint32_t remaining = launch_count;
843 while (remaining > 0) {
844 const uint32_t batch = std::min(remaining, MAX_DEPTH);
845 h_params.launch_offset = offset;
846 h_params.launch_count = batch;
847 uploadLaunchParams();
848 OPTIX_CHECK(optixLaunch(optix_pipeline, cuda_stream, d_params,
849 sizeof(OptiX8LaunchParams), &direct_sbt,
850 dim_x, dim_y, batch));
851 CUDA_CHECK(cudaStreamSynchronize(cuda_stream));
852 offset += batch;
853 remaining -= batch;
854 }
855}
856
858 if (!is_initialized) {
859 helios_runtime_error("ERROR (OptiX8Backend::launchDiffuseRays): Backend not initialized.");
860 }
861 if (gas_handle == 0) {
862 helios_runtime_error("ERROR (OptiX8Backend::launchDiffuseRays): No acceleration structure. "
863 "Call buildAccelerationStructure() first.");
864 }
865
866 applyLaunchParams(launch_params);
867
868 // Upload radiation_out buffers (emission + scattered energy from previous iteration).
869 // This must happen here because RadiationModel adds emission to flux_top/bottom AFTER
870 // calling uploadRadiationOut() for the direct-ray scatter, so the params always carry
871 // the most up-to-date radiation_out data.
872 if (!launch_params.radiation_out_top.empty() && d_radiation_out_top) {
873 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_radiation_out_top),
874 launch_params.radiation_out_top.data(),
875 launch_params.radiation_out_top.size() * sizeof(float),
876 cudaMemcpyHostToDevice));
877 }
878 if (!launch_params.radiation_out_bottom.empty() && d_radiation_out_bottom) {
879 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_radiation_out_bottom),
880 launch_params.radiation_out_bottom.data(),
881 launch_params.radiation_out_bottom.size() * sizeof(float),
882 cudaMemcpyHostToDevice));
883 }
884
885 // Upload band_launch_flag
886 if (d_band_launch_flag) { freeCUdeviceptr(d_band_launch_flag); }
887 const size_t Nbands_g = launch_params.band_launch_flag.size();
888 if (Nbands_g > 0) {
889 std::vector<uint8_t> flags_u8(Nbands_g);
890 for (size_t i = 0; i < Nbands_g; i++) {
891 flags_u8[i] = launch_params.band_launch_flag[i] ? 1u : 0u;
892 }
893 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_band_launch_flag),
894 Nbands_g * sizeof(uint8_t)));
895 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_band_launch_flag), flags_u8.data(),
896 Nbands_g * sizeof(uint8_t), cudaMemcpyHostToDevice));
897 }
898 h_params.band_launch_flag = reinterpret_cast<bool *>(d_band_launch_flag);
899 h_params.traversable = gas_handle;
900
901 // Upload diffuse params from launch_params (re-upload each launch since params may vary)
902 freeCUdeviceptr(d_diffuse_flux);
903 freeCUdeviceptr(d_diffuse_extinction);
904 freeCUdeviceptr(d_diffuse_peak_dir);
905 freeCUdeviceptr(d_diffuse_dist_norm);
906 freeCUdeviceptr(d_sky_radiance_params);
907
908 auto upload_f = [this](CUdeviceptr &ptr, const std::vector<float> &v) {
909 if (!v.empty()) {
910 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&ptr), v.size() * sizeof(float)));
911 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(ptr), v.data(),
912 v.size() * sizeof(float), cudaMemcpyHostToDevice));
913 }
914 };
915 upload_f(d_diffuse_flux, launch_params.diffuse_flux);
916 upload_f(d_diffuse_extinction, launch_params.diffuse_extinction);
917 upload_f(d_diffuse_dist_norm, launch_params.diffuse_dist_norm);
918
919 if (!launch_params.diffuse_peak_dir.empty()) {
920 std::vector<float3> pd_f3(launch_params.diffuse_peak_dir.size());
921 for (size_t i = 0; i < launch_params.diffuse_peak_dir.size(); i++) {
922 pd_f3[i] = make_float3(launch_params.diffuse_peak_dir[i].x,
923 launch_params.diffuse_peak_dir[i].y,
924 launch_params.diffuse_peak_dir[i].z);
925 }
926 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_diffuse_peak_dir),
927 pd_f3.size() * sizeof(float3)));
928 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_diffuse_peak_dir), pd_f3.data(),
929 pd_f3.size() * sizeof(float3), cudaMemcpyHostToDevice));
930 }
931
932 if (!launch_params.sky_radiance_params.empty()) {
933 std::vector<float4> sky_f4(launch_params.sky_radiance_params.size());
934 for (size_t i = 0; i < launch_params.sky_radiance_params.size(); i++) {
935 sky_f4[i] = make_float4(launch_params.sky_radiance_params[i].x,
936 launch_params.sky_radiance_params[i].y,
937 launch_params.sky_radiance_params[i].z,
938 launch_params.sky_radiance_params[i].w);
939 }
940 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_sky_radiance_params),
941 sky_f4.size() * sizeof(float4)));
942 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_sky_radiance_params), sky_f4.data(),
943 sky_f4.size() * sizeof(float4), cudaMemcpyHostToDevice));
944 }
945
946 h_params.diffuse_flux = reinterpret_cast<float *>(d_diffuse_flux);
947 h_params.diffuse_extinction = reinterpret_cast<float *>(d_diffuse_extinction);
948 h_params.diffuse_peak_dir = reinterpret_cast<float3 *>(d_diffuse_peak_dir);
949 h_params.diffuse_dist_norm = reinterpret_cast<float *>(d_diffuse_dist_norm);
950 h_params.sky_radiance_params = reinterpret_cast<float4 *>(d_sky_radiance_params);
951
952 // Early return when there are no rays to launch (e.g. diffuseRayCount=0 during scattering)
953 if (launch_params.rays_per_primitive == 0 || launch_params.launch_count == 0) {
954 return;
955 }
956
957 // Set up 2D stratification launch dimensions
958 // rays_per_primitive is always n*n (RadiationModel sets it as ceil(sqrt(N))^2)
959 const uint32_t rpp = launch_params.rays_per_primitive;
960 const uint32_t dim_x = static_cast<uint32_t>(sqrtf(static_cast<float>(rpp)));
961 const uint32_t dim_y = (dim_x > 0) ? (rpp / dim_x) : 1u;
962 h_params.launch_dim_x = dim_x;
963 h_params.launch_dim_y = dim_y;
964 h_params.prd_pool = nullptr;
965
966 // Use diffuse raygen record (index 1 in the raygen records array)
967 OptixShaderBindingTable diffuse_sbt = sbt;
968 diffuse_sbt.raygenRecord = d_raygen_record_diffuse;
969
970 // OptiX depth dimension is limited to 65535; batch if needed
971 const uint32_t MAX_DEPTH = 65535u;
972 const uint32_t launch_count = launch_params.launch_count;
973 uint32_t offset = launch_params.launch_offset;
974 uint32_t remaining = launch_count;
975 while (remaining > 0) {
976 const uint32_t batch = std::min(remaining, MAX_DEPTH);
977 h_params.launch_offset = offset;
978 h_params.launch_count = batch;
979 uploadLaunchParams();
980 OPTIX_CHECK(optixLaunch(optix_pipeline, cuda_stream, d_params,
981 sizeof(OptiX8LaunchParams), &diffuse_sbt,
982 dim_x, dim_y, batch));
983 CUDA_CHECK(cudaStreamSynchronize(cuda_stream));
984 offset += batch;
985 remaining -= batch;
986 }
987}
988
990 if (!is_initialized) {
991 helios_runtime_error("ERROR (OptiX8Backend::launchCameraRays): Backend not initialized.");
992 }
993 if (gas_handle == 0) {
994 helios_runtime_error("ERROR (OptiX8Backend::launchCameraRays): No acceleration structure. "
995 "Call buildAccelerationStructure() first.");
996 }
997
998 applyLaunchParams(launch_params);
999
1000 const uint32_t tile_w = launch_params.camera_resolution.x;
1001 const uint32_t tile_h = launch_params.camera_resolution.y;
1002 const uint32_t full_w = launch_params.camera_resolution_full.x;
1003 const uint32_t full_h = launch_params.camera_resolution_full.y;
1004 const uint32_t anti_samples = launch_params.antialiasing_samples;
1005 const uint32_t Nbands_l = launch_params.num_bands_launch;
1006 const uint32_t cam_id = launch_params.camera_id;
1007
1008 // Allocate/zero radiation_in_camera when starting a new camera or when band count changes.
1009 // Multiple tiles for the same camera accumulate into the same buffer without re-zeroing.
1010 const size_t Npixels = (size_t)full_w * full_h;
1011 const size_t cam_bytes = Npixels * Nbands_l * sizeof(float);
1012 if (cam_id != current_camera_launch_id || current_launch_band_count != Nbands_l) {
1013 reallocDevice(d_radiation_in_camera, cam_bytes);
1014 CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_radiation_in_camera), 0, cam_bytes));
1015 // Free pixel label/depth buffers from previous camera so getCameraResults skips them
1016 // until zeroCameraPixelBuffers() is called for the new camera.
1017 freeCUdeviceptr(d_camera_pixel_label);
1018 freeCUdeviceptr(d_camera_pixel_depth);
1019 h_params.camera_pixel_label = nullptr;
1020 h_params.camera_pixel_depth = nullptr;
1021 current_camera_launch_id = cam_id;
1022 current_launch_band_count = Nbands_l;
1023 }
1024 h_params.radiation_in_camera = reinterpret_cast<float *>(d_radiation_in_camera);
1025
1026 // Upload band_launch_flag
1027 if (d_band_launch_flag) { freeCUdeviceptr(d_band_launch_flag); }
1028 const size_t Nbands_g = launch_params.band_launch_flag.size();
1029 if (Nbands_g > 0) {
1030 std::vector<uint8_t> flags_u8(Nbands_g);
1031 for (size_t i = 0; i < Nbands_g; i++) {
1032 flags_u8[i] = launch_params.band_launch_flag[i] ? 1u : 0u;
1033 }
1034 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_band_launch_flag), Nbands_g * sizeof(uint8_t)));
1035 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_band_launch_flag), flags_u8.data(),
1036 Nbands_g * sizeof(uint8_t), cudaMemcpyHostToDevice));
1037 }
1038 h_params.band_launch_flag = reinterpret_cast<bool *>(d_band_launch_flag);
1039 h_params.traversable = gas_handle;
1040
1041 // Camera launch: x=antialiasing_samples, y=tile_width, z=tile_height
1042 h_params.launch_dim_x = anti_samples;
1043 h_params.launch_dim_y = tile_w;
1044
1045 uploadLaunchParams();
1046
1047 OptixShaderBindingTable camera_sbt = sbt;
1048 camera_sbt.raygenRecord = d_raygen_record_camera;
1049
1050 OPTIX_CHECK(optixLaunch(optix_pipeline, cuda_stream, d_params,
1051 sizeof(OptiX8LaunchParams), &camera_sbt,
1052 anti_samples, tile_w, tile_h));
1053 CUDA_CHECK(cudaStreamSynchronize(cuda_stream));
1054}
1055
1057 if (!is_initialized) {
1058 helios_runtime_error("ERROR (OptiX8Backend::launchPixelLabelRays): Backend not initialized.");
1059 }
1060 if (gas_handle == 0) {
1061 helios_runtime_error("ERROR (OptiX8Backend::launchPixelLabelRays): No acceleration structure. "
1062 "Call buildAccelerationStructure() first.");
1063 }
1064
1065 applyLaunchParams(launch_params);
1066
1067 const uint32_t tile_w = launch_params.camera_resolution.x;
1068 const uint32_t tile_h = launch_params.camera_resolution.y;
1069
1070 // Upload band_launch_flag
1071 if (d_band_launch_flag) { freeCUdeviceptr(d_band_launch_flag); }
1072 const size_t Nbands_g = launch_params.band_launch_flag.size();
1073 if (Nbands_g > 0) {
1074 std::vector<uint8_t> flags_u8(Nbands_g);
1075 for (size_t i = 0; i < Nbands_g; i++) {
1076 flags_u8[i] = launch_params.band_launch_flag[i] ? 1u : 0u;
1077 }
1078 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_band_launch_flag), Nbands_g * sizeof(uint8_t)));
1079 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_band_launch_flag), flags_u8.data(),
1080 Nbands_g * sizeof(uint8_t), cudaMemcpyHostToDevice));
1081 }
1082 h_params.band_launch_flag = reinterpret_cast<bool *>(d_band_launch_flag);
1083 h_params.traversable = gas_handle;
1084
1085 // Pixel label launch: 1 ray per pixel center, no antialiasing
1086 h_params.launch_dim_x = 1u;
1087 h_params.launch_dim_y = tile_w;
1088
1089 uploadLaunchParams();
1090
1091 OptixShaderBindingTable pixel_label_sbt = sbt;
1092 pixel_label_sbt.raygenRecord = d_raygen_record_pixel_label;
1093
1094 OPTIX_CHECK(optixLaunch(optix_pipeline, cuda_stream, d_params,
1095 sizeof(OptiX8LaunchParams), &pixel_label_sbt,
1096 1u, tile_w, tile_h));
1097 CUDA_CHECK(cudaStreamSynchronize(cuda_stream));
1098}
1099
1100// ---------------------------------------------------------------------------
1101// Results retrieval
1102// ---------------------------------------------------------------------------
1103
1105 const size_t Nprims = current_primitive_count;
1106 const size_t Nbands = current_band_count;
1107 const size_t total = Nprims * Nbands;
1108
1109 results.num_primitives = Nprims;
1110 results.num_bands = Nbands;
1111 results.num_sources = current_source_count;
1112 results.num_cameras = current_camera_count;
1113
1114 if (total > 0) {
1115 if (d_radiation_in) results.radiation_in = downloadFloat(d_radiation_in, total);
1116 if (d_radiation_out_top) results.radiation_out_top = downloadFloat(d_radiation_out_top, total);
1117 if (d_radiation_out_bottom) results.radiation_out_bottom = downloadFloat(d_radiation_out_bottom, total);
1118 if (d_scatter_buff_top) results.scatter_buff_top = downloadFloat(d_scatter_buff_top, total);
1119 if (d_scatter_buff_bottom) results.scatter_buff_bottom = downloadFloat(d_scatter_buff_bottom, total);
1120 }
1121
1122 // Camera scatter buffers: sized Nprims × Nbands_launch (may differ from Nbands_global)
1123 if (Nprims > 0 && current_launch_band_count > 0) {
1124 const size_t cam_total = Nprims * current_launch_band_count;
1125 if (d_scatter_buff_top_cam) results.scatter_buff_top_cam = downloadFloat(d_scatter_buff_top_cam, cam_total);
1126 if (d_scatter_buff_bottom_cam) results.scatter_buff_bottom_cam = downloadFloat(d_scatter_buff_bottom_cam, cam_total);
1127 }
1128}
1129
1130void OptiX8Backend::getCameraResults(std::vector<float> &pixel_data, std::vector<uint> &pixel_labels,
1131 std::vector<float> &pixel_depths, uint camera_id,
1132 const helios::int2 &resolution) {
1133 const size_t Npixels = (size_t)resolution.x * resolution.y;
1134
1135 if (d_radiation_in_camera && Npixels > 0 && current_launch_band_count > 0) {
1136 pixel_data = downloadFloat(d_radiation_in_camera, Npixels * current_launch_band_count);
1137 }
1138
1139 if (d_camera_pixel_label && Npixels > 0) {
1140 auto labels_u32 = downloadUInt32(d_camera_pixel_label, Npixels);
1141 pixel_labels.assign(labels_u32.begin(), labels_u32.end());
1142 }
1143
1144 if (d_camera_pixel_depth && Npixels > 0) {
1145 pixel_depths = downloadFloat(d_camera_pixel_depth, Npixels);
1146 }
1147}
1148
1149// ---------------------------------------------------------------------------
1150// Buffer management utilities
1151// ---------------------------------------------------------------------------
1152
1153void OptiX8Backend::zeroRadiationBuffers(size_t launch_band_count) {
1154 const size_t Nprims = current_primitive_count;
1155 if (Nprims == 0 || launch_band_count == 0) return;
1156
1157 if (launch_band_count > current_band_count) {
1158 helios_runtime_error("ERROR (OptiX8Backend::zeroRadiationBuffers): launch_band_count (" +
1159 std::to_string(launch_band_count) + ") exceeds current_band_count (" +
1160 std::to_string(current_band_count) + "). Call updateMaterials() first.");
1161 }
1162
1163 // Reset camera launch ID so camera pixel buffers get re-zeroed in launchCameraRays()
1164 current_camera_launch_id = 0xFFFFFFFFu;
1165
1166 const size_t bytes = Nprims * launch_band_count * sizeof(float);
1167
1168 if (d_radiation_in) CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_radiation_in), 0, bytes));
1169 if (d_radiation_out_top) CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_radiation_out_top), 0, bytes));
1170 if (d_radiation_out_bottom) CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_radiation_out_bottom), 0, bytes));
1171 if (d_scatter_buff_top) CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_scatter_buff_top), 0, bytes));
1172 if (d_scatter_buff_bottom) CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_scatter_buff_bottom), 0, bytes));
1173
1174 // Zero specular buffer: [source × camera × primitive × launch_band_count]
1175 const size_t specular_size = current_source_count * current_camera_count * Nprims * launch_band_count;
1176 if (specular_size > 0) {
1177 const size_t specular_bytes = specular_size * sizeof(float);
1178 reallocDevice(d_radiation_specular, specular_bytes);
1179 CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_radiation_specular), 0, specular_bytes));
1180 h_params.radiation_specular = reinterpret_cast<float *>(d_radiation_specular);
1181 }
1182}
1183
1185 const size_t total_bytes = current_primitive_count * current_band_count * sizeof(float);
1186 if (total_bytes == 0) return;
1187
1188 if (d_scatter_buff_top) CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_scatter_buff_top), 0, total_bytes));
1189 if (d_scatter_buff_bottom) CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_scatter_buff_bottom), 0, total_bytes));
1190}
1191
1193 const size_t Npixels = (size_t)resolution.x * resolution.y;
1194 if (Npixels == 0) return;
1195
1196 reallocDevice(d_camera_pixel_label, Npixels * sizeof(uint32_t));
1197 reallocDevice(d_camera_pixel_depth, Npixels * sizeof(float));
1198 CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_camera_pixel_label), 0, Npixels * sizeof(uint32_t)));
1199 CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_camera_pixel_depth), 0, Npixels * sizeof(float)));
1200
1201 h_params.camera_pixel_label = reinterpret_cast<uint32_t *>(d_camera_pixel_label);
1202 h_params.camera_pixel_depth = reinterpret_cast<float *>(d_camera_pixel_depth);
1203}
1204
1206 const size_t total = current_primitive_count * current_band_count * sizeof(float);
1207 if (total == 0) return;
1208 if (d_scatter_buff_top && d_radiation_out_top) {
1209 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_radiation_out_top),
1210 reinterpret_cast<const void *>(d_scatter_buff_top),
1211 total, cudaMemcpyDeviceToDevice));
1212 }
1213 if (d_scatter_buff_bottom && d_radiation_out_bottom) {
1214 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_radiation_out_bottom),
1215 reinterpret_cast<const void *>(d_scatter_buff_bottom),
1216 total, cudaMemcpyDeviceToDevice));
1217 }
1218}
1219
1220void OptiX8Backend::uploadRadiationOut(const std::vector<float> &radiation_out_top,
1221 const std::vector<float> &radiation_out_bottom) {
1222 if (!radiation_out_top.empty() && d_radiation_out_top) {
1223 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_radiation_out_top),
1224 radiation_out_top.data(),
1225 radiation_out_top.size() * sizeof(float),
1226 cudaMemcpyHostToDevice));
1227 }
1228 if (!radiation_out_bottom.empty() && d_radiation_out_bottom) {
1229 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_radiation_out_bottom),
1230 radiation_out_bottom.data(),
1231 radiation_out_bottom.size() * sizeof(float),
1232 cudaMemcpyHostToDevice));
1233 }
1234}
1235
1236void OptiX8Backend::uploadCameraScatterBuffers(const std::vector<float> &scatter_top_cam,
1237 const std::vector<float> &scatter_bottom_cam) {
1238 if (!scatter_top_cam.empty() && d_scatter_buff_top_cam) {
1239 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_scatter_buff_top_cam),
1240 scatter_top_cam.data(),
1241 scatter_top_cam.size() * sizeof(float),
1242 cudaMemcpyHostToDevice));
1243 }
1244 if (!scatter_bottom_cam.empty() && d_scatter_buff_bottom_cam) {
1245 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_scatter_buff_bottom_cam),
1246 scatter_bottom_cam.data(),
1247 scatter_bottom_cam.size() * sizeof(float),
1248 cudaMemcpyHostToDevice));
1249 }
1250}
1251
1252void OptiX8Backend::zeroCameraScatterBuffers(size_t launch_band_count) {
1253 const size_t Nprims = current_primitive_count;
1254 if (Nprims == 0 || launch_band_count == 0) return;
1255
1256 const size_t bytes = Nprims * launch_band_count * sizeof(float);
1257 reallocDevice(d_scatter_buff_top_cam, bytes);
1258 reallocDevice(d_scatter_buff_bottom_cam, bytes);
1259 CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_scatter_buff_top_cam), 0, bytes));
1260 CUDA_CHECK(cudaMemset(reinterpret_cast<void *>(d_scatter_buff_bottom_cam), 0, bytes));
1261
1262 h_params.scatter_buff_top_cam = reinterpret_cast<float *>(d_scatter_buff_top_cam);
1263 h_params.scatter_buff_bottom_cam = reinterpret_cast<float *>(d_scatter_buff_bottom_cam);
1264 current_launch_band_count = launch_band_count;
1265}
1266
1267void OptiX8Backend::uploadSourceFluxes(const std::vector<float> &fluxes) {
1268 freeCUdeviceptr(d_source_fluxes);
1269 if (!fluxes.empty()) {
1270 const size_t bytes = fluxes.size() * sizeof(float);
1271 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_source_fluxes), bytes));
1272 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_source_fluxes), fluxes.data(), bytes, cudaMemcpyHostToDevice));
1273 }
1274 // Assign unconditionally so an empty upload yields nullptr (not a dangling freed pointer that
1275 // would defeat the device-side null guards).
1276 h_params.source_fluxes = reinterpret_cast<float *>(d_source_fluxes);
1277}
1278
1279void OptiX8Backend::uploadSourceFluxesCam(const std::vector<float> &fluxes_cam) {
1280 freeCUdeviceptr(d_source_fluxes_cam);
1281 if (!fluxes_cam.empty()) {
1282 const size_t bytes = fluxes_cam.size() * sizeof(float);
1283 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_source_fluxes_cam), bytes));
1284 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_source_fluxes_cam), fluxes_cam.data(), bytes,
1285 cudaMemcpyHostToDevice));
1286 }
1287 // Assign unconditionally so an empty upload yields nullptr (not a dangling freed pointer).
1288 h_params.source_fluxes_cam = reinterpret_cast<float *>(d_source_fluxes_cam);
1289}
1290
1291// ---------------------------------------------------------------------------
1292// Diagnostics
1293// ---------------------------------------------------------------------------
1294
1296 size_t free_bytes = 0;
1297 size_t total_bytes = 0;
1298 CUDA_CHECK(cudaMemGetInfo(&free_bytes, &total_bytes));
1299 const float free_mb = static_cast<float>(free_bytes) / (1024.0f * 1024.0f);
1300 const float total_mb = static_cast<float>(total_bytes) / (1024.0f * 1024.0f);
1301 std::cout << "GPU memory: " << free_mb << " MB free / " << total_mb << " MB total" << std::endl;
1302}
1303
1304// ---------------------------------------------------------------------------
1305// Private helpers
1306// ---------------------------------------------------------------------------
1307
1308void OptiX8Backend::freeCUdeviceptr(CUdeviceptr &ptr) {
1309 if (ptr) {
1310 cudaFree(reinterpret_cast<void *>(ptr));
1311 ptr = 0;
1312 }
1313}
1314
1315void OptiX8Backend::freeGeometryBuffers() {
1316 freeCUdeviceptr(d_transform_matrix);
1317 freeCUdeviceptr(d_primitive_type);
1318 freeCUdeviceptr(d_primitive_positions);
1319 freeCUdeviceptr(d_primitive_uuid_arr);
1320 freeCUdeviceptr(d_primitiveID);
1321 freeCUdeviceptr(d_objectID);
1322 freeCUdeviceptr(d_object_subdivisions);
1323 freeCUdeviceptr(d_twosided_flag);
1324 freeCUdeviceptr(d_primitive_solid_fraction);
1325 freeCUdeviceptr(d_patch_vertices);
1326 freeCUdeviceptr(d_patch_UUIDs);
1327 freeCUdeviceptr(d_triangle_vertices);
1328 freeCUdeviceptr(d_triangle_UUIDs);
1329 freeCUdeviceptr(d_disk_centers);
1330 freeCUdeviceptr(d_disk_radii);
1331 freeCUdeviceptr(d_disk_normals);
1332 freeCUdeviceptr(d_disk_UUIDs);
1333 freeCUdeviceptr(d_tile_vertices);
1334 freeCUdeviceptr(d_tile_UUIDs);
1335 freeCUdeviceptr(d_voxel_vertices);
1336 freeCUdeviceptr(d_voxel_UUIDs);
1337 freeCUdeviceptr(d_bbox_vertices);
1338 freeCUdeviceptr(d_bbox_UUIDs);
1339 freeCUdeviceptr(d_mask_data);
1340 freeCUdeviceptr(d_mask_offsets);
1341 freeCUdeviceptr(d_mask_sizes);
1342 freeCUdeviceptr(d_mask_IDs);
1343 freeCUdeviceptr(d_uv_data);
1344 freeCUdeviceptr(d_uv_IDs);
1345 freeCUdeviceptr(d_aabbs);
1346 freeCUdeviceptr(d_gas_output);
1347 gas_handle = 0;
1348}
1349
1350void OptiX8Backend::freeMaterialBuffers() {
1351 freeCUdeviceptr(d_rho);
1352 freeCUdeviceptr(d_tau);
1353 freeCUdeviceptr(d_rho_cam);
1354 freeCUdeviceptr(d_tau_cam);
1355 freeCUdeviceptr(d_specular_exponent);
1356 freeCUdeviceptr(d_specular_scale);
1357 freeCUdeviceptr(d_glass_n);
1358 freeCUdeviceptr(d_glass_KL);
1359 freeCUdeviceptr(d_is_glass);
1360}
1361
1362void OptiX8Backend::reallocDevice(CUdeviceptr &ptr, size_t bytes) {
1363 freeCUdeviceptr(ptr);
1364 if (bytes > 0) {
1365 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&ptr), bytes));
1366 }
1367}
1368
1369std::vector<float> OptiX8Backend::downloadFloat(CUdeviceptr ptr, size_t count) const {
1370 std::vector<float> result(count);
1371 CUDA_CHECK(cudaMemcpy(result.data(), reinterpret_cast<const void *>(ptr),
1372 count * sizeof(float), cudaMemcpyDeviceToHost));
1373 return result;
1374}
1375
1376std::vector<uint32_t> OptiX8Backend::downloadUInt32(CUdeviceptr ptr, size_t count) const {
1377 std::vector<uint32_t> result(count);
1378 CUDA_CHECK(cudaMemcpy(result.data(), reinterpret_cast<const void *>(ptr),
1379 count * sizeof(uint32_t), cudaMemcpyDeviceToHost));
1380 return result;
1381}
1382
1383void OptiX8Backend::buildAABBs(const RayTracingGeometry &geometry) {
1384 const uint32_t Nprims = static_cast<uint32_t>(geometry.primitive_count);
1385 const uint32_t Nbboxes = static_cast<uint32_t>(geometry.bboxes.UUIDs.size());
1386 const uint32_t Ntotal = Nprims + Nbboxes;
1387 if (Ntotal == 0) return;
1388
1389 std::vector<OptixAabb> aabbs(Ntotal);
1390 const float eps = 1e-5f;
1391
1392 auto store_aabb = [&](uint32_t pos, float mn_x, float mn_y, float mn_z,
1393 float mx_x, float mx_y, float mx_z) {
1394 if (mx_x - mn_x < eps) { mn_x -= eps; mx_x += eps; }
1395 if (mx_y - mn_y < eps) { mn_y -= eps; mx_y += eps; }
1396 if (mx_z - mn_z < eps) { mn_z -= eps; mx_z += eps; }
1397 aabbs[pos] = {mn_x, mn_y, mn_z, mx_x, mx_y, mx_z};
1398 };
1399
1400 // Real primitives — AABB computed via canonical-space vertices + transform matrix
1401 for (uint32_t pos = 0; pos < Nprims; pos++) {
1402 const float *T = &geometry.transform_matrices[pos * 16];
1403 const uint32_t pt = geometry.primitive_types[pos];
1404
1405 float mn_x = FLT_MAX, mn_y = FLT_MAX, mn_z = FLT_MAX;
1406 float mx_x = -FLT_MAX, mx_y = -FLT_MAX, mx_z = -FLT_MAX;
1407
1408 auto expand = [&](float x, float y, float z) {
1409 const float wx = T[0]*x + T[1]*y + T[2]*z + T[3];
1410 const float wy = T[4]*x + T[5]*y + T[6]*z + T[7];
1411 const float wz = T[8]*x + T[9]*y + T[10]*z + T[11];
1412 if (wx < mn_x) mn_x = wx; if (wx > mx_x) mx_x = wx;
1413 if (wy < mn_y) mn_y = wy; if (wy > mx_y) mx_y = wy;
1414 if (wz < mn_z) mn_z = wz; if (wz > mx_z) mx_z = wz;
1415 };
1416
1417 if (pt == 0 || pt == 3) { // Patch or Tile: canonical space [-0.5, 0.5]^2
1418 expand(-0.5f, -0.5f, 0.f); expand( 0.5f, -0.5f, 0.f);
1419 expand(-0.5f, 0.5f, 0.f); expand( 0.5f, 0.5f, 0.f);
1420 } else if (pt == 1) { // Triangle: (0,0,0)-(0,1,0)-(1,1,0)
1421 expand(0.f, 0.f, 0.f); expand(0.f, 1.f, 0.f); expand(1.f, 1.f, 0.f);
1422 } else if (pt == 2) { // Disk: bounding box of unit circle
1423 expand(-0.5f, -0.5f, 0.f); expand( 0.5f, -0.5f, 0.f);
1424 expand(-0.5f, 0.5f, 0.f); expand( 0.5f, 0.5f, 0.f);
1425 } else if (pt == 4) { // Voxel: unit cube [0,1]^3
1426 for (float fx : {0.f, 1.f})
1427 for (float fy : {0.f, 1.f})
1428 for (float fz : {0.f, 1.f})
1429 expand(fx, fy, fz);
1430 } else { // Unknown type: unit box fallback
1431 expand(-0.5f, -0.5f, -0.5f); expand( 0.5f, 0.5f, 0.5f);
1432 }
1433
1434 store_aabb(pos, mn_x, mn_y, mn_z, mx_x, mx_y, mx_z);
1435 }
1436
1437 // Bbox faces: AABB from actual world-space vertices (4 vertices per face)
1438 for (uint32_t b = 0; b < Nbboxes; b++) {
1439 float mn_x = FLT_MAX, mn_y = FLT_MAX, mn_z = FLT_MAX;
1440 float mx_x = -FLT_MAX, mx_y = -FLT_MAX, mx_z = -FLT_MAX;
1441 for (int v = 0; v < 4; v++) {
1442 const helios::vec3 &vtx = geometry.bboxes.vertices[b * 4 + v];
1443 mn_x = std::min(mn_x, vtx.x); mx_x = std::max(mx_x, vtx.x);
1444 mn_y = std::min(mn_y, vtx.y); mx_y = std::max(mx_y, vtx.y);
1445 mn_z = std::min(mn_z, vtx.z); mx_z = std::max(mx_z, vtx.z);
1446 }
1447 store_aabb(Nprims + b, mn_x, mn_y, mn_z, mx_x, mx_y, mx_z);
1448 }
1449
1450 const size_t aabb_bytes = Ntotal * sizeof(OptixAabb);
1451 reallocDevice(d_aabbs, aabb_bytes);
1452 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_aabbs), aabbs.data(), aabb_bytes, cudaMemcpyHostToDevice));
1453}
1454
1455void OptiX8Backend::buildGAS(uint32_t Nprimitives) {
1456 if (d_gas_output) { cudaFree(reinterpret_cast<void *>(d_gas_output)); d_gas_output = 0; }
1457 gas_handle = 0;
1458
1459 if (Nprimitives == 0 || !d_aabbs) return;
1460
1461 const unsigned int build_flags[] = {OPTIX_GEOMETRY_FLAG_NONE};
1462 OptixBuildInputCustomPrimitiveArray ca = {};
1463 ca.aabbBuffers = &d_aabbs;
1464 ca.numPrimitives = Nprimitives;
1465 ca.strideInBytes = sizeof(OptixAabb);
1466 ca.flags = build_flags;
1467 ca.numSbtRecords = 1;
1468
1469 OptixBuildInput bi = {};
1470 bi.type = OPTIX_BUILD_INPUT_TYPE_CUSTOM_PRIMITIVES;
1471 bi.customPrimitiveArray = ca;
1472
1473 OptixAccelBuildOptions opts = {};
1474 opts.buildFlags = OPTIX_BUILD_FLAG_ALLOW_COMPACTION | OPTIX_BUILD_FLAG_PREFER_FAST_TRACE;
1475 opts.operation = OPTIX_BUILD_OPERATION_BUILD;
1476
1477 OptixAccelBufferSizes sizes = {};
1478 OPTIX_CHECK(optixAccelComputeMemoryUsage(optix_context, &opts, &bi, 1, &sizes));
1479
1480 CUdeviceptr d_temp = 0, d_pre_compact = 0, d_compact_size = 0;
1481 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_temp), sizes.tempSizeInBytes));
1482 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_pre_compact), sizes.outputSizeInBytes));
1483 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_compact_size), sizeof(uint64_t)));
1484
1485 OptixAccelEmitDesc emit = {};
1486 emit.type = OPTIX_PROPERTY_TYPE_COMPACTED_SIZE;
1487 emit.result = d_compact_size;
1488
1489 OPTIX_CHECK(optixAccelBuild(optix_context, cuda_stream, &opts, &bi, 1,
1490 d_temp, sizes.tempSizeInBytes,
1491 d_pre_compact, sizes.outputSizeInBytes,
1492 &gas_handle, &emit, 1));
1493 CUDA_CHECK(cudaStreamSynchronize(cuda_stream));
1494
1495 uint64_t compact_size = 0;
1496 CUDA_CHECK(cudaMemcpy(&compact_size, reinterpret_cast<const void *>(d_compact_size),
1497 sizeof(uint64_t), cudaMemcpyDeviceToHost));
1498
1499 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_gas_output), compact_size));
1500 OPTIX_CHECK(optixAccelCompact(optix_context, cuda_stream, gas_handle, d_gas_output, compact_size, &gas_handle));
1501 CUDA_CHECK(cudaStreamSynchronize(cuda_stream));
1502
1503 CUDA_CHECK(cudaFree(reinterpret_cast<void *>(d_temp)));
1504 CUDA_CHECK(cudaFree(reinterpret_cast<void *>(d_pre_compact)));
1505 CUDA_CHECK(cudaFree(reinterpret_cast<void *>(d_compact_size)));
1506
1507 h_params.traversable = gas_handle;
1508}
1509
1510void OptiX8Backend::buildSBT() {
1511 if (d_raygen_records) { cudaFree(reinterpret_cast<void *>(d_raygen_records)); d_raygen_records = 0; }
1512 if (d_miss_records) { cudaFree(reinterpret_cast<void *>(d_miss_records)); d_miss_records = 0; }
1513 if (d_hitgroup_records) { cudaFree(reinterpret_cast<void *>(d_hitgroup_records)); d_hitgroup_records = 0; }
1514
1515 // Raygen records: header only (no data), 4 records (direct, diffuse, camera, pixel_label)
1516 struct alignas(OPTIX_SBT_RECORD_ALIGNMENT) RaygenRecord {
1517 char header[OPTIX_SBT_RECORD_HEADER_SIZE];
1518 };
1519 constexpr int N_rg = 4;
1520 std::vector<RaygenRecord> rg_recs(N_rg);
1521 OPTIX_CHECK(optixSbtRecordPackHeader(pg_raygen_direct, &rg_recs[0]));
1522 OPTIX_CHECK(optixSbtRecordPackHeader(pg_raygen_diffuse, &rg_recs[1]));
1523 OPTIX_CHECK(optixSbtRecordPackHeader(pg_raygen_camera, &rg_recs[2]));
1524 OPTIX_CHECK(optixSbtRecordPackHeader(pg_raygen_pixel_label, &rg_recs[3]));
1525 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_raygen_records), N_rg * sizeof(RaygenRecord)));
1526 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_raygen_records), rg_recs.data(),
1527 N_rg * sizeof(RaygenRecord), cudaMemcpyHostToDevice));
1528
1529 // Cache individual record device pointers (used to select raygen per launch type)
1530 d_raygen_record_direct = d_raygen_records;
1531 d_raygen_record_diffuse = d_raygen_records + 1 * sizeof(RaygenRecord);
1532 d_raygen_record_camera = d_raygen_records + 2 * sizeof(RaygenRecord);
1533 d_raygen_record_pixel_label = d_raygen_records + 3 * sizeof(RaygenRecord);
1534
1535 // Miss records: header only, 4 records
1536 struct alignas(OPTIX_SBT_RECORD_ALIGNMENT) MissRecord {
1537 char header[OPTIX_SBT_RECORD_HEADER_SIZE];
1538 };
1539 constexpr int N_ms = 4;
1540 std::vector<MissRecord> ms_recs(N_ms);
1541 OPTIX_CHECK(optixSbtRecordPackHeader(pg_miss_direct, &ms_recs[0]));
1542 OPTIX_CHECK(optixSbtRecordPackHeader(pg_miss_diffuse, &ms_recs[1]));
1543 OPTIX_CHECK(optixSbtRecordPackHeader(pg_miss_camera, &ms_recs[2]));
1544 OPTIX_CHECK(optixSbtRecordPackHeader(pg_miss_pixel_label, &ms_recs[3]));
1545 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_miss_records), N_ms * sizeof(MissRecord)));
1546 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_miss_records), ms_recs.data(),
1547 N_ms * sizeof(MissRecord), cudaMemcpyHostToDevice));
1548
1549 // Hit group records: header + HitGroupData, 4 records (one per ray type)
1550 struct alignas(OPTIX_SBT_RECORD_ALIGNMENT) HitRecord {
1551 char header[OPTIX_SBT_RECORD_HEADER_SIZE];
1552 HitGroupData data;
1553 };
1554 constexpr int N_hg = 4;
1555 std::vector<HitRecord> hg_recs(N_hg);
1556 for (auto &r : hg_recs) {
1557 r.data.vertices = reinterpret_cast<float3 *>(d_patch_vertices);
1558 r.data.UUIDs = reinterpret_cast<uint32_t *>(d_patch_UUIDs);
1559 r.data.prim_type = 0; // patch
1560 }
1561 OPTIX_CHECK(optixSbtRecordPackHeader(pg_hit_direct, &hg_recs[0]));
1562 OPTIX_CHECK(optixSbtRecordPackHeader(pg_hit_diffuse, &hg_recs[1]));
1563 OPTIX_CHECK(optixSbtRecordPackHeader(pg_hit_camera, &hg_recs[2]));
1564 OPTIX_CHECK(optixSbtRecordPackHeader(pg_hit_pixel_label, &hg_recs[3]));
1565 CUDA_CHECK(cudaMalloc(reinterpret_cast<void **>(&d_hitgroup_records), N_hg * sizeof(HitRecord)));
1566 CUDA_CHECK(cudaMemcpy(reinterpret_cast<void *>(d_hitgroup_records), hg_recs.data(),
1567 N_hg * sizeof(HitRecord), cudaMemcpyHostToDevice));
1568
1569 sbt = {};
1570 sbt.raygenRecord = d_raygen_records; // updated per-launch type
1571 sbt.missRecordBase = d_miss_records;
1572 sbt.missRecordStrideInBytes = static_cast<uint32_t>(sizeof(MissRecord));
1573 sbt.missRecordCount = N_ms;
1574 sbt.hitgroupRecordBase = d_hitgroup_records;
1575 sbt.hitgroupRecordStrideInBytes = static_cast<uint32_t>(sizeof(HitRecord));
1576 sbt.hitgroupRecordCount = N_hg;
1577}
1578
1579void OptiX8Backend::uploadLaunchParams() {
1580 CUDA_CHECK(cudaMemcpyAsync(reinterpret_cast<void *>(d_params), &h_params,
1581 sizeof(OptiX8LaunchParams), cudaMemcpyHostToDevice, cuda_stream));
1582}
1583
1584void OptiX8Backend::applyLaunchParams(const RayTracingLaunchParams &params) {
1585 h_params.launch_offset = params.launch_offset;
1586 h_params.launch_count = params.launch_count;
1587 h_params.rays_per_primitive = params.rays_per_primitive;
1588 h_params.random_seed = params.random_seed;
1589 h_params.Nbands_global = params.num_bands_global;
1590 h_params.Nbands_launch = params.num_bands_launch;
1591 h_params.launch_face = params.launch_face;
1592 h_params.scattering_iteration = params.scattering_iteration;
1593 h_params.specular_reflection_enabled = params.specular_reflection_enabled;
1594 h_params.camera_ID = params.camera_id;
1595 h_params.camera_position = make_float3(params.camera_position.x, params.camera_position.y, params.camera_position.z);
1596 h_params.camera_direction = make_float2(params.camera_direction.x, params.camera_direction.y);
1597 h_params.camera_focal_length = params.camera_focal_length;
1598 h_params.camera_lens_diameter = params.camera_lens_diameter;
1599 h_params.FOV_aspect_ratio = params.camera_fov_aspect;
1600 h_params.camera_HFOV = params.camera_HFOV;
1601 h_params.camera_resolution.x = params.camera_resolution.x;
1602 h_params.camera_resolution.y = params.camera_resolution.y;
1603 h_params.camera_viewplane_length= params.camera_viewplane_length;
1604 h_params.camera_pixel_solid_angle = params.camera_pixel_solid_angle;
1605 h_params.camera_pixel_offset.x = params.camera_pixel_offset.x;
1606 h_params.camera_pixel_offset.y = params.camera_pixel_offset.y;
1607 h_params.camera_resolution_full.x = params.camera_resolution_full.x;
1608 h_params.camera_resolution_full.y = params.camera_resolution_full.y;
1609}
1610
1611std::string OptiX8Backend::findDeviceCodeFile() const {
1612 // OptiX 8 uses either PTX or OptixIR (.optixir)
1613 // Use the non-throwing resolver so we can probe each candidate in order
1614 const std::vector<std::string> candidate_names = {
1615 "OptiX8DeviceCode.optixir",
1616 "OptiX8DeviceCode.ptx",
1617 };
1618 for (const auto &name : candidate_names) {
1619 auto path = helios::tryResolveFilePath("plugins/radiation/" + name);
1620 if (!path.empty()) {
1621 return path.string();
1622 }
1623 }
1625 "ERROR (OptiX8Backend::findDeviceCodeFile): Could not find OptiX8DeviceCode.optixir or "
1626 "OptiX8DeviceCode.ptx in the radiation plugin asset directory. "
1627 "Ensure the radiation plugin was built with OptiX 8 support (HELIOS_HAVE_OPTIX8).");
1628 return ""; // unreachable
1629}
1630
1631} // namespace helios