From 5d3f9de85d8892bbb14b39aa4d1909cb3f8921f2 Mon Sep 17 00:00:00 2001 From: ggalgoczi Date: Fri, 15 May 2026 20:59:00 +0000 Subject: [PATCH 1/2] feat(csgoptix): enable triangulated volume GAS in physics pipeline Adds the wiring needed so triangulated solids (forced via `stree__force_triangulate_solid`) traverse the OptiX builtin triangle BVH inside the same IAS as the analytic CSG GAS. * PIP.cc: enable `OPTIX_PRIMITIVE_TYPE_FLAGS_TRIANGLE` alongside `_CUSTOM` in `usesPrimitiveTypeFlags`; create a second hitgroup program group `hitgroup_pg_tri` with `entryFunctionNameIS = nullptr` (OptiX 9 requires no IS program for triangle hitgroups). Both PGs are passed to `optixPipelineCreate` and accumulated in `configureStack`. * PIP.h: declare `hitgroup_pg_tri` alongside the existing analytic PG. * SBT.cc: `createHitgroup` now packs each record header with the PG matching its GAS kind (`hitgroup_pg_tri` when `isSolidTrimesh(gas_idx)`, `hitgroup_pg` otherwise) instead of using a single PG for all records. The TriMesh HitGroupData layout (`Binding.h`), the triangle build input (`SOPTIX_BuildInput_Mesh`), and the `__closesthit__ch` triangle branch already existed; this commit fills in the pipeline + SBT side so they are actually exercised on a physics launch. --- CSGOptiX/PIP.cc | 98 ++++++++++++++++++++++++++++++------------------- CSGOptiX/PIP.h | 7 ++-- CSGOptiX/SBT.cc | 6 +-- 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/CSGOptiX/PIP.cc b/CSGOptiX/PIP.cc index be280a77ba..1fa8929531 100644 --- a/CSGOptiX/PIP.cc +++ b/CSGOptiX/PIP.cc @@ -70,7 +70,7 @@ OptixPipelineCompileOptions PIP::CreatePipelineOptions(unsigned numPayloadValues pipeline_compile_options.numAttributeValues = numAttributeValues ; pipeline_compile_options.exceptionFlags = OPT::ExceptionFlags( CreatePipelineOptions_exceptionFlags ) ; pipeline_compile_options.pipelineLaunchParamsVariableName = "params"; - pipeline_compile_options.usesPrimitiveTypeFlags = OPTIX_PRIMITIVE_TYPE_FLAGS_CUSTOM ; + pipeline_compile_options.usesPrimitiveTypeFlags = OPTIX_PRIMITIVE_TYPE_FLAGS_CUSTOM | OPTIX_PRIMITIVE_TYPE_FLAGS_TRIANGLE; return pipeline_compile_options ; } @@ -410,40 +410,62 @@ PIP::createHitgroupPG void PIP::createHitgroupPG() { - OptixProgramGroupDesc desc = {}; - desc.kind = OPTIX_PROGRAM_GROUP_KIND_HITGROUP; - - desc.hitgroup.moduleIS = module ; - desc.hitgroup.entryFunctionNameIS = IS ; - - desc.hitgroup.moduleCH = module ; - desc.hitgroup.entryFunctionNameCH = CH ; - - desc.hitgroup.moduleAH = nullptr ; - desc.hitgroup.entryFunctionNameAH = nullptr ; - - size_t sizeof_log = 0 ; - char log[2048]; - unsigned num_program_groups = 1 ; - - - OPTIX_CHECK_LOG( optixProgramGroupCreate( - Ctx::context, - &desc, - num_program_groups, - &program_group_options, - log, - &sizeof_log, - &hitgroup_pg - ) ); - - if(sizeof_log > 0) std::cout << log << std::endl ; - assert( sizeof_log == 0); + size_t sizeof_log = 0; + char log[2048]; + unsigned num_program_groups = 1; + + // analytic (custom primitive) hitgroup: custom IS + CH + OptixProgramGroupDesc desc_ana = {}; + desc_ana.kind = OPTIX_PROGRAM_GROUP_KIND_HITGROUP; + desc_ana.hitgroup.moduleIS = module; + desc_ana.hitgroup.entryFunctionNameIS = IS; + desc_ana.hitgroup.moduleCH = module; + desc_ana.hitgroup.entryFunctionNameCH = CH; + desc_ana.hitgroup.moduleAH = nullptr; + desc_ana.hitgroup.entryFunctionNameAH = nullptr; + + OPTIX_CHECK_LOG(optixProgramGroupCreate( + Ctx::context, + &desc_ana, + num_program_groups, + &program_group_options, + log, + &sizeof_log, + &hitgroup_pg)); + + if (sizeof_log > 0) + std::cout << log << std::endl; + assert(sizeof_log == 0); + + // triangle hitgroup: builtin IS (must be null) + CH + sizeof_log = 0; + OptixProgramGroupDesc desc_tri = {}; + desc_tri.kind = OPTIX_PROGRAM_GROUP_KIND_HITGROUP; + desc_tri.hitgroup.moduleIS = nullptr; + desc_tri.hitgroup.entryFunctionNameIS = nullptr; + desc_tri.hitgroup.moduleCH = module; + desc_tri.hitgroup.entryFunctionNameCH = CH; + desc_tri.hitgroup.moduleAH = nullptr; + desc_tri.hitgroup.entryFunctionNameAH = nullptr; + + OPTIX_CHECK_LOG(optixProgramGroupCreate( + Ctx::context, + &desc_tri, + num_program_groups, + &program_group_options, + log, + &sizeof_log, + &hitgroup_pg_tri)); + + if (sizeof_log > 0) + std::cout << log << std::endl; + assert(sizeof_log == 0); } void PIP::destroyHitgroupPG() { OPTIX_CHECK( optixProgramGroupDestroy( hitgroup_pg ) ); + OPTIX_CHECK(optixProgramGroupDestroy(hitgroup_pg_tri)); } @@ -482,7 +504,7 @@ std::string PIP::Desc_PipelineLinkOptions(const OptixPipelineLinkOptions& pipeli void PIP::linkPipeline(unsigned max_trace_depth) { - OptixProgramGroup program_groups[] = { raygen_pg, miss_pg, hitgroup_pg }; + OptixProgramGroup program_groups[] = {raygen_pg, miss_pg, hitgroup_pg, hitgroup_pg_tri}; OptixPipelineLinkOptions pipeline_link_options = {}; pipeline_link_options.maxTraceDepth = max_trace_depth ; @@ -550,13 +572,15 @@ void PIP::configureStack() OptixStackSizes stackSizes = {}; #if OPTIX_VERSION <= 70600 - OPTIX_CHECK( optixUtilAccumulateStackSizes( raygen_pg, &stackSizes ) ); - OPTIX_CHECK( optixUtilAccumulateStackSizes( miss_pg, &stackSizes ) ); - OPTIX_CHECK( optixUtilAccumulateStackSizes( hitgroup_pg, &stackSizes ) ); + OPTIX_CHECK(optixUtilAccumulateStackSizes(raygen_pg, &stackSizes)); + OPTIX_CHECK(optixUtilAccumulateStackSizes(miss_pg, &stackSizes)); + OPTIX_CHECK(optixUtilAccumulateStackSizes(hitgroup_pg, &stackSizes)); + OPTIX_CHECK(optixUtilAccumulateStackSizes(hitgroup_pg_tri, &stackSizes)); #else - OPTIX_CHECK( optixUtilAccumulateStackSizes( raygen_pg, &stackSizes, pipeline ) ); - OPTIX_CHECK( optixUtilAccumulateStackSizes( miss_pg, &stackSizes, pipeline ) ); - OPTIX_CHECK( optixUtilAccumulateStackSizes( hitgroup_pg, &stackSizes, pipeline ) ); + OPTIX_CHECK(optixUtilAccumulateStackSizes(raygen_pg, &stackSizes, pipeline)); + OPTIX_CHECK(optixUtilAccumulateStackSizes(miss_pg, &stackSizes, pipeline)); + OPTIX_CHECK(optixUtilAccumulateStackSizes(hitgroup_pg, &stackSizes, pipeline)); + OPTIX_CHECK(optixUtilAccumulateStackSizes(hitgroup_pg_tri, &stackSizes, pipeline)); #endif diff --git a/CSGOptiX/PIP.h b/CSGOptiX/PIP.h index bf147cb9f1..d1b566e1a2 100644 --- a/CSGOptiX/PIP.h +++ b/CSGOptiX/PIP.h @@ -31,9 +31,10 @@ struct PIP OptixModule module = nullptr; - OptixProgramGroup raygen_pg = nullptr; - OptixProgramGroup miss_pg = nullptr; - OptixProgramGroup hitgroup_pg = nullptr; + OptixProgramGroup raygen_pg = nullptr; + OptixProgramGroup miss_pg = nullptr; + OptixProgramGroup hitgroup_pg = nullptr; // analytic (custom primitive): IS + CH + OptixProgramGroup hitgroup_pg_tri = nullptr; // triangle: CH only (builtin IS) OptixPipeline pipeline = nullptr; diff --git a/CSGOptiX/SBT.cc b/CSGOptiX/SBT.cc index bc8c5598ec..2dafccdae5 100644 --- a/CSGOptiX/SBT.cc +++ b/CSGOptiX/SBT.cc @@ -899,9 +899,6 @@ void SBT::createHitgroup() hitgroup = new HitGroup[tot_rec] ; HitGroup* hg = hitgroup ; - for(unsigned i=0 ; i < tot_rec ; i++) // pack headers CPU side - OPTIX_CHECK( optixSbtRecordPackHeader( pip->hitgroup_pg, hitgroup + i ) ); - unsigned sbt_offset = 0 ; for(IT it=vgas.begin() ; it !=vgas.end() ; it++) @@ -974,6 +971,9 @@ void SBT::createHitgroup() int boundary = foundry->getPrimBoundary_(prim); assert( boundary > -1 ); + OptixProgramGroup record_pg = trimesh ? pip->hitgroup_pg_tri : pip->hitgroup_pg; + OPTIX_CHECK(optixSbtRecordPackHeader(record_pg, hg)); + if( trimesh == false ) // analytic { setPrimData( hg->data.prim, prim ); // copy numNode, nodeOffset from CSGPrim into hg->data From 0a01b1f2ae2c555a0c8f5119b87efb7e1c4129ef Mon Sep 17 00:00:00 2001 From: ggalgoczi Date: Fri, 15 May 2026 20:59:00 +0000 Subject: [PATCH 2/2] test(ci): compare triangulated vs analytic raindrop hit counts Add a CI test that reuses the existing GPURaytrace binary on tests/geom/opticks_raindrop.gdml. Runs once with the default analytic CSG path and once with `stree__force_triangulate_solid=G4_WATER_solid`, asserting bit-identical Opticks NumHits. A G4Box triangulates to 12 flat triangles that exactly coincide with the analytic surface (validated across multiple seeds), so a strict equality check is appropriate: any divergence indicates a regression in the triangulated-GAS pipeline wiring (PIP primitive flags, second hitgroup PG, or SBT per-record header packing). Hooked into .github/workflows/build-pull-request.yaml next to the existing test_GPURaytrace.sh invocation. --- .github/workflows/build-pull-request.yaml | 1 + tests/test_triangulated.sh | 24 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100755 tests/test_triangulated.sh diff --git a/.github/workflows/build-pull-request.yaml b/.github/workflows/build-pull-request.yaml index 22ac71cd2f..a7cd197b07 100644 --- a/.github/workflows/build-pull-request.yaml +++ b/.github/workflows/build-pull-request.yaml @@ -184,6 +184,7 @@ jobs: docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_opticks.sh docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_simg4ox.sh docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_GPURaytrace.sh + docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_triangulated.sh docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_GPUPhotonFileSource.sh docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_GPUPhotonSource_8x8SiPM.sh docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_wavelength_shifting.sh diff --git a/tests/test_triangulated.sh b/tests/test_triangulated.sh new file mode 100755 index 0000000000..72156bffe6 --- /dev/null +++ b/tests/test_triangulated.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Forcing triangulation of a box-shaped solid (here G4_WATER_solid in +# opticks_raindrop.gdml) must produce bit-identical GPU hit counts vs +# the analytic CSG path: a G4Box triangulates to 12 flat triangles that +# coincide with the analytic surface, so the only thing this test can +# detect breaking is the triangulated-GAS wiring (pipeline flags, second +# hitgroup PG, per-record SBT header packing). +set -e + +GDML="$OPTICKS_HOME/tests/geom/opticks_raindrop.gdml" +MAC="$OPTICKS_HOME/tests/run.mac" +SEED=42 + +ANA=$(USER=ci GEOM=triraindrop_ana \ + GPURaytrace -g "$GDML" -m "$MAC" -s "$SEED" 2>&1 \ + | awk '/Opticks: NumHits:/ {print $NF}') + +TRI=$(USER=ci GEOM=triraindrop_tri stree__force_triangulate_solid=G4_WATER_solid \ + GPURaytrace -g "$GDML" -m "$MAC" -s "$SEED" 2>&1 \ + | awk '/Opticks: NumHits:/ {print $NF}') + +echo "analytic=$ANA triangulated=$TRI" +[ -n "$ANA" ] && [ "$ANA" = "$TRI" ] || { echo "FAILED: triangulated($TRI) != analytic($ANA)"; exit 1; } +echo "PASSED"