Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .devcontainer/cuda/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# CUDA-enabled development container for the IPC Toolkit.
#
# This mirrors ../Dockerfile (the default Ubuntu dev container) but is based on
# NVIDIA's CUDA "devel" image so nvcc is available. Like the default container
# it does NOT copy the source: the workspace is bind-mounted at runtime by the
# Dev Containers tooling. Compiling CUDA does not require a GPU; running it
# does. On a Linux host with the NVIDIA Container Toolkit you can expose the GPU
# by adding "--gpus=all" to runArgs in devcontainer.json.

ARG CUDA_IMAGE=nvidia/cuda:12.6.2-devel-ubuntu22.04
FROM ${CUDA_IMAGE}

# Set environment variables
ENV DEBIAN_FRONTEND=noninteractive
ENV CCACHE_DIR=/home/devuser/.ccache
ENV CCACHE_MAXSIZE=1G
ENV CXX_STANDARD=17

# Install essential packages (nvcc + host gcc ship in the CUDA devel image).
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
git \
wget \
curl \
fish \
zsh \
ninja-build \
ccache \
rsync \
python3 \
python3-pip \
python3-dev \
libgmp-dev \
libssl-dev \
sudo \
software-properties-common \
lsb-release \
gnupg \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Create a new user with sudo privileges
RUN useradd -m devuser \
&& echo "devuser:password" | chpasswd \
&& usermod -aG sudo devuser \
&& echo "devuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \
&& mkdir -p $CCACHE_DIR \
&& chown devuser:devuser $CCACHE_DIR

# Set up Python tools
RUN pip3 install --no-cache-dir --upgrade pip setuptools wheel pre-commit

# Add Kitware APT repository for a recent CMake (project requires >= 3.24)
RUN wget -qO- https://apt.kitware.com/keys/kitware-archive-latest.asc | \
gpg --dearmor -o /usr/share/keyrings/kitware-archive-keyring.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main" | \
tee /etc/apt/sources.list.d/kitware.list > /dev/null \
&& apt-get update && apt-get install -y --no-install-recommends cmake \
&& rm -rf /var/lib/apt/lists/* \
&& cmake --version && nvcc --version

# Install LLVM/Clang and clang-format 18 (matches the default dev container so
# pre-commit's clang-format hook behaves identically).
RUN wget -q https://apt.llvm.org/llvm.sh -O /tmp/llvm.sh \
&& chmod +x /tmp/llvm.sh \
&& /tmp/llvm.sh 18 || true
RUN apt-get update \
&& apt-get install -y --no-install-recommends clang-18 clang-tools-18 clang-format-18 \
&& rm -rf /var/lib/apt/lists/* \
&& clang-format-18 --version

# Set the default user and working directory
USER devuser
WORKDIR /home/devuser/workspace

CMD ["bash"]
79 changes: 79 additions & 0 deletions .devcontainer/cuda/build-cuda.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
#
# Headless CUDA compile check that reuses the CUDA dev-container image.
#
# The dev container (devcontainer.json) is for *interactive* development. This
# script is the *batch* counterpart: it builds the same image and compiles the
# whole project with CUDA enabled, then exits with the build's status. Use it
# from a Mac (compile-only; no GPU) or in CI to keep the CUDA build green.
#
# The source is mounted read-only and copied into the container (minus build
# artifacts and the machine-specific IPCToolkitOptions.cmake) so the build is
# hermetic and never writes into your host working tree.
#
# Usage:
# .devcontainer/cuda/build-cuda.sh # cuda-release, arch 75;80;86;89
# PRESET=test .devcontainer/cuda/build-cuda.sh # test preset (CUDA + tests)
# CUDA_ARCH="86" .devcontainer/cuda/build-cuda.sh # single architecture
# JOBS=4 .devcontainer/cuda/build-cuda.sh # limit parallelism (memory)
#
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

IMAGE_NAME="${IMAGE_NAME:-ipc-toolkit-cuda-dev}"
PRESET="${PRESET:-cuda-release}"
CUDA_ARCH="${CUDA_ARCH:-75;80;86;89}"
CUDA_IMAGE="${CUDA_IMAGE:-nvidia/cuda:12.6.2-devel-ubuntu22.04}"
# Heavy TUs (headers textually include implementations under CUDA) can OOM the
# VM at full parallelism; default below nproc.
JOBS="${JOBS:-4}"

echo ">> Building CUDA dev image '${IMAGE_NAME}'"
docker build \
-f "${REPO_ROOT}/.devcontainer/cuda/Dockerfile" \
-t "${IMAGE_NAME}" \
--build-arg "CUDA_IMAGE=${CUDA_IMAGE}" \
"${REPO_ROOT}"

echo ">> Compiling (preset=${PRESET}, arch=${CUDA_ARCH})"
# Run as root so the named cache volumes are writable; the source is mounted
# read-only and copied to a scratch dir inside the container.
docker run --rm --user root \
-e PRESET="${PRESET}" \
-e CUDA_ARCH="${CUDA_ARCH}" \
-e JOBS="${JOBS}" \
-v "${REPO_ROOT}":/src:ro \
-v ipc-toolkit-cuda-workspace:/workspace \
-v ipc-toolkit-cpm-cache:/cpm-cache \
-v ipc-toolkit-cuda-ccache:/root/.ccache \
"${IMAGE_NAME}" \
bash -euo pipefail -c '
export CPM_SOURCE_CACHE=/cpm-cache CCACHE_DIR=/root/.ccache
mkdir -p /workspace
# /workspace is a persistent named volume: rsync copies only files
# that changed since the last run (the macOS<->VM file-share is slow,
# so minimizing reads matters) and ninja can then build incrementally.
# The excludes also shield the persistent build/ dir from --delete.
echo ">> [1/3] Syncing source into the container (delta copy)..."
time rsync -a --delete \
--exclude=/build \
--exclude=/.git \
--exclude=/.ccache \
--exclude=/docs \
--exclude=/notebooks \
--exclude=/IPCToolkitOptions.cmake \
/src/ /workspace/
rm -f /workspace/IPCToolkitOptions.cmake
cd /workspace
echo ">> [2/3] Configuring (preset=${PRESET})..."
cmake --preset="${PRESET}" -G Ninja \
-DCMAKE_CUDA_ARCHITECTURES="${CUDA_ARCH}" \
-DSCALABLE_CCD_CUDA_ARCHITECTURES="${CUDA_ARCH}" \
-DCMAKE_CXX_FLAGS="-Wno-psabi" \
-DCMAKE_CUDA_FLAGS="-Xcompiler=-Wno-psabi"
echo ">> [3/3] Building (-j ${JOBS})..."
cmake --build --preset="${PRESET}" -j "${JOBS}"
'

echo ">> Done. CUDA build succeeded (compile-only; code was not executed)."
53 changes: 53 additions & 0 deletions .devcontainer/cuda/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json",
"name": "IPCToolkit CUDA Development Container",
"build": {
"dockerfile": "Dockerfile",
// Context is the repository root (two levels up from this file) so the
// build sees the whole project, matching the default dev container.
"context": "../.."
},
// On a Linux host with the NVIDIA Container Toolkit, uncomment the next line
// to pass the GPU into the container so the CUDA code can actually run.
// (Leave it commented on macOS/Windows or GPU-less hosts, where it errors.)
// "runArgs": ["--gpus=all"],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.shell.linux": "/bin/zsh",
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
"C_Cpp.default.intelliSenseMode": "gcc-x64",
"C_Cpp.default.compilerPath": "/usr/bin/clang++-18",
"C_Cpp.clang_format_path": "/usr/bin/clang-format-18",
"C_Cpp.clang_format_style": "file",
"cmake.configureOnOpen": false,
"cmake.buildDirectory": "${workspaceFolder}/build",
"python.pythonPath": "/usr/bin/python3"
},
"extensions": [
"ms-vscode.cpptools",
"ms-vscode.cmake-tools",
"xaver.clang-format",
"ms-python.python",
"ms-azuretools.vscode-docker",
"eamodio.gitlens",
"twxs.cmake",
"jeff-hykin.better-cpp-syntax",
"vadimcn.vscode-lldb",
"ms-python.vscode-pylance"
]
}
},
"postCreateCommand": "pre-commit install",
"remoteUser": "devuser",
"mounts": [
"source=${localWorkspaceFolder}/.ccache,target=/home/devuser/.ccache,type=bind,consistency=cached"
],
"forwardPorts": [],
"remoteEnv": {
"CCACHE_DIR": "/home/devuser/.ccache",
"CCACHE_MAXSIZE": "1G"
},
"workspaceFolder": "/home/devuser/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/home/devuser/workspace,type=bind,consistency=cached"
}
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Shrinks the build context uploaded to the Docker daemon. Both dev containers
# (.devcontainer/Dockerfile and .devcontainer/cuda/Dockerfile) use the repo
# root as their build context but never COPY it in (the workspace is mounted at
# runtime), so this only speeds up context transfer — it does not affect image
# contents.
build/
docs/_build/
*.o
*.so
*.a
__pycache__/
*.pyc
.git/
.venv/
venv/
.DS_Store
31 changes: 31 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,37 @@ cmake --build --preset=python
# or: pip install .
```

### CUDA Builds

CUDA is off by default. The `cuda-release` and `cuda-debug` presets turn it on.
Compiling CUDA needs nvcc but **not** a GPU; only running it needs the GPU.

On a machine with no NVIDIA GPU or no CUDA toolkit — any Mac, for instance —
use the CUDA dev container to check that the CUDA build still compiles. It
builds an nvcc-equipped image and compiles the whole project inside it, then
exits with the build's status:

```bash
./.devcontainer/cuda/build-cuda.sh
```

`PRESET` (default `cuda-release`), `CUDA_ARCH`, and `JOBS` override the
defaults, e.g. `PRESET=test ./.devcontainer/cuda/build-cuda.sh` to build the
CUDA tests too. The source is mounted read-only and rsynced into a named
volume, so a run never writes into your working tree, and later runs are
incremental.

**This needs a running Docker daemon.** On macOS that means Docker Desktop or
a colima VM (`colima start`); the failure mode otherwise is a confusing
`/var/run/docker.sock` connection error rather than a clear diagnostic. A VM
also stops when the machine sleeps, so a script that worked an hour ago may
need the VM restarted.

For interactive work, `.devcontainer/cuda/devcontainer.json` opens the same
image as a VS Code dev container. On a Linux host with the NVIDIA Container
Toolkit, uncomment its `runArgs` to pass the GPU through so the CUDA code can
actually run.

## Running Tests

Tests use Catch2. Test files mirror the source structure: `tests/src/tests/` mirrors `src/ipc/`.
Expand Down
24 changes: 18 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ if(IPC_TOOLKIT_WITH_CUDA)
# library to be built with -dc as the member functions could be called by
# other libraries and executables.
set_target_properties(ipc_toolkit PROPERTIES CUDA_SEPARABLE_COMPILATION ON)

# Device code calls constexpr host functions (std::numeric_limits,
# std::array). PUBLIC so downstream CUDA consumers (e.g. the tests) inherit
# the flag.
target_compile_options(ipc_toolkit PUBLIC
"$<$<COMPILE_LANGUAGE:CUDA>:--expt-relaxed-constexpr>")
endif()

# Fill in configuration options
Expand Down Expand Up @@ -217,6 +223,14 @@ source_group(TREE "${PROJECT_SOURCE_DIR}" FILES ${IPC_TOOLKIT_SOURCES})
include(eigen)
target_link_libraries(ipc_toolkit PUBLIC Eigen3::Eigen)

# Logger
# NOTE: Included before the CCD dependencies on purpose. Scalable CCD carries
# its own spdlog recipe guarded on `if(TARGET spdlog::spdlog)`, so whichever
# runs first wins -- and if that is Scalable CCD's, the unpatched spdlog is the
# one CPM registers and cmake/patches/fmt-nvcc-compat.patch never applies.
include(spdlog)
target_link_libraries(ipc_toolkit PUBLIC spdlog::spdlog)

# libigl
include(libigl)
target_link_libraries(ipc_toolkit PRIVATE igl::core igl::predicates)
Expand All @@ -233,11 +247,6 @@ target_link_libraries(ipc_toolkit PRIVATE tight_inclusion::tight_inclusion)
include(scalable_ccd)
target_link_libraries(ipc_toolkit PRIVATE scalable_ccd::scalable_ccd)


# Logger
include(spdlog)
target_link_libraries(ipc_toolkit PUBLIC spdlog::spdlog)

# TinyAD
include(tinyad)
# TODO: Make this a private dependency once we stop exposing TinyAD types in the public API.
Expand Down Expand Up @@ -307,7 +316,10 @@ if(IPC_TOOLKIT_WITH_SIMD)
# unit's own flags. ipc/utils/simd.hpp exposes batch types in the public API,
# and a consumer compiled without these flags would name a different type
# than the one instantiated in the library, failing to link.
target_compile_options(ipc_toolkit PUBLIC ${SIMD_CXX_FLAGS})
# NOTE: Restricted to C++ sources because these are MSVC-style /arch flags
# that nvcc would otherwise treat as input files, and device code has no use
# for CPU SIMD anyway.
target_compile_options(ipc_toolkit PUBLIC "$<$<COMPILE_LANGUAGE:CXX>:${SIMD_CXX_FLAGS}>")

# Link against cross-platform xsimd library
include(xsimd)
Expand Down
4 changes: 3 additions & 1 deletion cmake/ipc_toolkit/ipc_toolkit_use_colors.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
# > fatal error C1090: PDB API call failed, error code '23'
# To avoid this problem, we force PDB write to be synchronous with /FS.
# https://developercommunity.visualstudio.com/content/problem/48897/c1090-pdb-api-call-failed-error-code-23.html
add_compile_options(/FS)
# Exclude CUDA: nvcc would treat a bare /FS as an input file. CMake already
# forwards /FS to the MSVC host compiler for CUDA targets via -Xcompiler.
add_compile_options($<$<NOT:$<COMPILE_LANGUAGE:CUDA>>:/FS>)
else()
include(ipc_toolkit_filter_flags)
set(IPC_TOOLKIT_GLOBAL_FLAGS
Expand Down
42 changes: 42 additions & 0 deletions cmake/patches/fmt-nvcc-compat.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
diff --git a/include/spdlog/fmt/bundled/base.h b/include/spdlog/fmt/bundled/base.h
index 620456b..0740975 100644
--- a/include/spdlog/fmt/bundled/base.h
+++ b/include/spdlog/fmt/bundled/base.h
@@ -455,7 +455,14 @@ struct is_std_string_like<T, void_t<decltype(std::declval<T>().find_first_of(
const typename T::value_type*> {};

// Check if the literal encoding is UTF-8.
+#if defined(__CUDACC__)
+// NVCC's device front-end (EDG) does not honor the host compiler's /utf-8
+// flag, so the compile-time probe below misfires. The host pass is compiled
+// with /utf-8, so UTF-8 is in fact enabled.
+enum { is_utf8_enabled = 1 };
+#else
enum { is_utf8_enabled = "\u00A7"[1] == '\xA7' };
+#endif
enum { use_utf8 = !FMT_WIN32 || is_utf8_enabled };

#ifndef FMT_UNICODE
diff --git a/include/spdlog/fmt/bundled/format.h b/include/spdlog/fmt/bundled/format.h
index 4a65300..2ce9069 100644
--- a/include/spdlog/fmt/bundled/format.h
+++ b/include/spdlog/fmt/bundled/format.h
@@ -3138,8 +3138,18 @@ constexpr auto fractional_part_rounding_thresholds(int index) -> uint32_t {
// It is equal to ceil(2^31 + 2^32/10^(k + 1)).
// These are stored in a string literal because we cannot have static arrays
// in constexpr functions and non-static ones are poorly optimized.
+#if defined(__CUDACC__)
+ // NVCC's device front-end (EDG) rejects char32_t hex escapes whose value has
+ // the high bit set ("character value is out of range"), so use an equivalent
+ // uint32_t array instead of a UTF-32 string literal.
+ constexpr uint32_t thresholds[] = {0x9999999au, 0x828f5c29u, 0x80418938u,
+ 0x80068db9u, 0x8000a7c6u, 0x800010c7u,
+ 0x800001aeu, 0x8000002bu};
+ return thresholds[index];
+#else
return U"\x9999999a\x828f5c29\x80418938\x80068db9\x8000a7c6\x800010c7"
U"\x800001ae\x8000002b"[index];
+#endif
}

template <typename Float>
2 changes: 1 addition & 1 deletion cmake/recipes/scalable_ccd.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ message(STATUS "Third-party: creating target 'scalable_ccd::scalable_ccd'")

include(CPM)
CPMAddPackage(
URI "gh:continuous-collision-detection/scalable-ccd#c80af01cab083b3eeb8dac80312ec9cfe479a5cf"
URI "gh:continuous-collision-detection/scalable-ccd#8f9347c1afc36f2dda17424c15ff5b68087fe8dc"
OPTIONS "SCALABLE_CCD_WITH_CUDA ${IPC_TOOLKIT_WITH_CUDA}"
)

Expand Down
9 changes: 8 additions & 1 deletion cmake/recipes/spdlog.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ option(SPDLOG_INSTALL "Generate the install target" ON)
set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "spdlog")

include(CPM)
CPMAddPackage("gh:gabime/spdlog@1.17.0")
# The bundled fmt trips up NVCC's device front-end (EDG): a compile-time /utf-8
# probe misfires and a char32_t table uses hex escapes with the high bit set.
# Neither is fixable via compiler flags (the front-end never sees /utf-8), so we
# patch the bundled headers to guard both cases behind __CUDACC__.
CPMAddPackage(
URI "gh:gabime/spdlog@1.17.0"
PATCHES "${CMAKE_CURRENT_LIST_DIR}/../patches/fmt-nvcc-compat.patch"
)

set_target_properties(spdlog PROPERTIES POSITION_INDEPENDENT_CODE ON)

Expand Down
Loading
Loading