Skip to content

Commit 85f0590

Browse files
committed
[1.3.46] 2025-08-25
* Enhanced GitHub Actions workflows with new Windows GPU self-tests and improved CI configuration * Upgraded documentation build system with improved cross-referencing and layout enhancements ## Context - Implemented filesystem-based asset path resolution with fallback mechanisms for different installation types - Added extensive new utility functions in `global.h` for asset management and path validation - Added comprehensive test coverage for new asset resolution functionality in `Test_functions.h` ## Visualizer - Fixed some potential segfaults that could occur when calling `Visualizer::plotInteractive()` or `Visualizer::plotUpdate()` in headless mode.
1 parent c09d5d0 commit 85f0590

591 files changed

Lines changed: 21308 additions & 17606 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/linux_GPU_selftests.yaml

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,42 @@ jobs:
2222
role-to-assume: ${{ secrets.OIDC_ROLE_ARN }}
2323
aws-region: us-west-2
2424
- run: |
25-
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID }}
26-
aws ec2 wait instance-running --instance-ids ${{ secrets.EC2_INSTANCE_ID }}
25+
# Wait for instance to be in a state where it can be started (max 5 minutes)
26+
echo "Waiting for instance to be in a startable state..."
27+
timeout 300 bash -c '
28+
while true; do
29+
STATE=$(aws ec2 describe-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_LINUX }} --query "Reservations[0].Instances[0].State.Name" --output text)
30+
echo "Current instance state: $STATE"
31+
if [[ "$STATE" == "stopped" || "$STATE" == "running" ]]; then
32+
echo "Instance is in a startable state: $STATE"
33+
break
34+
fi
35+
echo "Instance is in transitional state: $STATE. Waiting 10 seconds..."
36+
sleep 10
37+
done
38+
' || {
39+
echo "Timeout waiting for instance to reach startable state"
40+
exit 1
41+
}
42+
43+
# Only start if not already running
44+
CURRENT_STATE=$(aws ec2 describe-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_LINUX }} --query "Reservations[0].Instances[0].State.Name" --output text)
45+
if [[ "$CURRENT_STATE" == "stopped" ]]; then
46+
echo "Starting instance..."
47+
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_LINUX }}
48+
aws ec2 wait instance-running --instance-ids ${{ secrets.EC2_INSTANCE_ID_LINUX }}
49+
elif [[ "$CURRENT_STATE" == "running" ]]; then
50+
echo "Instance is already running"
51+
else
52+
echo "Unexpected instance state: $CURRENT_STATE"
53+
exit 1
54+
fi
2755
2856
run_samples_linux:
2957
# The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac.
3058
# You can convert this to a matrix build if you need cross-platform coverage.
3159
# See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
32-
runs-on: [self-hosted]
60+
runs-on: [self-hosted,helios]
3361
needs: start-gpu
3462
steps:
3563
- uses: actions/checkout@v3
@@ -53,5 +81,5 @@ jobs:
5381
role-to-assume: ${{ secrets.OIDC_ROLE_ARN }}
5482
aws-region: us-west-2
5583
- run: |
56-
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID }}
57-
aws ec2 wait instance-stopped --instance-ids ${{ secrets.EC2_INSTANCE_ID }}
84+
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_LINUX }}
85+
aws ec2 wait instance-stopped --instance-ids ${{ secrets.EC2_INSTANCE_ID_LINUX }}

.github/workflows/windows_GPU_selftests.yaml

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,36 @@ jobs:
2222
role-to-assume: ${{ secrets.OIDC_ROLE_ARN }}
2323
aws-region: us-west-2
2424
- run: |
25-
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_WIN }}
26-
aws ec2 wait instance-running --instance-ids ${{ secrets.EC2_INSTANCE_ID_WIN }}
25+
# Wait for instance to be in a state where it can be started (max 5 minutes)
26+
echo "Waiting for instance to be in a startable state..."
27+
timeout 300 bash -c '
28+
while true; do
29+
STATE=$(aws ec2 describe-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_WIN }} --query "Reservations[0].Instances[0].State.Name" --output text)
30+
echo "Current instance state: $STATE"
31+
if [[ "$STATE" == "stopped" || "$STATE" == "running" ]]; then
32+
echo "Instance is in a startable state: $STATE"
33+
break
34+
fi
35+
echo "Instance is in transitional state: $STATE. Waiting 10 seconds..."
36+
sleep 10
37+
done
38+
' || {
39+
echo "Timeout waiting for instance to reach startable state"
40+
exit 1
41+
}
42+
43+
# Only start if not already running
44+
CURRENT_STATE=$(aws ec2 describe-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_WIN }} --query "Reservations[0].Instances[0].State.Name" --output text)
45+
if [[ "$CURRENT_STATE" == "stopped" ]]; then
46+
echo "Starting instance..."
47+
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_WIN }}
48+
aws ec2 wait instance-running --instance-ids ${{ secrets.EC2_INSTANCE_ID_WIN }}
49+
elif [[ "$CURRENT_STATE" == "running" ]]; then
50+
echo "Instance is already running"
51+
else
52+
echo "Unexpected instance state: $CURRENT_STATE"
53+
exit 1
54+
fi
2755
2856
run_samples_windows:
2957
runs-on: [self-hosted, Windows]

core/include/Context.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2154,6 +2154,7 @@ namespace helios {
21542154

21552155
void addTexture(const char *texture_file);
21562156

2157+
21572158
bool doesTextureFileExist(const char *texture_file) const;
21582159

21592160
bool validateTextureFileExtenstion(const char *texture_file) const;
@@ -2262,7 +2263,7 @@ namespace helios {
22622263
OBJmaterial(const RGBcolor &a_color, std::string a_texture, uint a_materialID) : color{a_color}, texture{std::move(a_texture)}, materialID{a_materialID} {};
22632264
};
22642265

2265-
static std::map<std::string, OBJmaterial> loadMTL(const std::string &filebase, const std::string &material_file);
2266+
std::map<std::string, OBJmaterial> loadMTL(const std::string &filebase, const std::string &material_file);
22662267

22672268
void loadPData(pugi::xml_node p, uint UUID);
22682269

@@ -6129,6 +6130,8 @@ namespace helios {
61296130
*/
61306131
void writePLY(const char *filename, const std::vector<uint> &UUIDs) const;
61316132

6133+
// Asset directory registration removed - now using HELIOS_BUILD resolution
6134+
61326135
//! Load geometry contained in a Wavefront OBJ file (.obj). Model will be placed at the origin without any scaling or rotation applied.
61336136
/**
61346137
* \param[in] filename name of OBJ file
@@ -6199,6 +6202,16 @@ namespace helios {
61996202
*/
62006203
void writeOBJ(const std::string &filename, const std::vector<uint> &UUIDs, const std::vector<std::string> &primitive_dat_fields, bool write_normals = false) const;
62016204

6205+
//------------ FILE PATH RESOLUTION ----------------//
6206+
6207+
//! Unified file path resolution for Context methods - resolves relative paths using build directory
6208+
/**
6209+
* \param[in] filename Relative or absolute file path to resolve
6210+
* \return std::filesystem::path Resolved absolute file path
6211+
* \note Uses HELIOS_BUILD environment variable or auto-detection to resolve relative paths
6212+
*/
6213+
std::filesystem::path resolveFilePath(const std::string &filename) const;
6214+
62026215
//! Set simulation date by day, month, year
62036216
/**
62046217
* \param[in] day Day of the month (1-31)

core/include/global.h

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ constexpr To scast(From &&v) noexcept {
9999
// pugi XML parser
100100
#include "pugixml.hpp"
101101

102+
// Standard library for file path resolution
103+
#include <filesystem>
104+
102105
// *** Groups *** //
103106

104107
//! Miscellaneous helper functions
@@ -1115,6 +1118,70 @@ namespace helios {
11151118
*/
11161119
[[nodiscard]] bool validateOutputPath(std::string &output_directory, const std::vector<std::string> &allowable_file_extensions = {});
11171120

1121+
//--------------------- ASSET PATH RESOLUTION -----------------------------------//
1122+
1123+
//! Resolve asset file path using cpplocate, allowing executables to run from any directory
1124+
/**
1125+
* \param[in] relativePath Relative path to the asset file (e.g., "plugins/visualizer/shaders/shader.vert")
1126+
* \return Absolute path to the asset file
1127+
* \note This function searches for assets in multiple locations: build directory, system install locations, and custom paths
1128+
* \ingroup functions
1129+
*/
1130+
[[nodiscard]] std::filesystem::path resolveAssetPath(const std::string& relativePath);
1131+
1132+
//! Resolve plugin-specific asset path
1133+
/**
1134+
* \param[in] pluginName Name of the plugin (e.g., "visualizer", "plantarchitecture", "radiation")
1135+
* \param[in] assetPath Relative path within the plugin's asset directory
1136+
* \return Absolute path to the plugin asset file
1137+
* \ingroup functions
1138+
*/
1139+
[[nodiscard]] std::filesystem::path resolvePluginAsset(const std::string& pluginName, const std::string& assetPath);
1140+
1141+
1142+
//! Resolve file path using standard Helios resolution hierarchy
1143+
/**
1144+
* \param[in] filename File name with or without path (e.g., "primaryShader.vert" or "shaders/primaryShader.vert")
1145+
* \return Absolute path to the file
1146+
* \ingroup functions
1147+
*/
1148+
[[nodiscard]] std::filesystem::path resolveFilePath(const std::string& filename);
1149+
1150+
//! Resolve spectral data file path
1151+
/**
1152+
* \param[in] spectraFile Spectral data filename with or without path (e.g., "camera_spectral_library.xml")
1153+
* \return Absolute path to the spectral data file
1154+
* \ingroup functions
1155+
*/
1156+
[[nodiscard]] std::filesystem::path resolveSpectraPath(const std::string& spectraFile);
1157+
1158+
//! Validate that an asset file exists and is readable
1159+
/**
1160+
* \param[in] assetPath Path to the asset file to validate
1161+
* \return True if the file exists and is readable, false otherwise
1162+
* \ingroup functions
1163+
*/
1164+
[[nodiscard]] bool validateAssetPath(const std::filesystem::path& assetPath);
1165+
1166+
//! Find the project root directory (directory containing top-level CMakeLists.txt)
1167+
/**
1168+
* \param[in] startPath Starting directory for search (defaults to current working directory)
1169+
* \return Absolute path to the project root directory, or empty path if not found
1170+
* \note Searches upward from startPath for a directory containing CMakeLists.txt
1171+
* \ingroup functions
1172+
*/
1173+
[[nodiscard]] std::filesystem::path findProjectRoot(const std::filesystem::path& startPath = std::filesystem::current_path());
1174+
1175+
//! Resolve file path using project-based resolution strategy
1176+
/**
1177+
* \param[in] relativePath Relative path to the file
1178+
* \return Absolute path to the file
1179+
* \note Resolution order: 1) Current working directory, 2) Project directory, 3) Error
1180+
* \throws std::runtime_error if file cannot be found
1181+
* \ingroup functions
1182+
*/
1183+
[[nodiscard]] std::filesystem::path resolveProjectFile(const std::string& relativePath);
1184+
11181185
//! Read values contained in a text file into a one-dimensional vector of floats
11191186
/**
11201187
* \param[in] filepath Path to text file

core/src/Context.cpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ std::minstd_rand0 *Context::getRandomGenerator() {
4949
return &generator;
5050
}
5151

52+
// Asset directory registration system removed - now using HELIOS_BUILD resolution
53+
54+
std::filesystem::path Context::resolveFilePath(const std::string &filename) const {
55+
// Use the global helios::resolveFilePath function which implements HELIOS_BUILD resolution
56+
return helios::resolveFilePath(filename);
57+
}
58+
5259
void Context::addTexture(const char *texture_file) {
5360
if (textures.find(texture_file) == textures.end()) { // texture has not already been added
5461

@@ -61,12 +68,19 @@ void Context::addTexture(const char *texture_file) {
6168
helios_runtime_error("ERROR (Context::addTexture): Texture file " + std::string(texture_file) + " does not exist.");
6269
}
6370

64-
textures.emplace(texture_file, Texture(texture_file));
71+
// Use unified path resolution
72+
auto resolved_path = resolveFilePath(texture_file);
73+
textures.emplace(texture_file, Texture(resolved_path.string().c_str()));
6574
}
6675
}
6776

6877
bool Context::doesTextureFileExist(const char *texture_file) const {
69-
return std::filesystem::exists(texture_file);
78+
try {
79+
auto resolved_path = resolveFilePath(texture_file);
80+
return std::filesystem::exists(resolved_path);
81+
} catch (const std::runtime_error&) {
82+
return false;
83+
}
7084
}
7185

7286
bool Context::validateTextureFileExtenstion(const char *texture_file) const {

core/src/Context_fileIO.cpp

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,7 +1119,11 @@ std::vector<uint> Context::loadXML(const char *filename, bool quiet) {
11191119
helios_runtime_error("failed.\n File " + fn + " is not XML format.");
11201120
}
11211121

1122-
XMLfiles.emplace_back(filename);
1122+
// Resolve file path using unified resolution
1123+
std::filesystem::path resolved_path = resolveFilePath(filename);
1124+
std::string resolved_filename = resolved_path.string();
1125+
1126+
XMLfiles.emplace_back(resolved_filename);
11231127

11241128
uint ID;
11251129
std::vector<uint> UUID;
@@ -1128,7 +1132,7 @@ std::vector<uint> Context::loadXML(const char *filename, bool quiet) {
11281132
pugi::xml_document xmldoc;
11291133

11301134
// load file
1131-
pugi::xml_parse_result load_result = xmldoc.load_file(filename);
1135+
pugi::xml_parse_result load_result = xmldoc.load_file(resolved_filename.c_str());
11321136

11331137
// error checking
11341138
if (!load_result) {
@@ -3222,8 +3226,12 @@ std::vector<uint> Context::loadPLY(const char *filename, const vec3 &origin, flo
32223226

32233227
bool ifColor = false;
32243228

3229+
// Resolve file path using unified resolution
3230+
std::filesystem::path resolved_path = resolveFilePath(filename);
3231+
std::string resolved_filename = resolved_path.string();
3232+
32253233
std::ifstream inputPly;
3226-
inputPly.open(filename);
3234+
inputPly.open(resolved_filename);
32273235

32283236
if (!inputPly.is_open()) {
32293237
helios_runtime_error("ERROR (Context::loadPLY): Couldn't open " + std::string(filename));
@@ -3580,16 +3588,19 @@ std::vector<uint> Context::loadOBJ(const char *filename, const vec3 &origin, con
35803588

35813589
std::vector<uint> UUID;
35823590

3591+
// Resolve file path using unified resolution
3592+
std::filesystem::path resolved_path = resolveFilePath(filename);
3593+
std::string resolved_filename = resolved_path.string();
3594+
35833595
std::ifstream inputOBJ, inputMTL;
3584-
inputOBJ.open(filename);
3596+
inputOBJ.open(resolved_filename);
35853597

35863598
if (!inputOBJ.is_open()) {
35873599
helios_runtime_error("ERROR (Context::loadOBJ): Couldn't open " + std::string(filename));
35883600
}
35893601

3590-
// determine the base file path for 'filename'
3591-
std::string fstring = filename;
3592-
std::string filebase = getFilePath(fstring);
3602+
// determine the base file path for resolved filename
3603+
std::string filebase = getFilePath(resolved_filename);
35933604

35943605
// determine bounding box
35953606
float boxmin = 100000;
@@ -3811,26 +3822,34 @@ std::map<std::string, Context::OBJmaterial> Context::loadMTL(const std::string &
38113822
std::ifstream inputMTL;
38123823

38133824
std::string file = material_file;
3814-
3815-
// first look for mtl file using path given in obj file
3816-
inputMTL.open(file.c_str());
3817-
if (!inputMTL.is_open()) {
3818-
// if that doesn't work, try looking in the same directory where obj file is located
3819-
file = filebase + file;
3820-
file.erase(remove(file.begin(), file.end(), ' '), file.end());
3821-
for (size_t i = file.size(); i-- > 0;) {
3822-
if (strcmp(&file.at(i), "l") == 0) {
3823-
break;
3824-
}
3825-
3826-
file.erase(file.begin() + scast<int>(i));
3827-
}
3828-
if (file.empty()) {
3829-
helios_runtime_error("ERROR (Context::loadMTL): Material file does not have correct file extension (.mtl).");
3830-
}
3825+
std::string resolved_file;
3826+
3827+
// Try unified resolution first
3828+
try {
3829+
std::filesystem::path resolved_path = resolveFilePath(file);
3830+
resolved_file = resolved_path.string();
3831+
inputMTL.open(resolved_file.c_str());
3832+
} catch (const std::runtime_error &) {
3833+
// If unified resolution fails, fall back to original logic
38313834
inputMTL.open(file.c_str());
38323835
if (!inputMTL.is_open()) {
3833-
helios_runtime_error("ERROR (Context::loadMTL): Material file " + std::string(file) + " given in .obj file cannot be found.");
3836+
// if that doesn't work, try looking in the same directory where obj file is located
3837+
file = filebase + file;
3838+
file.erase(remove(file.begin(), file.end(), ' '), file.end());
3839+
for (size_t i = file.size(); i-- > 0;) {
3840+
if (strcmp(&file.at(i), "l") == 0) {
3841+
break;
3842+
}
3843+
3844+
file.erase(file.begin() + scast<int>(i));
3845+
}
3846+
if (file.empty()) {
3847+
helios_runtime_error("ERROR (Context::loadMTL): Material file does not have correct file extension (.mtl).");
3848+
}
3849+
inputMTL.open(file.c_str());
3850+
if (!inputMTL.is_open()) {
3851+
helios_runtime_error("ERROR (Context::loadMTL): Material file " + std::string(file) + " given in .obj file cannot be found.");
3852+
}
38343853
}
38353854
}
38363855

@@ -4430,7 +4449,11 @@ void Context::writePrimitiveData(const std::string &filename, const std::vector<
44304449
}
44314450

44324451
void Context::loadTabularTimeseriesData(const std::string &data_file, const std::vector<std::string> &col_labels, const std::string &a_delimeter, const std::string &a_date_string_format, uint headerlines) {
4433-
std::ifstream datafile(data_file); // open the file
4452+
// Resolve file path using project-based resolution
4453+
std::filesystem::path resolved_path = resolveProjectFile(data_file);
4454+
std::string resolved_filename = resolved_path.string();
4455+
4456+
std::ifstream datafile(resolved_filename); // open the file
44344457

44354458
if (!datafile.is_open()) { // check that file exists
44364459
helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Weather data file '" + data_file + "' does not exist.");

0 commit comments

Comments
 (0)