diff --git a/.github/workflows/avx-clang.yml b/.github/workflows/avx-clang.yml index 33bdc7f..6c412ed 100644 --- a/.github/workflows/avx-clang.yml +++ b/.github/workflows/avx-clang.yml @@ -1,4 +1,4 @@ -# Native x86-64 build + tests with Clang (the AVX2/FMA path). +# Native x86-64 build + tests with Clang (the AVX2/FMA path). The Release leg also builds bench/. name: avx-clang on: @@ -7,6 +7,7 @@ on: paths: - 'src/**' - 'tests/**' + - 'bench/**' - 'cmake/**' - '**/CMakeLists.txt' - '.github/workflows/avx-clang.yml' @@ -21,13 +22,19 @@ concurrency: jobs: build-test: + name: ${{ matrix.build_type }} runs-on: ubuntu-latest container: image: ghcr.io/aotodev/sgl-ci:latest + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] steps: - uses: actions/checkout@v4 - name: Configure, build and test run: | - CXX=clang++ cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DSGL_TESTS=ON + CXX=clang++ cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DSGL_TESTS=ON \ + -DSGL_BENCHMARKS=${{ matrix.build_type == 'Release' && 'ON' || 'OFF' }} cmake --build build -j ctest --test-dir build --output-on-failure diff --git a/.github/workflows/avx-gcc.yml b/.github/workflows/avx-gcc.yml index 0205ee8..729f231 100644 --- a/.github/workflows/avx-gcc.yml +++ b/.github/workflows/avx-gcc.yml @@ -1,4 +1,4 @@ -# Native x86-64 build + tests with GCC (the AVX2/FMA path). +# Native x86-64 build + tests with GCC (the AVX2/FMA path). The Release leg also builds bench/. name: avx-gcc on: @@ -7,6 +7,7 @@ on: paths: - 'src/**' - 'tests/**' + - 'bench/**' - 'cmake/**' - '**/CMakeLists.txt' - '.github/workflows/avx-gcc.yml' @@ -21,13 +22,19 @@ concurrency: jobs: build-test: + name: ${{ matrix.build_type }} runs-on: ubuntu-latest container: image: ghcr.io/aotodev/sgl-ci:latest + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] steps: - uses: actions/checkout@v4 - name: Configure, build and test run: | - CXX=g++ cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DSGL_TESTS=ON + CXX=g++ cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DSGL_TESTS=ON \ + -DSGL_BENCHMARKS=${{ matrix.build_type == 'Release' && 'ON' || 'OFF' }} cmake --build build -j ctest --test-dir build --output-on-failure diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 0000000..c5b693d --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,45 @@ +# Installs sgl, then builds tests/install against it through find_package, as a downstream project would. +name: install + +on: + push: + branches: [master] + paths: + - 'src/**' + - 'tests/install/**' + - 'cmake/**' + - '**/CMakeLists.txt' + - '.github/workflows/install.yml' + pull_request: + +permissions: + contents: read + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + install: + name: ${{ matrix.cxx }} + runs-on: ubuntu-latest + container: + image: ghcr.io/aotodev/sgl-ci:latest + strategy: + fail-fast: false + matrix: + cxx: [g++, clang++] + env: + CXX: ${{ matrix.cxx }} + steps: + - uses: actions/checkout@v4 + - name: Build and install sgl + run: | + cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$RUNNER_TEMP/prefix" + cmake --build build -j + cmake --install build + - name: Build and run the consumer + run: | + cmake -G Ninja -S tests/install -B consumer -DCMAKE_PREFIX_PATH="$RUNNER_TEMP/prefix" + cmake --build consumer + ctest --test-dir consumer --output-on-failure diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..c5e08a0 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,43 @@ +# Apple Silicon build + tests with Homebrew LLVM Clang and its libc++. Not Apple Clang: CMake scans +# C++ modules only for LLVM Clang, GCC and MSVC. +name: macos + +on: + push: + branches: [master] + paths: + - 'src/**' + - 'tests/**' + - 'cmake/**' + - '**/CMakeLists.txt' + - '.github/workflows/macos.yml' + pull_request: + +permissions: + contents: read + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + build-test: + name: ${{ matrix.build_type }} + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] + steps: + - uses: actions/checkout@v4 + - name: Install LLVM and Ninja + run: brew install llvm ninja + - name: Configure, build and test + # Links Homebrew's libc++: its headers are newer than the system libc++. + run: | + llvm="$(brew --prefix llvm)" + cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DSGL_TESTS=ON \ + -DCMAKE_C_COMPILER="$llvm/bin/clang" -DCMAKE_CXX_COMPILER="$llvm/bin/clang++" \ + -DCMAKE_EXE_LINKER_FLAGS="-L$llvm/lib/c++ -L$llvm/lib/unwind -lunwind -Wl,-rpath,$llvm/lib/c++" + cmake --build build -j + ctest --test-dir build --output-on-failure diff --git a/.github/workflows/msvc.yml b/.github/workflows/msvc.yml new file mode 100644 index 0000000..3ae88f8 --- /dev/null +++ b/.github/workflows/msvc.yml @@ -0,0 +1,37 @@ +# Windows x64 build + tests with MSVC, on the hosted runner: the CI image cannot run MSVC. +name: msvc + +on: + push: + branches: [master] + paths: + - 'src/**' + - 'tests/**' + - 'cmake/**' + - '**/CMakeLists.txt' + - '.github/workflows/msvc.yml' + pull_request: + +permissions: + contents: read + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + build-test: + name: ${{ matrix.build_type }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] + steps: + - uses: actions/checkout@v4 + - uses: ilammy/msvc-dev-cmd@v1 + - name: Configure, build and test + run: | + cmake -G Ninja -B build -DCMAKE_CXX_COMPILER=cl -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DSGL_TESTS=ON + cmake --build build -j + ctest --test-dir build --output-on-failure diff --git a/.github/workflows/neon.yml b/.github/workflows/neon.yml index 592dd8c..e920f0a 100644 --- a/.github/workflows/neon.yml +++ b/.github/workflows/neon.yml @@ -22,15 +22,20 @@ concurrency: jobs: build-test: + name: ${{ matrix.build_type }} runs-on: ubuntu-latest container: image: ghcr.io/aotodev/sgl-ci:latest + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] steps: - uses: actions/checkout@v4 - name: Configure, build and test run: | cmake -G Ninja -B build \ -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-aarch64.cmake \ - -DCMAKE_BUILD_TYPE=Release -DSGL_TESTS=ON + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DSGL_TESTS=ON cmake --build build -j ctest --test-dir build --output-on-failure diff --git a/.github/workflows/sanitize.yml b/.github/workflows/sanitize.yml new file mode 100644 index 0000000..a21fd20 --- /dev/null +++ b/.github/workflows/sanitize.yml @@ -0,0 +1,42 @@ +# ASan + UBSan over the test suite. Both compilers: only Clang's UBSan checks pointer-offset overflow. +name: sanitize + +on: + push: + branches: [master] + paths: + - 'src/**' + - 'tests/**' + - 'cmake/**' + - '**/CMakeLists.txt' + - '.github/workflows/sanitize.yml' + pull_request: + +permissions: + contents: read + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + sanitize: + name: ${{ matrix.cxx }} + runs-on: ubuntu-latest + container: + image: ghcr.io/aotodev/sgl-ci:latest + # LeakSanitizer suspends threads through ptrace. + options: --cap-add=SYS_PTRACE + strategy: + fail-fast: false + matrix: + cxx: [clang++, g++] + env: + UBSAN_OPTIONS: print_stacktrace=1 + steps: + - uses: actions/checkout@v4 + - name: Configure, build and test + run: | + CXX=${{ matrix.cxx }} cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Debug -DSGL_TESTS=ON -DSGL_SANITIZERS=ON + cmake --build build -j + ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 624e06c..aa0874a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,10 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Compiler support check: each minimum is the higher of the C++20 named-module floor # and the CWG 2518 floor. # --------------------------------------------------------------------------------------- -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") +if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + message(FATAL_ERROR "sgl: Apple Clang is not supported: CMake scans C++20 modules only for LLVM " + "Clang, GCC and MSVC. On macOS use LLVM Clang, e.g. Homebrew's llvm.") +elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "17") message(FATAL_ERROR "sgl: Clang >= 17 required (C++20 modules + CWG 2518) " "(found ${CMAKE_CXX_COMPILER_VERSION}).") @@ -73,6 +76,13 @@ option(SGL_PIC "Build with position-independent code" OFF) option(SGL_TESTS "Build sgl tests" OFF) option(SGL_BENCHMARKS "Build sgl benchmarks" OFF) option(SGL_INSTALL "Generate install + find_package() rules" "${PROJECT_IS_TOP_LEVEL}") +option(SGL_SANITIZERS "Build everything with ASan + UBSan (GCC/Clang, top level only)" OFF) + +# Directory-wide so the module units, the tests and the fetched GoogleTest all agree. +if(SGL_SANITIZERS AND PROJECT_IS_TOP_LEVEL AND NOT MSVC) + add_compile_options(-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) +endif() # --------------------------------------------------------------------------------------- # Library (always STATIC: the module interface is the real deliverable) @@ -90,6 +100,10 @@ target_compile_options(${PROJECT_NAME} PUBLIC "$<$,$>:/arch:AVX2>" ) +# C4324: vec3 and the boxes are padded to a register width on purpose. PUBLIC: MSVC repeats it in +# every TU that uses the types, so a consumer on /W4 /WX needs it too. +target_compile_options(${PROJECT_NAME} PUBLIC "$<$:/wd4324>") + if(SGL_PIC) set_target_properties(${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() diff --git a/README.md b/README.md index 73db8d0..c1b5e63 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ [![avx-gcc](https://github.com/aotodev/sgl/actions/workflows/avx-gcc.yml/badge.svg?branch=master)](https://github.com/aotodev/sgl/actions/workflows/avx-gcc.yml) [![avx-clang](https://github.com/aotodev/sgl/actions/workflows/avx-clang.yml/badge.svg?branch=master)](https://github.com/aotodev/sgl/actions/workflows/avx-clang.yml) [![neon](https://github.com/aotodev/sgl/actions/workflows/neon.yml/badge.svg?branch=master)](https://github.com/aotodev/sgl/actions/workflows/neon.yml) +[![msvc](https://github.com/aotodev/sgl/actions/workflows/msvc.yml/badge.svg?branch=master)](https://github.com/aotodev/sgl/actions/workflows/msvc.yml) +[![macos](https://github.com/aotodev/sgl/actions/workflows/macos.yml/badge.svg?branch=master)](https://github.com/aotodev/sgl/actions/workflows/macos.yml) +[![sanitize](https://github.com/aotodev/sgl/actions/workflows/sanitize.yml/badge.svg?branch=master)](https://github.com/aotodev/sgl/actions/workflows/sanitize.yml) +[![install](https://github.com/aotodev/sgl/actions/workflows/install.yml/badge.svg?branch=master)](https://github.com/aotodev/sgl/actions/workflows/install.yml) A C++20 module-based, SIMD-accelerated computational geometry library. @@ -116,6 +120,8 @@ compiles unchanged against the other. floor for C++20 named modules plus CWG 2518 (`static_assert(false)` in uninstantiated template branches) - A C++20 standard library and toolchain with named-module support +- On macOS, LLVM Clang (e.g. Homebrew's `llvm`): Apple Clang is not supported, because + CMake does not scan C++20 modules for it ## Building @@ -142,6 +148,11 @@ CXX=clang++ cmake -B build -G Ninja -DCMAKE_CXX_FLAGS="-stdlib=libc++" # MSVC (x64 Native Tools prompt, or a cross wrapper), Ninja generator cmake -B build -G Ninja + +# macOS: Homebrew LLVM Clang, linking its own libc++ rather than the older system one +llvm="$(brew --prefix llvm)" +CXX="$llvm/bin/clang++" cmake -B build -G Ninja \ + -DCMAKE_EXE_LINKER_FLAGS="-L$llvm/lib/c++ -L$llvm/lib/unwind -lunwind -Wl,-rpath,$llvm/lib/c++" ``` Then `cmake --build build` as usual. @@ -190,6 +201,7 @@ module interface with the matching instruction set. | `SGL_BENCHMARKS` | `OFF` | Build the Google Benchmark suite | | `SGL_PIC` | `OFF` | Build with position-independent code | | `SGL_INSTALL` | top-level only | Generate install + `find_package()` rules | +| `SGL_SANITIZERS` | `OFF` | Build with ASan + UBSan (GCC/Clang) | ### Running the tests diff --git a/src/sgl/soa.cppm b/src/sgl/soa.cppm index bf291dc..7106d04 100644 --- a/src/sgl/soa.cppm +++ b/src/sgl/soa.cppm @@ -161,7 +161,11 @@ template <> struct vocab_point { template concept vocab_point_type = requires { vocab_point>::dimension; }; -template constexpr std::size_t dimension_of = vocab_point::dimension; +template constexpr std::size_t dimension_of = 0; +template <> constexpr std::size_t dimension_of = 2; +template <> constexpr std::size_t dimension_of = 3; +template <> constexpr std::size_t dimension_of = 2; +template <> constexpr std::size_t dimension_of = 3; template <> constexpr std::size_t dimension_of = 2; template <> constexpr std::size_t dimension_of = 3; @@ -711,7 +715,8 @@ ray_hit hit_by( const std::array& origin, const std::array& direction, const T t_min = 0, const T t_max = std::numeric_limits::infinity()) noexcept { std::array inv{}; for (std::size_t a{}; a < D; ++a) { - inv[a] = T{1} / direction[a]; + /* No division by zero: keeps the FP divide-by-zero flag clear, and MSVC flags a constant one (C4723) in the caller. */ + inv[a] = direction[a] == T{0} ? std::copysign(std::numeric_limits::infinity(), direction[a]) : T{1} / direction[a]; } return {origin, inv, t_min, t_max}; } @@ -922,14 +927,16 @@ template auto bounds(const R& range) noexcept { auto hi1{hi0}; std::size_t i{}; - for (; std::floating_point && i + 2 * lanes <= n; i += 2 * lanes) { - for (std::size_t a{}; a < D; ++a) { - const auto x0{wide::load(view.axis[a] + i)}; - const auto x1{wide::load(view.axis[a] + i + lanes)}; - lo0[a] = min(x0, lo0[a]); - hi0[a] = max(x0, hi0[a]); - lo1[a] = min(x1, lo1[a]); - hi1[a] = max(x1, hi1[a]); + if constexpr (std::floating_point) { + for (; i + 2 * lanes <= n; i += 2 * lanes) { + for (std::size_t a{}; a < D; ++a) { + const auto x0{wide::load(view.axis[a] + i)}; + const auto x1{wide::load(view.axis[a] + i + lanes)}; + lo0[a] = min(x0, lo0[a]); + hi0[a] = max(x0, hi0[a]); + lo1[a] = min(x1, lo1[a]); + hi1[a] = max(x1, hi1[a]); + } } } for (; i + lanes <= n; i += lanes) { diff --git a/tests/install/CMakeLists.txt b/tests/install/CMakeLists.txt new file mode 100644 index 0000000..65dfbca --- /dev/null +++ b/tests/install/CMakeLists.txt @@ -0,0 +1,14 @@ +# Consumes an installed sgl through find_package, as a downstream project would. +cmake_minimum_required(VERSION 3.30) +project(sgl_install_test LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(sgl REQUIRED) + +add_executable(consumer main.cpp) +target_link_libraries(consumer PRIVATE sgl::sgl) + +enable_testing() +add_test(NAME consumer COMMAND consumer) diff --git a/tests/install/main.cpp b/tests/install/main.cpp new file mode 100644 index 0000000..028ac52 --- /dev/null +++ b/tests/install/main.cpp @@ -0,0 +1,21 @@ +/** + * @file main.cpp + * @brief Imports an installed sgl and touches both the AoS and the SoA surface. + */ +#include +#include + +import sgl; + +int main() { + const sgl::vec3 c{sgl::cross(sgl::vec3{1, 0, 0}, sgl::vec3{0, 1, 0})}; + + sgl::soa::point_buffer3f pts; + pts.push_back(sgl::vec3{1, 1, 1}); + pts.push_back(sgl::vec3{5, 5, 5}); + const auto inside{sgl::soa::count(pts, sgl::soa::inside(sgl::box3d{{0, 0, 0}, {2, 2, 2}}))}; + + const bool ok{c.z == 1.0f && inside == 1}; + std::puts(ok ? "sgl install ok" : "sgl install broken"); + return ok ? 0 : 1; +} diff --git a/tests/src/soa_test.cpp b/tests/src/soa_test.cpp index 5b6333e..fb2ff92 100644 --- a/tests/src/soa_test.cpp +++ b/tests/src/soa_test.cpp @@ -145,7 +145,7 @@ ref_hit ref_ray(const std::array& lo, const std::array& hi, float t_far{t_max}; bool inside{true}; for (std::size_t a{}; a < 3; ++a) { - const float inv{1.0f / d[a]}; + const float inv{d[a] == 0.0f ? std::copysign(inf, d[a]) : 1.0f / d[a]}; if (std::isinf(inv)) { inside = inside && lo[a] <= o[a] && o[a] <= hi[a]; continue; @@ -435,7 +435,12 @@ template std::vector run_match_indices(const R } std::vector as_u32(const std::vector& v) { - return {v.begin(), v.end()}; + std::vector out; + out.reserve(v.size()); + for (const std::size_t i : v) { + out.push_back(static_cast(i)); + } + return out; } } // namespace diff --git a/tests/src/soa_types_test.cpp b/tests/src/soa_types_test.cpp index a18b92c..0ef0ea6 100644 --- a/tests/src/soa_types_test.cpp +++ b/tests/src/soa_types_test.cpp @@ -152,7 +152,7 @@ template ref_hit ref_ray(const point& lo, const point& hi, co T t_far{t_max}; bool inside{true}; for (std::size_t a{}; a < 3; ++a) { - const T inv{T{1} / d[a]}; + const T inv{d[a] == T{0} ? std::copysign(std::numeric_limits::infinity(), d[a]) : T{1} / d[a]}; if (std::isinf(inv)) { inside = inside && lo[a] <= o[a] && o[a] <= hi[a]; continue;