diff --git a/.github/workflows/continuous.yaml b/.github/workflows/continuous.yaml index bfa9289e..64be9f5e 100644 --- a/.github/workflows/continuous.yaml +++ b/.github/workflows/continuous.yaml @@ -4,6 +4,9 @@ on: pull_request: branches: - main + push: + branches: + - main concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -21,26 +24,26 @@ jobs: Unix: name: ${{ matrix.os }} (${{ matrix.compiler }}, ${{ matrix.config }}, ${{ matrix.sanitizer }}Sanitizer) runs-on: ${{ matrix.os }} + env: + CCACHE_COMPRESSLEVEL: 5 strategy: fail-fast: false matrix: - os: [ubuntu-24.04, macos-15-intel, macos-15] - config: [RelWithDebInfo, Debug] - compiler: [gcc, apple, llvm] - sanitizer: ["Address", "Thread"] # TODO: Add Memory+Undefined Sanitizer - exclude: - - os: macos-15-intel - compiler: gcc - - os: macos-15-intel - sanitizer: Thread - - os: macos-15-intel - config: Debug - - os: macos-15 - compiler: gcc - - os: ubuntu-24.04 - compiler: apple + include: + # Linux release builds exercise tests disabled in Debug mode, and ASan also enables LSan. + - { os: ubuntu-24.04, compiler: gcc, config: RelWithDebInfo, sanitizer: "Address;Undefined", split_debug: ON } + - { os: ubuntu-24.04, compiler: gcc, config: RelWithDebInfo, sanitizer: Thread } + - { os: ubuntu-24.04, compiler: llvm, config: Debug, sanitizer: "Address;Undefined" } + - { os: ubuntu-24.04, compiler: llvm, config: Debug, sanitizer: Thread } + + # Keep AppleClang release coverage on Intel and full sanitizer coverage on Apple Silicon. + - { os: macos-15-intel, compiler: apple, config: RelWithDebInfo, sanitizer: "Address;Undefined" } + - { os: macos-15, compiler: apple, config: RelWithDebInfo, sanitizer: "Address;Undefined" } + - { os: macos-15, compiler: apple, config: RelWithDebInfo, sanitizer: Thread } + - { os: macos-15, compiler: llvm, config: Debug, sanitizer: "Address;Undefined" } + - { os: macos-15, compiler: llvm, config: Debug, sanitizer: Thread } steps: - - name: Free Disk Space + - name: Free Disk Space (Linux) if: runner.os == 'Linux' uses: jlumbroso/free-disk-space@main with: @@ -57,6 +60,25 @@ jobs: docker-images: true swap-storage: true + - name: Free Disk Space (macOS) + if: runner.os == 'macOS' + run: | + echo "Disk usage before cleanup:" + df -h + sudo rm -rf /Library/Developer/CoreSimulator + sudo rm -rf "$HOME/Library/Developer/CoreSimulator" + sudo rm -rf /System/Library/AssetsV2/com_apple_MobileAsset_iOSSimulatorRuntime + sudo rm -rf /System/Library/AssetsV2/com_apple_MobileAsset_xrOSSimulatorRuntime + sudo rm -rf /System/Library/AssetsV2/com_apple_MobileAsset_watchOSSimulatorRuntime + sudo rm -rf /System/Library/AssetsV2/com_apple_MobileAsset_appleTVOSSimulatorRuntime + sudo rm -rf "$ANDROID_HOME" + sudo rm -rf /usr/local/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + echo "Disk usage after cleanup:" + df -h + - name: Set swap space if: runner.os == 'Linux' && matrix.compiler == 'gcc' && matrix.config == 'RelWithDebInfo' uses: pierotofy/set-swap-space@fc79b3f67fa8a838184ce84a674ca12238d2c761 @@ -133,7 +155,11 @@ jobs: - name: Ccache uses: hendrikmuhs/ccache-action@v1.2 with: - key: ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.sanitizer }}-${{ matrix.config }} + key: ${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.config }}-${{ matrix.sanitizer }} + # Keep more objects, and do not prune untouched entries because cancelled jobs + # would save an incomplete cache as the newest restore candidate. + max-size: 750M + verbose: 1 - name: Setup Xcode version if: matrix.os == 'macos-15-intel' @@ -153,11 +179,11 @@ jobs: - name: Select embree isa (macOS x64) if: runner.os == 'macOS' && runner.arch == 'X64' - run: echo "embree_max_isa=DEFAULT" >> $GITHUB_ENV + run: echo "embree_max_isa=SSE2" >> $GITHUB_ENV - name: Select embree isa (macOS arm64) if: runner.os == 'macOS' && runner.arch == 'ARM64' - run: echo "embree_max_isa=NONE" >> $GITHUB_ENV + run: echo "embree_max_isa=NEON" >> $GITHUB_ENV - name: Get number of CPU cores uses: SimenB/github-actions-cpu-cores@v1 @@ -171,13 +197,35 @@ jobs: -DLAGRANGE_JENKINS=ON \ -DLAGRANGE_ALL=ON \ -DLAGRANGE_LIMIT_PARALLELISM=ON \ + -DLAGRANGE_LIMIT_GITHUB_ACTIONS_PARALLELISM=ON \ -DOPENVDB_CORE_SHARED=ON \ -DOPENVDB_CORE_STATIC=OFF \ - -DUSE_EXPLICIT_INSTANTIATION=OFF \ + -DUSE_EXPLICIT_INSTANTIATION=ON \ -DLAGRANGE_POLYSCOPE_MOCK_BACKEND=ON \ + -DLAGRANGE_USE_SPLIT_DEBUG_INFO=${{ matrix.split_debug }} \ -DEMBREE_MAX_ISA=${{ env.embree_max_isa }} \ -DUSE_SANITIZER="${{ matrix.sanitizer }}" + - name: Build Embree + if: runner.os == 'Linux' || matrix.compiler == 'llvm' + run: cmake --build ${{ env.build_dir }} --target embree -j 2 + + - name: Show disk space + if: always() + run: | + echo "disk usage:" + df -h + + - name: Build OpenVDB + if: runner.os == 'Linux' || matrix.compiler == 'llvm' + run: cmake --build ${{ env.build_dir }} --target openvdb_shared -j 2 + + - name: Show disk space + if: always() + run: | + echo "disk usage:" + df -h + - name: Build run: cmake --build ${{ env.build_dir }} -j ${{ steps.cpu-cores.outputs.count }} @@ -190,6 +238,12 @@ jobs: - name: Tests run: cd ${{ env.build_dir }}; ctest --verbose -j ${{ steps.cpu-cores.outputs.count }} + - name: Clean and report ccache + if: always() + run: | + ccache --cleanup + ccache --show-stats --verbose + #################### # Windows #################### @@ -199,6 +253,7 @@ jobs: runs-on: ${{ matrix.os }} env: SCCACHE_GHA_ENABLED: "true" + SCCACHE_CACHE_ZSTD_LEVEL: "5" strategy: fail-fast: false matrix: @@ -250,7 +305,7 @@ jobs: id: cpu-cores - name: Sccache - uses: mozilla-actions/sccache-action@v0.0.10 + uses: mozilla-actions/sccache-action@v0.0.11 - name: Set x64 vars if: matrix.os == 'windows-2025' diff --git a/CMakeLists.txt b/CMakeLists.txt index 56321d21..9d2ed5cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -258,11 +258,13 @@ option(LAGRANGE_EXAMPLES "Build all examples" option(LAGRANGE_FMT_EIGEN_FIX "Avoid MSVC C1001 error releated to fmt and eigen" ${LAGRANGE_FMT_EIGEN_FIX_DEFAULT}) option(LAGRANGE_INSTALL "Enable installation" ${LAGRANGE_TOPLEVEL_PROJECT}) option(LAGRANGE_LIMIT_PARALLELISM "Limit parallelism according to available cpu/memory" OFF) +option(LAGRANGE_LIMIT_GITHUB_ACTIONS_PARALLELISM "Limit heavy compile jobs on GitHub Actions" OFF) option(LAGRANGE_MORE_WARNINGS "Increase the level of warnings when compiling" OFF) option(LAGRANGE_PROPAGATE_WARNINGS "Propagate warning flags to dependent targets" ON) option(LAGRANGE_PERFORMANCE_TESTS "Build all performance tests" OFF) option(LAGRANGE_UNIT_TESTS "Build all unit tests" ${LAGRANGE_TOPLEVEL_PROJECT}) option(LAGRANGE_USE_PCH "Enable precompiled headers" OFF) +option(LAGRANGE_USE_SPLIT_DEBUG_INFO "Store debug information in separate compressed files" OFF) option(LAGRANGE_USE_WASM_EXCEPTIONS "Use -fwasm-exception flag with Emscripten" ON) option(LAGRANGE_USE_WASM_THREADS "Enable threads (-pthread) with Emscripten" ON) option(LAGRANGE_WITH_TRACY "Build tracy client with Lagrange" OFF) @@ -441,6 +443,9 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) if(LAGRANGE_LIMIT_PARALLELISM) lagrange_limit_parallelism() endif() +if(LAGRANGE_LIMIT_GITHUB_ACTIONS_PARALLELISM) + lagrange_limit_github_actions_parallelism() +endif() set(TBB_ENABLE_WASM_THREADS ${LAGRANGE_USE_WASM_THREADS}) diff --git a/cmake/lagrange/lagrange_global_flags.cmake b/cmake/lagrange/lagrange_global_flags.cmake index 4f2db768..13aa95c7 100644 --- a/cmake/lagrange/lagrange_global_flags.cmake +++ b/cmake/lagrange/lagrange_global_flags.cmake @@ -49,6 +49,11 @@ else() add_compile_options(${LAGRANGE_GLOBAL_FLAGS}) endif() +if(LAGRANGE_USE_SPLIT_DEBUG_INFO) + add_compile_options(-gsplit-dwarf -gz=zstd) + add_link_options(-Wl,--compress-debug-sections=zstd) +endif() + if(LAGRANGE_WITH_TRACY) include(lagrange_filter_flags) set(LAGRANGE_GLOBAL_FLAGS diff --git a/cmake/lagrange/lagrange_limit_parallelism.cmake b/cmake/lagrange/lagrange_limit_parallelism.cmake index 8dd9d046..2cf9c7e5 100644 --- a/cmake/lagrange/lagrange_limit_parallelism.cmake +++ b/cmake/lagrange/lagrange_limit_parallelism.cmake @@ -58,6 +58,19 @@ function(lagrange_limit_parallelism) endif() endfunction() +function(lagrange_limit_github_actions_parallelism) + set(num_heavy_compile_jobs 2) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" + AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang" + AND USE_SANITIZER MATCHES "([Aa]ddress)" + AND USE_SANITIZER MATCHES "([Uu]ndefined)") + set(num_heavy_compile_jobs 1) + endif() + + message(STATUS "Parallelism: Limiting heavy compile pool to ${num_heavy_compile_jobs}") + set_property(GLOBAL APPEND PROPERTY JOB_POOLS pool-heavy-compile=${num_heavy_compile_jobs}) +endfunction() + # If this file is run in script mode, it echoes the number of physical cores for use as # the -j flag for cmake --build and ctest. Link parallelism is not relevant here — it is # enforced by Ninja job pools set during the configure step. diff --git a/cmake/lagrange/lagrange_set_sanitizers.cmake b/cmake/lagrange/lagrange_set_sanitizers.cmake index 184e7cd7..93a46985 100644 --- a/cmake/lagrange/lagrange_set_sanitizers.cmake +++ b/cmake/lagrange/lagrange_set_sanitizers.cmake @@ -12,6 +12,18 @@ include(sanitizers) +# Some bundled dependencies intentionally use operations rejected by UBSan (for example, +# function-pointer casts in Embree). Keep the sanitizer enabled for Lagrange while allowing +# concrete third-party targets to opt out without propagating the flag to their consumers. +function(lagrange_disable_ubsan_for_external target) + if(TARGET "${target}" AND USE_SANITIZER MATCHES "([Uu]ndefined)") + target_compile_options("${target}" PRIVATE + $<$:-fno-sanitize=undefined> + $<$:-fno-sanitize=undefined> + ) + endif() +endfunction() + if(MSVC AND USE_SANITIZER) # To use sanitizers with MSVC you need to disable incompatible options. See details here: # https://learn.microsoft.com/en-us/cpp/sanitizers/asan?view=msvc-170#ide-msbuild diff --git a/cmake/recipes/external/OpenVDB.cmake b/cmake/recipes/external/OpenVDB.cmake index c0bae13e..cbf41161 100644 --- a/cmake/recipes/external/OpenVDB.cmake +++ b/cmake/recipes/external/OpenVDB.cmake @@ -176,6 +176,11 @@ function(openvdb_import_target) # Inject real Boost dependencies instead of dummy Boost:headers one foreach(name IN ITEMS openvdb_static openvdb_shared) if(TARGET ${name}) + if(LAGRANGE_LIMIT_GITHUB_ACTIONS_PARALLELISM + AND CMAKE_SYSTEM_NAME STREQUAL "Linux" + AND CMAKE_GENERATOR MATCHES "^Ninja") + set_property(TARGET ${name} PROPERTY JOB_POOL_COMPILE pool-heavy-compile) + endif() target_link_libraries(${name} PUBLIC Boost::algorithm diff --git a/cmake/recipes/external/embree.cmake b/cmake/recipes/external/embree.cmake index a323f51c..46081ec1 100644 --- a/cmake/recipes/external/embree.cmake +++ b/cmake/recipes/external/embree.cmake @@ -129,6 +129,14 @@ function(embree_import_target) unignore_package(TBB) + # Embree has several memory-intensive translation units. Limit their concurrency on + # Linux when the project's memory-aware parallelism policy is enabled. + if(LAGRANGE_LIMIT_GITHUB_ACTIONS_PARALLELISM + AND CMAKE_SYSTEM_NAME STREQUAL "Linux" + AND CMAKE_GENERATOR MATCHES "^Ninja") + set_property(TARGET embree PROPERTY JOB_POOL_COMPILE pool-heavy-compile) + endif() + # Disable warnings if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") # Embree's subgrid.h is known for causing array subscript out of bound @@ -146,6 +154,25 @@ function(embree_import_target) target_compile_options(embree PRIVATE "-Wno-unused-but-set-variable") endif() + # Embree intentionally dispatches through ABI-compatible function pointers and uses + # type-punning downcasts internally. Exclude only its implementation from UBSan; consumers + # and Lagrange's raycasting code remain instrumented. + foreach(target IN ITEMS + embree + embree_sse42 + embree_avx + embree_avx2 + embree_avx512 + algorithms + lexers + math + simd + sys + tasking + ) + lagrange_disable_ubsan_for_external(${target}) + endforeach() + # Now we need to do some juggling to propagate the include directory properties # along with the `embree` target add_library(embree::embree INTERFACE IMPORTED GLOBAL) diff --git a/cmake/recipes/external/ufbx.cmake b/cmake/recipes/external/ufbx.cmake index 7ac2c981..d55b097a 100644 --- a/cmake/recipes/external/ufbx.cmake +++ b/cmake/recipes/external/ufbx.cmake @@ -32,6 +32,10 @@ target_include_directories(ufbx PUBLIC set_target_properties(ufbx PROPERTIES FOLDER third_party) set_target_properties(ufbx PROPERTIES POSITION_INDEPENDENT_CODE ON) +# ufbx's bit reader intentionally shifts by the encoded bit count before validating it. +# UBSan diagnoses this in valid FBX inputs, so exclude only the bundled implementation. +lagrange_disable_ubsan_for_external(ufbx) + # Install rules set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME ufbx) install(DIRECTORY ${ufbx_SOURCE_DIR} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) diff --git a/modules/core/src/SurfaceMesh.cpp b/modules/core/src/SurfaceMesh.cpp index 9874f50d..41671431 100644 --- a/modules/core/src/SurfaceMesh.cpp +++ b/modules/core/src/SurfaceMesh.cpp @@ -38,6 +38,7 @@ // clang-format on #include +#include #include #include @@ -3697,40 +3698,46 @@ SurfaceMesh to_surface_mesh(const SurfaceMeshInfo& info) if (ai->is_indexed) { switch (value_type) { -#define LA_X_restore_indexed(_, ValueType) \ - case make_attribute_value_type(): { \ - auto values = span( \ - reinterpret_cast(ai->values_bytes.data()), \ - ai->values_bytes.size() / sizeof(ValueType)); \ - auto indices = span( \ - reinterpret_cast(ai->indices_bytes.data()), \ - ai->indices_bytes.size() / sizeof(Index)); \ - id = mesh.template create_attribute_internal( \ - ai->name, \ - element, \ - usage, \ - ai->values_num_channels, \ - values, \ - indices); \ - break; \ +#define LA_X_restore_indexed(_, ValueType) \ + case make_attribute_value_type(): { \ + la_runtime_assert(ai->values_bytes.size() % sizeof(ValueType) == 0); \ + la_runtime_assert(ai->indices_bytes.size() % sizeof(Index) == 0); \ + std::vector values(ai->values_bytes.size() / sizeof(ValueType)); \ + std::vector indices(ai->indices_bytes.size() / sizeof(Index)); \ + if (!values.empty()) { \ + std::memcpy(values.data(), ai->values_bytes.data(), ai->values_bytes.size()); \ + } \ + if (!indices.empty()) { \ + std::memcpy(indices.data(), ai->indices_bytes.data(), ai->indices_bytes.size()); \ + } \ + id = mesh.template create_attribute_internal( \ + ai->name, \ + element, \ + usage, \ + ai->values_num_channels, \ + span(values.data(), values.size()), \ + span(indices.data(), indices.size())); \ + break; \ } LA_ATTRIBUTE_X(restore_indexed, 0) #undef LA_X_restore_indexed } } else { switch (value_type) { -#define LA_X_restore_attr(_, ValueType) \ - case make_attribute_value_type(): { \ - auto data = span( \ - reinterpret_cast(ai->data_bytes.data()), \ - ai->data_bytes.size() / sizeof(ValueType)); \ - id = mesh.template create_attribute_internal( \ - ai->name, \ - element, \ - usage, \ - ai->num_channels, \ - data); \ - break; \ +#define LA_X_restore_attr(_, ValueType) \ + case make_attribute_value_type(): { \ + la_runtime_assert(ai->data_bytes.size() % sizeof(ValueType) == 0); \ + std::vector data(ai->data_bytes.size() / sizeof(ValueType)); \ + if (!data.empty()) { \ + std::memcpy(data.data(), ai->data_bytes.data(), ai->data_bytes.size()); \ + } \ + id = mesh.template create_attribute_internal( \ + ai->name, \ + element, \ + usage, \ + ai->num_channels, \ + span(data.data(), data.size())); \ + break; \ } LA_ATTRIBUTE_X(restore_attr, 0) #undef LA_X_restore_attr