diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d7286555..8166eee9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,6 +37,11 @@ if (XDG_ENABLE_MOAB) list(APPEND TEST_NAMES test_overlap_check) endif() +# This test really should be appended when any GPU library is enabled +if (XDG_ENABLE_GPRT) + list(APPEND TEST_NAMES test_direct_ray_buffer_access) +endif() + foreach(test ${TEST_NAMES}) add_executable(${test} ${test}.cpp) target_link_libraries(${test} xdg Catch2::Catch2WithMain) @@ -48,6 +53,18 @@ foreach(test ${TEST_NAMES}) TEST_PREFIX "${test}::") endforeach() +if (XDG_ENABLE_GPRT) + embed_devicecode( + OUTPUT_TARGET + test_direct_ray_buffer_access_deviceCode + HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/test_direct_ray_buffer_access_shared.h + SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/test_direct_ray_buffer_access_deviceCode.slang + ) + target_link_libraries(test_direct_ray_buffer_access test_direct_ray_buffer_access_deviceCode) +endif() + set( TEST_FILES diff --git a/tests/test_direct_ray_buffer_access.cpp b/tests/test_direct_ray_buffer_access.cpp new file mode 100644 index 00000000..97a96a11 --- /dev/null +++ b/tests/test_direct_ray_buffer_access.cpp @@ -0,0 +1,201 @@ +// for testing +#include +#include +#include +#include + +// xdg includes +#include "xdg/constants.h" +#include "xdg/mesh_manager_interface.h" +#include "xdg/gprt/ray_tracer.h" +#include "xdg/xdg.h" +#include "mesh_mock.h" +#include "test_direct_ray_buffer_access_shared.h" +#include "util.h" +#include "gprt.h" + +#include + +using namespace xdg; +using namespace xdg::test; + +extern GPRTProgram test_direct_ray_buffer_access_deviceCode; + +static RayPopulationCallback make_populate_callback(const std::vector& origins, + const std::vector& directions, + const std::vector& volume_ids, + GPRTContext context, + GPRTComputeOf packRays) { + return [&origins, &directions, volume_ids, context, packRays] + (const DeviceRayHitBuffers& buffer, size_t num_rays) { + + // When passing arrays to the callback, ensure they are the correct size + assert(origins.size() == num_rays); + assert(directions.size() == num_rays); + + // Convert to double3 for use on GPU + std::vector origins_device(num_rays); + std::vector directions_device(num_rays); + for (size_t i = 0; i < num_rays; ++i) { + origins_device[i] = {origins[i].x, origins[i].y, origins[i].z}; + directions_device[i] = {directions[i].x, directions[i].y, directions[i].z}; + } + + auto origins_buffer = gprtDeviceBufferCreate(context, num_rays, origins_device.data()); + auto directions_buffer = gprtDeviceBufferCreate(context, num_rays, directions_device.data()); + auto volume_ids_buffer = gprtDeviceBufferCreate(context, num_rays, volume_ids.data()); + + constexpr uint32_t threads_per_group = 256; + const uint32_t groups = static_cast((num_rays + threads_per_group - 1) / threads_per_group); + + ExternalRayParams params = {}; + params.xdgRays = static_cast(buffer.rayDevPtr); + params.origins = gprtBufferGetDevicePointer(origins_buffer); + params.directions = gprtBufferGetDevicePointer(directions_buffer); + params.num_rays = static_cast(num_rays); + params.total_threads = groups * threads_per_group; + params.volume_mesh_ids = gprtBufferGetDevicePointer(volume_ids_buffer); // Pass array of volume IDs to compute shader + params.enabled = 1u; + + gprtComputeLaunch(packRays, + { groups, 1, 1 }, + { threads_per_group, 1, 1 }, + params); + gprtComputeSynchronize(context); + + gprtBufferDestroy(origins_buffer); + gprtBufferDestroy(directions_buffer); + if (volume_ids_buffer) { + gprtBufferDestroy(volume_ids_buffer); + } + }; +} + +// This is a GPU only test - skip if no GPU ray tracing backends are enabled +TEMPLATE_TEST_CASE("Ray Fire with external populated rays", "[rayfire][mock]", + GPRT_Raytracer) +{ + constexpr auto rt_backend = TestType::value; + + DYNAMIC_SECTION(fmt::format("Backend = {}", rt_backend)) { + check_ray_tracer_supported(rt_backend); // skip if backend not enabled at configuration time + std::shared_ptr xdg = XDG::create(MeshLibrary::MOAB, rt_backend); + REQUIRE(xdg->ray_tracing_interface()->library() == rt_backend); + REQUIRE(xdg->mesh_manager()->mesh_library() == MeshLibrary::MOAB); + const auto& mesh_manager = xdg->mesh_manager(); + mesh_manager->load_file("jezebel.h5m"); + mesh_manager->init(); + xdg->prepare_raytracer(); + + std::vector origins; + std::vector directions; + size_t N = 64; + make_rays(N, origins, directions); + + auto gprt_rt = std::dynamic_pointer_cast(xdg->ray_tracing_interface()); + REQUIRE(gprt_rt); + + std::vector volumes = mesh_manager->volumes(); + REQUIRE(volumes.size() >= 2); + const MeshID volume_id = volumes[0]; + const MeshID volume_id_alt = volumes[1]; + GPRTContext context = gprt_rt->context(); + GPRTModule module = gprtModuleCreate(context, test_direct_ray_buffer_access_deviceCode); + auto packRays = gprtComputeCreate(context, module, "pack_external_rays"); + + std::vector expected_distances(N, INFTY); + std::vector expected_surfaces(N, ID_NONE); + std::vector volume_ids(N, volume_id); + for (size_t i = 0; i < N; ++i) { + volume_ids[i] = (i % 2 == 0) ? volume_id : volume_id_alt; // Volume IDs alternating between two volumes + const auto [dist, surf] = xdg->ray_fire(volume_ids[i], origins[i], directions[i]); + expected_distances[i] = dist; + expected_surfaces[i] = surf; + } + + // Create callback to populate rays on device + RayPopulationCallback populate_callback = make_populate_callback(origins, + directions, + volume_ids, + context, + packRays); + + // Populate rays via external API + xdg->populate_rays_external(N, populate_callback); + + xdg->ray_fire_prepared(N); + std::vector hits; + xdg->transfer_hits_buffer_to_host(N, hits); + + REQUIRE(hits.size() == N); + for (size_t i = 0; i < N; ++i) { + REQUIRE(hits[i].surf_id == expected_surfaces[i]); + if (expected_surfaces[i] != ID_NONE) { + REQUIRE_THAT(hits[i].distance, Catch::Matchers::WithinAbs(expected_distances[i], 1e-6)); + } + } + + gprtComputeDestroy(packRays); + gprtModuleDestroy(module); + } +} + +TEMPLATE_TEST_CASE("Point-in-volume with external populated rays", "[piv][mock]", + GPRT_Raytracer) +{ + constexpr auto rt_backend = TestType::value; + + DYNAMIC_SECTION(fmt::format("Backend = {}", rt_backend)) { + check_ray_tracer_supported(rt_backend); // skip if backend not enabled at configuration time + std::shared_ptr xdg = XDG::create(MeshLibrary::MOAB, rt_backend); + REQUIRE(xdg->ray_tracing_interface()->library() == rt_backend); + REQUIRE(xdg->mesh_manager()->mesh_library() == MeshLibrary::MOAB); + const auto& mesh_manager = xdg->mesh_manager(); + mesh_manager->load_file("jezebel.h5m"); + mesh_manager->init(); + xdg->prepare_raytracer(); + + std::vector points; + std::vector directions; + size_t N = 64; + make_points(N, points, directions); + + auto gprt_rt = std::dynamic_pointer_cast(xdg->ray_tracing_interface()); + REQUIRE(gprt_rt); + + std::vector volumes = mesh_manager->volumes(); + REQUIRE(volumes.size() >= 2); + const MeshID volume_id = volumes[0]; + const MeshID volume_id_alt = volumes[1]; + GPRTContext context = gprt_rt->context(); + GPRTModule module = gprtModuleCreate(context, test_direct_ray_buffer_access_deviceCode); + auto packRays = gprtComputeCreate(context, module, "pack_external_rays"); + + std::vector expected_piv(N, 0); + std::vector volume_ids(N, volume_id); + for (size_t i = 0; i < N; ++i) { + volume_ids[i] = (i % 2 == 0) ? volume_id : volume_id_alt; + expected_piv[i] = static_cast(xdg->point_in_volume(volume_ids[i], points[i], &directions[i])); + } + + RayPopulationCallback populate_callback = make_populate_callback(points, + directions, + volume_ids, + context, + packRays); + xdg->populate_rays_external(N, populate_callback); + + xdg->point_in_volume_prepared(N); + std::vector hits; + xdg->transfer_hits_buffer_to_host(N, hits); + + REQUIRE(hits.size() == N); + for (size_t i = 0; i < N; ++i) { + const auto expected = expected_piv[i] ? xdg::PointInVolume::INSIDE : xdg::PointInVolume::OUTSIDE; // convert back to enum + REQUIRE(hits[i].piv == expected); + } + + gprtComputeDestroy(packRays); + gprtModuleDestroy(module); + } +} diff --git a/tests/test_direct_ray_buffer_access_deviceCode.slang b/tests/test_direct_ray_buffer_access_deviceCode.slang new file mode 100644 index 00000000..b61e6126 --- /dev/null +++ b/tests/test_direct_ray_buffer_access_deviceCode.slang @@ -0,0 +1,24 @@ +#include "test_direct_ray_buffer_access_shared.h" + +[shader("compute")] +[numthreads(256, 1, 1)] +void pack_external_rays(uint3 DispatchThreadID: SV_DispatchThreadID, + uniform ExternalRayParams extParams) +{ + uint globalThreadID = DispatchThreadID.x; + uint stride = extParams.total_threads; // Groups * 256 + + // Grid-stride loop: each thread handles ray idx, idx+stride, idx+2*stride, ... + for (uint idx = globalThreadID; idx < extParams.num_rays; idx += stride) + { + xdg::dblRay r; + r.origin = extParams.origins[idx]; + r.direction = extParams.directions[idx]; + r.exclude_primitives = nullptr; + r.exclude_count = 0; + r.volume_mesh_id = extParams.volume_mesh_ids[idx]; // Set volume mesh ID per ray + r.enabled = extParams.enabled; + + extParams.xdgRays[idx] = r; // Write to device ray buffer + } +} diff --git a/tests/test_direct_ray_buffer_access_shared.h b/tests/test_direct_ray_buffer_access_shared.h new file mode 100644 index 00000000..e8d5ceb3 --- /dev/null +++ b/tests/test_direct_ray_buffer_access_shared.h @@ -0,0 +1,13 @@ +#include "gprt.h" + +#include "../include/xdg/gprt/ray.h" + +struct ExternalRayParams { + xdg::dblRay* xdgRays; + double3* origins; + double3* directions; + uint num_rays; + uint total_threads; + int32_t* volume_mesh_ids; + uint enabled; +}; diff --git a/tests/test_files b/tests/test_files index a3caf0af..ca579198 160000 --- a/tests/test_files +++ b/tests/test_files @@ -1 +1 @@ -Subproject commit a3caf0af3f128944c4d6eac93b481df6e4efd97c +Subproject commit ca57919851224047ef86fab177a0bfe9fa920127 diff --git a/tests/test_point_in_volume.cpp b/tests/test_point_in_volume.cpp index ae34e823..acb62a41 100644 --- a/tests/test_point_in_volume.cpp +++ b/tests/test_point_in_volume.cpp @@ -24,7 +24,6 @@ TEMPLATE_TEST_CASE("Point-in-volume on MeshMock", "[piv][mock]", check_ray_tracer_supported(rt_backend); // skip if backend not enabled at configuration time auto rti = create_raytracer(rt_backend); REQUIRE(rti); - rti->init(); // Keep MeshMock usage consistent across backends auto mm = std::make_shared(false); @@ -78,3 +77,71 @@ TEMPLATE_TEST_CASE("Point-in-volume on MeshMock", "[piv][mock]", REQUIRE(result == false); } } + +TEMPLATE_TEST_CASE("Batch API Point-in-volume on MeshMock", "[piv][mock][batch]", + Embree_Raytracer, + GPRT_Raytracer) +{ + constexpr auto rt_backend = TestType::value; + + DYNAMIC_SECTION(fmt::format("Backend = {}", rt_backend)) { + check_ray_tracer_supported(rt_backend); + if (rt_backend == RTLibrary::EMBREE) { + SKIP("Skipping PIV batch for Embree: batch API not implemented yet"); + } + + auto rti = create_raytracer(rt_backend); + REQUIRE(rti); + + auto mm = std::make_shared(false); + mm->init(); + REQUIRE(mm->mesh_library() == MeshLibrary::MOCK); + + auto [volume_tree, element_tree] = rti->register_volume(mm, mm->volumes()[0]); + REQUIRE(volume_tree != ID_NONE); + REQUIRE(element_tree == ID_NONE); + + rti->init(); + + std::vector points; + std::vector directions; + std::vector has_dir; + size_t N; + + SECTION("N=0 no-op") { + rti->point_in_volume(volume_tree, + nullptr, /*points*/ + 0, /*num_points*/ + nullptr /*results*/); + SUCCEED("N=0 completed without error"); + } + + SECTION("N=1") { + N = 1; + make_points(N, points, directions); + + auto scalar_result = static_cast(rti->point_in_volume(volume_tree, points[0], &directions[0])); + + std::vector batch_result(N, 0xFF); + rti->point_in_volume(volume_tree, points.data(), N, batch_result.data(), directions.data()); + REQUIRE(batch_result[0] == scalar_result); + } + + SECTION("N=64") { + N = 64; + make_points(N, points, directions); + + // Store results of scalar point_in_volume calls to verify batch against scalar + std::vector scalar_results(N, 0); + for (size_t i = 0; i < N; ++i) { + scalar_results[i] = static_cast(rti->point_in_volume(volume_tree, points[i], &directions[i])); + } + + std::vector batch_results(N, 0xFF); + rti->point_in_volume(volume_tree, points.data(), N, batch_results.data(), directions.data()); + for (size_t i = 0; i < points.size(); ++i) { + REQUIRE(batch_results[i] == scalar_results[i]); + } + } + } +} diff --git a/tests/test_ray_fire.cpp b/tests/test_ray_fire.cpp index be816c36..552a4ed6 100644 --- a/tests/test_ray_fire.cpp +++ b/tests/test_ray_fire.cpp @@ -4,13 +4,14 @@ #include #include - // xdg includes #include "xdg/constants.h" #include "xdg/mesh_manager_interface.h" #include "mesh_mock.h" #include "util.h" +#include + using namespace xdg; using namespace xdg::test; @@ -109,4 +110,81 @@ TEMPLATE_TEST_CASE("Ray Fire on MeshMock (per-backend sections)", "[rayfire][moc intersection = rti->ray_fire(volume_tree, origin, direction, INFTY, HitOrientation::EXITING, &exclude_primitives); REQUIRE(intersection.second == ID_NONE); } -} \ No newline at end of file +} + +TEMPLATE_TEST_CASE("Batch API Ray Fire on MeshMock", "[rayfire][mock][batch]", + Embree_Raytracer, + GPRT_Raytracer) +{ + constexpr auto rt_backend = TestType::value; + + DYNAMIC_SECTION(fmt::format("Backend = {}", rt_backend)) { + check_ray_tracer_supported(rt_backend); + if (rt_backend == RTLibrary::EMBREE) { + SKIP("Skipping batch query mechanics test for Embree: batch API not implemented."); + } + + auto rti = create_raytracer(rt_backend); + REQUIRE(rti); + + auto mm = std::make_shared(false); + mm->init(); + REQUIRE(mm->mesh_library() == MeshLibrary::MOCK); + + auto [volume_tree, element_tree] = rti->register_volume(mm, mm->volumes()[0]); + REQUIRE(volume_tree != ID_NONE); + REQUIRE(element_tree == ID_NONE); + + rti->init(); + + std::vector origins; + std::vector directions; + size_t N; + + // ---- N = 0 ---- + SECTION("N=0 no-op") { + rti->ray_fire(volume_tree, nullptr, nullptr, 0, nullptr, nullptr, + INFTY, HitOrientation::EXITING, nullptr); + SUCCEED("N=0 completed without error"); + } + + // ---- N = 1 ---- + SECTION("N=1 equals scalar") { + N = 1; + make_rays(N, origins, directions); + + auto [dist_scalar, id_scalar] = rti->ray_fire(volume_tree, origins[0], directions[0], INFTY, HitOrientation::EXITING); + + double dist_batch = -1.0; + MeshID id_batch = ID_NONE; + rti->ray_fire(volume_tree, origins.data(), directions.data(), 1, + &dist_batch, &id_batch, INFTY, HitOrientation::EXITING, nullptr); + + REQUIRE(id_batch == id_scalar); + REQUIRE_THAT(dist_batch, Catch::Matchers::WithinAbs(dist_scalar, 1e-6)); + } + + // ---- N = 64 ---- + SECTION("N=64 matches scalar for all") { + N = 64; + make_rays(N, origins, directions); + + std::vector dist_scalar(64, INFTY); + std::vector id_scalar(64, ID_NONE); + for (size_t i = 0; i < 64; ++i) { + auto [d, id] = rti->ray_fire(volume_tree, origins[i], directions[i], INFTY, HitOrientation::EXITING); + dist_scalar[i] = d; id_scalar[i] = id; + } + + std::vector dist_batch(64, -1.0); + std::vector id_batch(64, ID_NONE); + rti->ray_fire(volume_tree, origins.data(), directions.data(), origins.size(), + dist_batch.data(), id_batch.data(), INFTY, HitOrientation::EXITING, nullptr); + + for (size_t i = 0; i < 64; ++i) { + REQUIRE(id_batch[i] == id_scalar[i]); + REQUIRE_THAT(dist_batch[i], Catch::Matchers::WithinAbs(dist_scalar[i], 1e-6)); + } + } + } +} diff --git a/tests/util.h b/tests/util.h index 579ff652..58d81d8f 100644 --- a/tests/util.h +++ b/tests/util.h @@ -96,3 +96,32 @@ create_raytracer(xdg::RTLibrary rt) { return nullptr; } + +inline void make_rays(size_t N, std::vector& origins, std::vector& directions) +{ + origins.clear(); + directions.clear(); + origins.reserve(N); + directions.reserve(N); + for (size_t i = 0; i < N; ++i) { + int axis = static_cast(i % 3); + double s = (i % 2) ? 1.0 : -1.0; + origins.push_back({0.0, 0.0, 0.0}); + if (axis == 0) directions.push_back({s, 0.0, 0.0}); + else if (axis == 1) directions.push_back({0.0, s, 0.0}); + else directions.push_back({0.0, 0.0, s}); + } +} + +inline void make_points(size_t N, std::vector& points, std::vector& directions) +{ + points.resize(N); + directions.resize(N); + for (size_t i = 0; i < N; ++i) { + // evens inside (origin), odds just outside +X + points[i] = (i % 2 == 0) ? xdg::Position{0.0, 0.0, 0.0} : xdg::Position{5.1, 0.0, 0.0}; + // alternate ±X directions + directions[i] = (i % 2 == 0) ? xdg::Direction{1.0, 0.0, 0.0} + : xdg::Direction{-1.0, 0.0, 0.0}; + } +} diff --git a/vendor/GPRT b/vendor/GPRT index f1e95e41..405d9ee9 160000 --- a/vendor/GPRT +++ b/vendor/GPRT @@ -1 +1 @@ -Subproject commit f1e95e4188cde591547d6b4a33a70bf2afaeec59 +Subproject commit 405d9ee9f5ee8e1a0455f776f9e2c3adffb64160