11 KiB
CUDA Slicer: GPU-Accelerated Slicing & Polygon Offsetting Library
A high-performance C/C++ and CUDA library with native Python bindings for Linux & Windows providing GPU-accelerated 2D/3D polygon offsetting, Jump Flooding Signed Distance Fields (JFA / SDF), and slicing for 3D printing, CAM, and computational geometry.
🚀 Benchmark Summary (3DBenchy, 239 Layers @ 0.20 mm)
Tested on NVIDIA RTX A4500 Laptop GPU (16 GB VRAM) vs 12th Gen Intel Core i7 CPU:
| Pipeline | Total Time (Mesh \to Offset Contours) |
Slicing / Offsetting Rate | Speedup vs PrusaSlicer |
|---|---|---|---|
| 100% GPU Direct Mesh Slicing (0.10 mm Pitch) | 74.4 ms | 3,212 layers/sec | 57.47x Faster |
| 100% GPU Direct Mesh Slicing (0.05 mm Pitch) | 190.9 ms | 1,252 layers/sec | 22.55x Faster |
| Hybrid CPU Slicing + GPU JFA (0.05 mm Pitch) | 739.4 ms | 323 layers/sec | 5.82x Faster |
| PrusaSlicer CLI Headless (CPU Baseline) | 4,305.7 ms | 55 layers/sec | 1.00x (Baseline) |
⚡ Key Optimizations Applied:
- Stage 6 (Marching Squares Vectorization): Reduced from 72.5 ms down to 35.4 ms (2.05x faster / 6,751 layer/s) via zero-allocation discrete edge-table addressing, thread-local buffer reuse, and on-the-fly collinear point decimation (>95% vertex reduction).
- Stage 5 (D2H Transfer): Reduced from 136.3 ms down to 4.5 ms (30x–48x faster) via GPU 32-bit bit-packing + page-locked pinned host DMA (
cudaMallocHost).- Stage 3 (GPU JFA Distance Field): Reduced from 118.9 ms down to 52.8 ms (2.25x faster) via perimeter-band limited steps and integer arithmetic.
🐍 Python Package Integration
1. Installation
A. Install from Source (Linux & Windows)
Make sure you have NVIDIA CUDA Toolkit (11.5+ or 12.x) and a C++ compiler (GCC on Linux, MSVC 2019/2022 on Windows) installed:
# Clone the repository
cd cudaslicer
# Install directly with pip
pip install .
B. Build Wheel (.whl)
python -m pip install --upgrade build wheel
python -m build --wheel
# The wheel will be generated in dist/
# e.g., dist/cudaslicer-1.0.0-cp310-cp310-linux_x86_64.whl
# or dist/cudaslicer-1.0.0-cp310-cp310-win_amd64.whl
2. Python API Quickstart
A. Backend Selection & GPU Capability Check
import cudaslicer
# 1. Check if a CUDA GPU is present
if cudaslicer.has_cuda():
print("CUDA GPU is active!")
else:
print("Running in multi-threaded CPU compatibility mode.")
# 2. Control compute backend globally ('auto', 'cuda', 'cpu')
cudaslicer.set_backend("auto") # Auto: use CUDA if available, fallback to CPU
B. Offsetting 2D Polygon Contours (Single Layer or Multi-Layer)
import numpy as np
import cudaslicer
# Define a polygon (e.g. 50x50 mm square)
poly = np.array([
[10.0, 10.0],
[60.0, 10.0],
[60.0, 60.0],
[10.0, 60.0]
], dtype=np.float32)
# Generate 2 inward perimeters (at -0.5*w and -1.5*w) and infill mask (at -2.0*w)
# pitch_mm defaults to 0.10 mm, backend defaults to 'auto'
layer = cudaslicer.offset_polygons(
layers=[poly],
perimeters_count=2,
layer_width=0.45,
pitch_mm=0.10,
backend="auto" # 'auto', 'cuda', or 'cpu'
)
# Unpack directly
perimeters, infill_mask = layer
print(f"Outer Perimeter 1 (-0.225mm): {len(perimeters[0])} loop(s)")
print(f"Inner Perimeter 2 (-0.675mm): {len(perimeters[1])} loop(s)")
print(f"Infill Region Mask (-0.900mm): shape {infill_mask.shape}, {infill_mask.sum()} px")
C. Offsetting 2D / 3D Occupancy Masks Directly
import numpy as np
import cudaslicer
# Create a 2D binary grid (e.g., 200 x 200 pixels)
mask = np.zeros((200, 200), dtype=np.uint8)
mask[40:160, 40:160] = 1
# Compute Euclidean perimeters and infill mask
perimeters, infill_mask = cudaslicer.offset_masks(
masks=mask,
perimeters_count=2,
layer_width=0.45,
pitch_mm=0.10,
backend="auto"
)
print("Perimeters shape:", perimeters.shape) # (2, 200, 200)
print("Infill mask shape:", infill_mask.shape) # (200, 200)
D. End-to-End Mesh Slicing with Custom First Layer Height
import trimesh
import cudaslicer
mesh = trimesh.load("benchy.stl")
# 100% GPU Slicing & Perimeter Offsetting
layers = cudaslicer.slice_mesh(
mesh_or_triangles=mesh,
layer_height=0.20,
first_layer_height=0.25, # custom first layer height
perimeters_count=2,
layer_width=0.45,
pitch_mm=0.10 # default 0.10
)
print(f"Sliced {len(layers)} layers in GPU VRAM!")
layer_0 = layers[0] # First layer at Z = 0.25 mm
perims, infill_mask = layer_0
print(f"Layer 0 (Z = {layer_0.z} mm): {len(perims)} perimeters, infill mask: {infill_mask.shape}")
E. GPU Signed Distance Fields (SDF), Boolean CSG & Gyroid Infill
Execute boolean CSG operations (Union, Difference, Intersection, Smooth Blending) and 3D procedural infill directly on GPU SDF stacks with zero CPU memory roundtrips:
import trimesh
import cudaslicer
mesh_a = trimesh.load("benchy.stl")
cutter_box = trimesh.creation.box(extents=(12.0, 60.0, 25.0))
# 1. Direct GPU slice to continuous SDF stacks
sdf_a = cudaslicer.slice_mesh_to_sdf(mesh_a, layer_height=0.20, pitch_mm=0.10)
sdf_b = cudaslicer.slice_mesh_to_sdf(cutter_box, layer_height=0.20, pitch_mm=0.10)
# 2. Boolean CSG subtraction entirely in GPU VRAM
sdf_carved = sdf_a.difference(sdf_b.offset(delta_mm=0.5))
# 3. GPU procedural 3D gyroid infill generation
sdf_infill = sdf_carved.generate_infill(pattern="gyroid", cell_size_mm=4.0, density=0.25)
# 4. Direct vectorization with sub-pixel LERP and RDP polyline simplification
layers = sdf_infill.vectorize(
perimeters_count=2,
layer_width=0.45,
subpixel_lerp=True, # Natural arbitrary angles (no 45° quantization)
simplify_tolerance_mm=0.02 # RDP polyline decimation
)
📐 Sub-Pixel Linear Interpolation (LERP) vs. Standard Marching Squares
The Problem: 45° & 90° Angle Quantization in Standard Marching Squares
Standard discrete Marching Squares (and binary mask polygon extractors) evaluates binary cell states (0 or 1 at each voxel corner). When an edge intersects a boundary, it snaps the intersection point to the geometric midpoint of the grid cell edge (t = 0.5):
Discrete Marching Squares (Snap to t = 0.5):
(x, y+1) +---------+ (x+1, y+1)
| * | <- Midpoint snapping forces line segments to connect
| / | only at (0.5, 0), (0, 0.5), (1, 0.5), etc.
| * | <- Result: angles restricted to 0°, 45°, 90°, 135°!
(x, y) +---------+ (x+1, y)
Because segment vectors between grid edge midpoints are strictly constrained to Cartesian diagonals, diagonal walls and curves are quantized into jagged 45° chamfers and stair-stepped facets.
The Solution: Continuous SDF Linear Interpolation (LERP)
In CUDASlicer, the vectorizer uses continuous floating-point Signed Distance Field (SDF) values (d_0, d_1) at the cell vertices to solve for the exact sub-pixel boundary crossing where the distance field equals the target isovalue (iso\_val = 0.0 for outer perimeter, -d for offset contours):
t = \frac{iso\_val - d_0}{d_1 - d_0}, \quad t \in [0.0, 1.0]
Continuous SDF LERP (Sub-Pixel Placement):
(x, y+1) +---------+ (x+1, y+1)
| |
| * | <- Exact root crossing t = (0 - d0) / (d1 - d0)
| / | <- Arbitrary natural angles (e.g. 12.3°, 37.8°, smooth curves)
(x, y) +-----*---+ (x+1, y)
- Horizontal Edge:
P_x = \text{min\_x} + (x + t) \cdot \text{pitch}, \quad P_y = \text{min\_y} + y \cdot \text{pitch} - Vertical Edge:
P_x = \text{min\_x} + x \cdot \text{pitch}, \quad P_y = \text{min\_y} + (y + t) \cdot \text{pitch}
Since t varies continuously in [0, 1], vertices fall at their true Euclidean geometric boundaries, eliminating jagged 45° artifacts.
Polyline Simplification & Decimation
- On-the-Fly Collinear Decimation: During loop traversal, collinear vertices (
\Delta x_1 \Delta y_2 - \Delta y_1 \Delta x_2 \approx 0) along straight spans are pruned on the fly with zero memory overhead. - Ramer-Douglas-Peucker (RDP) Polyline Simplification: Traversed closed loops are post-processed with
simplify_contour_rdp(loop, tolerance_mm)(default tolerance0.02 mm), removing sub-pixel jitter while preserving smooth curved contours and exact sharp corners.
🛠️ Building Standalone C/C++ Libraries (Windows & Linux)
Linux Build (libcudaslicer_offset.so & .a)
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . -j$(nproc)
Outputs:
- Shared Library:
build/lib/libcudaslicer_offset.so - Static Library:
build/lib/libcudaslicer_offset.a - C Benchmark CLI:
build/bin/gpu_slicer_c_bench
Windows Build (cudaslicer_offset.dll & .lib)
Open x64 Native Tools Command Prompt for VS 2022 and run:
mkdir build
cd build
cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release
Outputs:
- DLL:
build/lib/Release/cudaslicer_offset.dll - Import Lib:
build/lib/Release/cudaslicer_offset.lib - Benchmark:
build/bin/Release/gpu_slicer_c_bench.exe
⚙️ Cross-Platform C API Reference (include/gpu_offset.h)
The public header provides a portable C API with __declspec(dllexport) / __declspec(dllimport) on Windows and default symbol visibility on Linux:
#include "gpu_offset.h"
// 1. Initialize GPU
gpu_offset_init(0);
// 2. Configure slicing & offsetting parameters
GpuOffsetConfig config = {
.pitch_mm = 0.10f, // default 0.10 mm
.pad_mm = 1.5f,
.perimeters_count = 2, // 2 perimeter passes
.layer_width = 0.45f, // 0.45 mm extrusion width
.first_layer_height = 0.25f, // 0.25 mm first layer height
.export_infill_mask = 1, // generate 2D binary infill mask
.use_gpu_rasterizer = 1
};
// 3. 100% Direct GPU Mesh Slicing & Offsetting
GpuLayerResult* results = NULL;
size_t num_layers = 0;
GpuOffsetStats stats = {0};
gpu_offset_slice_mesh_and_offset(
triangles, num_triangles,
min_x, min_y, min_z, max_x, max_y, max_z,
0.20f, 0.25f, &config,
&results, &num_layers, &stats
);
// Access perimeters and infill mask
for (size_t l = 0; l < num_layers; ++l) {
float z = results[l].z_height;
size_t num_perims = results[l].num_perimeters; // 2
GpuContour* infill_cnts = results[l].infill.contours;
uint8_t* infill_mask = results[l].infill.mask;
}
// 4. Cleanup
gpu_offset_free_results(results, num_layers);
gpu_offset_cleanup();
🔄 Automated CI/CD (GitHub Actions)
- Building Linux Python wheels (
manylinux_2_35_x86_64) across Python 3.9, 3.10, 3.11, 3.12. - Building Windows x64 Python wheels (
win_amd64) across Python 3.9, 3.10, 3.11, 3.12. - Building standalone Linux
.soand Windows.dllbinaries as downloadable CI artifacts.
📄 License
MIT License. Free for commercial and research use.