Skip to content

Commit 1e45bc2

Browse files
committed
[1.3.64] 2026-01-30
- Added non-throwing path resolution functions `tryResolveFilePath()` and `tryResolvePluginAsset()` that return empty paths instead of throwing exceptions when files are not found. - Refactored `resolveTextureFile()` to use non-throwing path resolution functions, eliminating exception handling for file probing. - Added overloaded `PlantArchitecture::getPlantInternodeObjectIDs()` that can take a shoot type label string in order to only get object IDs for that shoot type. - Refactored ray tracing architecture with backend abstraction layer to enable future support for OptiX 7.7 and Vulkan backends while maintaining backward compatibility with OptiX 6.5. This is a major overhaul, but should be fully backward compatible - please report any errors you encounter. - Added type-safe buffer indexing utilities (`BufferIndexing.h`, `IndexTypes.h`) to eliminate manual index calculations and prevent indexing errors in multi-dimensional GPU buffers.
1 parent 9198ef5 commit 1e45bc2

26 files changed

+7014
-3979
lines changed

core/include/global.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,17 @@ namespace helios {
11741174
*/
11751175
[[nodiscard]] std::filesystem::path resolvePluginAsset(const std::string &pluginName, const std::string &assetPath);
11761176

1177+
//! Attempt to resolve a plugin asset path without throwing exceptions
1178+
/**
1179+
* Similar to resolvePluginAsset() but returns an empty path instead of throwing on failure.
1180+
* Useful for probing whether a plugin asset exists.
1181+
*
1182+
* \param[in] pluginName Name of the plugin (e.g., "plantarchitecture")
1183+
* \param[in] assetPath Path relative to plugin directory (e.g., "assets/textures/leaf.png")
1184+
* \return Absolute canonical path to the asset if found, empty path otherwise
1185+
* \ingroup functions
1186+
*/
1187+
[[nodiscard]] std::filesystem::path tryResolvePluginAsset(const std::string &pluginName, const std::string &assetPath);
11771188

11781189
//! Resolve file path using standard Helios resolution hierarchy
11791190
/**
@@ -1189,6 +1200,17 @@ namespace helios {
11891200
*/
11901201
[[nodiscard]] std::filesystem::path resolveFilePath(const std::string &filename);
11911202

1203+
//! Attempt to resolve a file path without throwing exceptions
1204+
/**
1205+
* Similar to resolveFilePath() but returns an empty path instead of throwing on failure.
1206+
* Useful for probing whether a file exists at various locations.
1207+
*
1208+
* \param[in] filename File name with or without path (e.g., "texture.jpg" or "models/texture.jpg")
1209+
* \return Absolute canonical path to the file if found, empty path otherwise
1210+
* \ingroup functions
1211+
*/
1212+
[[nodiscard]] std::filesystem::path tryResolveFilePath(const std::string &filename);
1213+
11921214
//! Resolve spectral data file path
11931215
/**
11941216
* \param[in] spectraFile Spectral data filename with or without path (e.g., "camera_spectral_library.xml")

core/src/Context.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,11 @@ std::vector<uint> Context::getAllUUIDs() const {
498498
}
499499
cached_all_uuids.push_back(UUID);
500500
}
501+
502+
// Sort UUIDs for consistent ordering across platforms
503+
// (std::unordered_map iteration order is platform-dependent)
504+
std::sort(cached_all_uuids.begin(), cached_all_uuids.end());
505+
501506
all_uuids_cache_valid = true;
502507
return cached_all_uuids;
503508
}

core/src/global.cpp

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2692,20 +2692,30 @@ std::filesystem::path helios::resolveAssetPath(const std::string &relativePath)
26922692
return resolveFilePath(relativePath);
26932693
}
26942694

2695+
std::filesystem::path helios::tryResolvePluginAsset(const std::string &pluginName, const std::string &assetPath) {
2696+
std::string pluginAssetPath = "plugins/" + pluginName + "/" + assetPath;
2697+
return tryResolveFilePath(pluginAssetPath);
2698+
}
2699+
26952700
std::filesystem::path helios::resolvePluginAsset(const std::string &pluginName, const std::string &assetPath) {
26962701
std::string pluginAssetPath = "plugins/" + pluginName + "/" + assetPath;
26972702
return resolveFilePath(pluginAssetPath);
26982703
}
26992704

27002705

2701-
std::filesystem::path helios::resolveFilePath(const std::string &filename) {
2706+
std::filesystem::path helios::tryResolveFilePath(const std::string &filename) {
2707+
// Non-throwing version for probing file existence
2708+
if (filename.empty()) {
2709+
return {};
2710+
}
2711+
27022712
// 1. If absolute path, validate and return
27032713
std::filesystem::path filepath(filename);
27042714
if (filepath.is_absolute()) {
27052715
if (std::filesystem::exists(filepath)) {
27062716
return std::filesystem::canonical(filepath);
27072717
} else {
2708-
helios_runtime_error("ERROR (helios::resolveFilePath): Absolute file path " + filename + " does not exist.");
2718+
return {};
27092719
}
27102720
}
27112721

@@ -2723,7 +2733,28 @@ std::filesystem::path helios::resolveFilePath(const std::string &filename) {
27232733
return std::filesystem::canonical(buildDirPath);
27242734
}
27252735

2726-
// File not found in either location - provide clear error message
2736+
// File not found in any location
2737+
return {};
2738+
}
2739+
2740+
std::filesystem::path helios::resolveFilePath(const std::string &filename) {
2741+
// Handle empty string case - return current working directory
2742+
if (filename.empty()) {
2743+
return std::filesystem::current_path();
2744+
}
2745+
2746+
// Try to resolve using the non-throwing version first
2747+
std::filesystem::path result = tryResolveFilePath(filename);
2748+
2749+
if (!result.empty()) {
2750+
return result;
2751+
}
2752+
2753+
// File not found - provide clear error message
2754+
std::filesystem::path currentDirPath = std::filesystem::current_path() / filename;
2755+
std::string buildDir = getBuildDirectory();
2756+
std::filesystem::path buildDirPath = std::filesystem::path(buildDir) / filename;
2757+
27272758
helios_runtime_error("ERROR (helios::resolveFilePath): Could not locate asset file: " + filename + " (checked: " + currentDirPath.string() + " and " + buildDirPath.string() + "). " +
27282759
"Ensure file exists relative to current directory or HELIOS_BUILD path.");
27292760
return {}; // This line should never be reached due to helios_runtime_error throwing

doc/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
# [1.3.64] 2026-01-30
4+
5+
## Core
6+
- Added non-throwing path resolution functions `tryResolveFilePath()` and `tryResolvePluginAsset()` that return empty paths instead of throwing exceptions when files are not found.
7+
8+
## Plant Architecture
9+
- Refactored `resolveTextureFile()` to use non-throwing path resolution functions, eliminating exception handling for file probing.
10+
- Added overloaded `PlantArchitecture::getPlantInternodeObjectIDs()` that can take a shoot type label string in order to only get object IDs for that shoot type.
11+
12+
## Radiation
13+
- Refactored ray tracing architecture with backend abstraction layer to enable future support for OptiX 7.7 and Vulkan backends while maintaining backward compatibility with OptiX 6.5. This is a major overhaul, but should be fully backward compatible - please report any errors you encounter.
14+
- Added type-safe buffer indexing utilities (`BufferIndexing.h`, `IndexTypes.h`) to eliminate manual index calculations and prevent indexing errors in multi-dimensional GPU buffers.
15+
316
# [1.3.63] 2026-01-21
417

518
## Core

plugins/plantarchitecture/include/PlantArchitecture.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2656,6 +2656,15 @@ class PlantArchitecture {
26562656
*/
26572657
[[nodiscard]] std::vector<uint> getPlantInternodeObjectIDs(uint plantID) const;
26582658

2659+
//! Get object IDs for internode (Tube) objects matching a specified shoot type label
2660+
/**
2661+
* \param[in] plantID ID of the plant instance.
2662+
* \param[in] shoot_type_label Label of the shoot type to filter internodes by.
2663+
* \return Vector of object IDs for all internodes matching the specified shoot type label.
2664+
* \note Throws a helios_runtime_error if the plant does not exist or if no shoots with the specified shoot type label are found.
2665+
*/
2666+
[[nodiscard]] std::vector<uint> getPlantInternodeObjectIDs(uint plantID, const std::string &shoot_type_label) const;
2667+
26592668
//! Get object IDs for all petiole (Tube) objects for a given plant
26602669
/**
26612670
* \param[in] plantID ID of the plant instance.

plugins/plantarchitecture/src/PlantArchitecture.cpp

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,15 @@ std::string PlantArchitecture::resolveTextureFile(const std::string &texture_fil
137137
}
138138

139139
// Try resolving as a general file path (handles already-resolved paths and paths relative to cwd)
140-
try {
141-
return helios::resolveFilePath(texture_file).string();
142-
} catch (const std::runtime_error &) {
143-
// Continue to plugin-specific resolution
140+
std::filesystem::path resolved_path = helios::tryResolveFilePath(texture_file);
141+
if (!resolved_path.empty()) {
142+
return resolved_path.string();
144143
}
145144

146145
// Try resolving as a plugin asset path
147-
try {
148-
return helios::resolvePluginAsset("plantarchitecture", texture_file).string();
149-
} catch (const std::runtime_error &) {
150-
// Continue to fallback
146+
resolved_path = helios::tryResolvePluginAsset("plantarchitecture", texture_file);
147+
if (!resolved_path.empty()) {
148+
return resolved_path.string();
151149
}
152150

153151
// If path doesn't have "assets/" prefix, try appropriate asset subdirectory based on file extension
@@ -159,10 +157,9 @@ std::string PlantArchitecture::resolveTextureFile(const std::string &texture_fil
159157
std::string subdirectory = (extension == ".obj" || extension == ".mtl") ? "assets/obj/" : "assets/textures/";
160158
std::string assets_path = subdirectory + filename;
161159

162-
try {
163-
return helios::resolvePluginAsset("plantarchitecture", assets_path).string();
164-
} catch (const std::runtime_error &) {
165-
// Fall through to error
160+
resolved_path = helios::tryResolvePluginAsset("plantarchitecture", assets_path);
161+
if (!resolved_path.empty()) {
162+
return resolved_path.string();
166163
}
167164
}
168165

@@ -4423,6 +4420,32 @@ std::vector<uint> PlantArchitecture::getPlantInternodeObjectIDs(uint plantID) co
44234420
return objIDs;
44244421
}
44254422

4423+
std::vector<uint> PlantArchitecture::getPlantInternodeObjectIDs(uint plantID, const std::string &shoot_type_label) const {
4424+
if (plant_instances.find(plantID) == plant_instances.end()) {
4425+
helios_runtime_error("ERROR (PlantArchitecture::getPlantInternodeObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");
4426+
}
4427+
4428+
std::vector<uint> objIDs;
4429+
4430+
auto &shoot_tree = plant_instances.at(plantID).shoot_tree;
4431+
4432+
bool shoot_type_found = false;
4433+
for (auto &shoot: shoot_tree) {
4434+
if (shoot->shoot_type_label == shoot_type_label) {
4435+
shoot_type_found = true;
4436+
if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
4437+
objIDs.push_back(shoot->internode_tube_objID);
4438+
}
4439+
}
4440+
}
4441+
4442+
if (!shoot_type_found) {
4443+
helios_runtime_error("ERROR (PlantArchitecture::getPlantInternodeObjectIDs): No shoots with shoot type label '" + shoot_type_label + "' exist for plant with ID " + std::to_string(plantID) + ".");
4444+
}
4445+
4446+
return objIDs;
4447+
}
4448+
44264449
std::vector<uint> PlantArchitecture::getPlantPetioleObjectIDs(uint plantID) const {
44274450
if (plant_instances.find(plantID) == plant_instances.end()) {
44284451
helios_runtime_error("ERROR (PlantArchitecture::getPlantPetioleObjectIDs): Plant with ID of " + std::to_string(plantID) + " does not exist.");

plugins/plantarchitecture/tests/selfTest.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2388,6 +2388,61 @@ DOCTEST_TEST_CASE("PlantArchitecture listShootTypeLabels - multiple instances")
23882388
DOCTEST_CHECK(std::find(tomato_labels.begin(), tomato_labels.end(), "mainstem") != tomato_labels.end());
23892389
}
23902390

2391+
DOCTEST_TEST_CASE("PlantArchitecture getPlantInternodeObjectIDs with shoot type filter") {
2392+
Context context;
2393+
PlantArchitecture plantarchitecture(&context);
2394+
2395+
// Build a bean plant (has two shoot types: "unifoliate" and "trifoliate")
2396+
plantarchitecture.loadPlantModelFromLibrary("bean");
2397+
uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0.0);
2398+
2399+
// Get all internode object IDs without filter
2400+
std::vector<uint> all_internodes = plantarchitecture.getPlantInternodeObjectIDs(plantID);
2401+
DOCTEST_CHECK(all_internodes.size() > 0);
2402+
2403+
// Get internode object IDs for "unifoliate" shoot type
2404+
std::vector<uint> unifoliate_internodes = plantarchitecture.getPlantInternodeObjectIDs(plantID, "unifoliate");
2405+
DOCTEST_CHECK(unifoliate_internodes.size() > 0);
2406+
2407+
// Get internode object IDs for "trifoliate" shoot type
2408+
std::vector<uint> trifoliate_internodes = plantarchitecture.getPlantInternodeObjectIDs(plantID, "trifoliate");
2409+
DOCTEST_CHECK(trifoliate_internodes.size() > 0);
2410+
2411+
// Verify that filtered results are subsets of all internodes
2412+
for (uint objID : unifoliate_internodes) {
2413+
DOCTEST_CHECK(std::find(all_internodes.begin(), all_internodes.end(), objID) != all_internodes.end());
2414+
}
2415+
for (uint objID : trifoliate_internodes) {
2416+
DOCTEST_CHECK(std::find(all_internodes.begin(), all_internodes.end(), objID) != all_internodes.end());
2417+
}
2418+
2419+
// Verify no overlap between unifoliate and trifoliate internodes
2420+
for (uint objID : unifoliate_internodes) {
2421+
DOCTEST_CHECK(std::find(trifoliate_internodes.begin(), trifoliate_internodes.end(), objID) == trifoliate_internodes.end());
2422+
}
2423+
2424+
// Verify that sum of filtered internodes equals total internodes
2425+
DOCTEST_CHECK(unifoliate_internodes.size() + trifoliate_internodes.size() == all_internodes.size());
2426+
}
2427+
2428+
DOCTEST_TEST_CASE("PlantArchitecture getPlantInternodeObjectIDs with shoot type filter - error cases") {
2429+
std::string error_message;
2430+
{
2431+
capture_cerr cerr_buffer;
2432+
Context context;
2433+
PlantArchitecture plantarchitecture(&context);
2434+
2435+
plantarchitecture.loadPlantModelFromLibrary("bean");
2436+
uint plantID = plantarchitecture.buildPlantInstanceFromLibrary(make_vec3(0, 0, 0), 0.0);
2437+
2438+
// Should throw for non-existent shoot type
2439+
DOCTEST_CHECK_THROWS(static_cast<void>(plantarchitecture.getPlantInternodeObjectIDs(plantID, "nonexistent_shoot_type")));
2440+
2441+
// Should throw for invalid plant ID
2442+
DOCTEST_CHECK_THROWS(static_cast<void>(plantarchitecture.getPlantInternodeObjectIDs(9999, "unifoliate")));
2443+
}
2444+
}
2445+
23912446
int PlantArchitecture::selfTest(int argc, char **argv) {
23922447
return helios::runDoctestWithValidation(argc, argv);
23932448
}

plugins/radiation/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ add_library(radiation STATIC
1414
"src/RadiationCamera.cpp"
1515
"src/CameraCalibration.cpp"
1616
"src/LensFlare.cpp"
17+
"src/RayTracingTypes.cpp"
18+
"src/backends/OptiX6Backend.cpp"
19+
"src/backends/BackendFactory.cpp"
1720
"tests/selfTest.cpp"
1821
)
1922

2023
target_include_directories(radiation PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/lib/json" )
24+
target_include_directories(radiation PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/backends")
2125

2226
find_package(CUDAToolkit REQUIRED)
2327
set(CMAKE_CUDA_COMPILER ${CUDAToolkit_NVCC_EXECUTABLE})

0 commit comments

Comments
 (0)