This commit is contained in:
Nicolas Predella 2026-09-25 18:09:19 +03:00
commit 1019a91e34
20 changed files with 4610 additions and 0 deletions

34
.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# Build and artifact directories
build/
bin/
lib/
dist/
*.egg-info/
__pycache__/
*.pyc
*.pyo
*.pyd
# Compiled shared and static libraries / objects
*.so
*.a
*.o
*.obj
*.dylib
*.dll
*.lib
*.exp
# Testing temp tools, AppImages, and experimental benchmarks
tests_and_tools/
# Slicing output artifacts
*.gcode
*.png
# IDE and OS metadata
.vscode/
.idea/
.DS_Store
*.swp
*~

109
CMakeLists.txt Normal file
View file

@ -0,0 +1,109 @@
cmake_minimum_required(VERSION 3.18)
project(cudaslicer LANGUAGES C CXX CUDA)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CUDA_STANDARD 14)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
# Cross-platform compiler settings
if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2 /W3 /MP")
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -O3 --use_fast_math -Xcompiler=/MD,/W3")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -Wall -pthread -fPIC")
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -O3 --use_fast_math -Xcompiler -fPIC,-Wall")
endif()
# Set broad CUDA GPU architecture support (Turing, Ampere, Ada Lovelace, Hopper/Blackwell)
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
set(CMAKE_CUDA_ARCHITECTURES "75;80;86;89")
endif()
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
${CUDA_INCLUDE_DIRS}
)
# -----------------------------------------------------------------------------
# 1. Standalone C/C++ Shared Library (libcudaslicer_offset.so / cudaslicer_offset.dll)
# -----------------------------------------------------------------------------
add_library(cudaslicer_offset SHARED
src/gpu_offset.cu
src/cpu_offset.cpp
)
set_target_properties(cudaslicer_offset PROPERTIES
OUTPUT_NAME "cudaslicer_offset"
CUDA_SEPARABLE_COMPILATION OFF
)
target_compile_definitions(cudaslicer_offset PRIVATE GPU_OFFSET_EXPORTS)
target_link_libraries(cudaslicer_offset PUBLIC cudart)
# -----------------------------------------------------------------------------
# 2. Standalone C/C++ Static Library
# -----------------------------------------------------------------------------
add_library(cudaslicer_offset_static STATIC
src/gpu_offset.cu
src/cpu_offset.cpp
)
set_target_properties(cudaslicer_offset_static PROPERTIES
OUTPUT_NAME "cudaslicer_offset"
CUDA_SEPARABLE_COMPILATION OFF
)
target_link_libraries(cudaslicer_offset_static PUBLIC cudart)
# -----------------------------------------------------------------------------
# 3. C++ Benchmark Binary
# -----------------------------------------------------------------------------
add_executable(gpu_slicer_c_bench
src/main_bench.cpp
src/stl_slicer.cpp
)
target_link_libraries(gpu_slicer_c_bench PRIVATE cudaslicer_offset cudart)
if(UNIX)
target_link_libraries(gpu_slicer_c_bench PRIVATE pthread)
endif()
# -----------------------------------------------------------------------------
# 4. Pybind11 Python Native Extension (_cudaslicer_backend)
# -----------------------------------------------------------------------------
if(NOT DEFINED Python3_EXECUTABLE)
find_program(Python3_EXECUTABLE NAMES python3 python)
endif()
find_package(Python3 COMPONENTS Interpreter REQUIRED)
execute_process(
COMMAND "${Python3_EXECUTABLE}" -c "import sysconfig; print(sysconfig.get_path('include'))"
OUTPUT_VARIABLE _python3_include_dir
OUTPUT_STRIP_TRAILING_WHITESPACE
)
execute_process(
COMMAND "${Python3_EXECUTABLE}" -c "import pybind11; print(pybind11.get_include())"
OUTPUT_VARIABLE _pybind11_include_dir
OUTPUT_STRIP_TRAILING_WHITESPACE
)
execute_process(
COMMAND "${Python3_EXECUTABLE}" -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX') or '.so')"
OUTPUT_VARIABLE _ext_suffix
OUTPUT_STRIP_TRAILING_WHITESPACE
)
add_library(_cudaslicer_backend MODULE
python/cudaslicer/bindings.cpp
)
set_target_properties(_cudaslicer_backend PROPERTIES
PREFIX ""
SUFFIX "${_ext_suffix}"
)
target_include_directories(_cudaslicer_backend PRIVATE "${_python3_include_dir}" "${_pybind11_include_dir}")
target_link_libraries(_cudaslicer_backend PRIVATE cudaslicer_offset_static cudart)
# -----------------------------------------------------------------------------
# Output Directories
# -----------------------------------------------------------------------------
if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(cudaslicer_offset PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib")
set_target_properties(cudaslicer_offset_static PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib")
set_target_properties(gpu_slicer_c_bench PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin")
set_target_properties(_cudaslicer_backend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/python/cudaslicer")
endif()

45
Makefile Normal file
View file

@ -0,0 +1,45 @@
NVCC ?= nvcc
CXX ?= g++
ARCH ?= sm_86
CXXFLAGS = -O3 -std=c++17 -Wall -pthread -fPIC -Iinclude -Isrc
NVCCFLAGS = -O3 -std=c++14 -arch=$(ARCH) --use_fast_math -Xcompiler -fPIC,-Wall -Iinclude -Isrc
LIB_DIR = lib
BIN_DIR = bin
BUILD_DIR = build
SHARED_LIB = $(LIB_DIR)/libcudaslicer_offset.so
STATIC_LIB = $(LIB_DIR)/libcudaslicer_offset.a
BENCH_BIN = $(BIN_DIR)/gpu_slicer_c_bench
all: $(SHARED_LIB) $(STATIC_LIB) $(BENCH_BIN)
$(BUILD_DIR)/gpu_offset.o: src/gpu_offset.cu include/gpu_offset.h src/cpu_offset.h src/vectorizer.h | $(BUILD_DIR)
$(NVCC) $(NVCCFLAGS) -c $< -o $@
$(BUILD_DIR)/cpu_offset.o: src/cpu_offset.cpp src/cpu_offset.h src/vectorizer.h include/gpu_offset.h | $(BUILD_DIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(BUILD_DIR)/stl_slicer.o: src/stl_slicer.cpp src/stl_slicer.h include/gpu_offset.h | $(BUILD_DIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(BUILD_DIR)/main_bench.o: src/main_bench.cpp src/stl_slicer.h include/gpu_offset.h | $(BUILD_DIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(SHARED_LIB): $(BUILD_DIR)/gpu_offset.o $(BUILD_DIR)/cpu_offset.o | $(LIB_DIR)
$(NVCC) -shared -arch=$(ARCH) -o $@ $^ -lcudart
$(STATIC_LIB): $(BUILD_DIR)/gpu_offset.o $(BUILD_DIR)/cpu_offset.o | $(LIB_DIR)
ar rcs $@ $^
$(BENCH_BIN): $(BUILD_DIR)/main_bench.o $(BUILD_DIR)/stl_slicer.o $(SHARED_LIB) | $(BIN_DIR)
$(CXX) $(CXXFLAGS) $(BUILD_DIR)/main_bench.o $(BUILD_DIR)/stl_slicer.o -L$(LIB_DIR) -lcudaslicer_offset -lcudart -Wl,-rpath,'$$ORIGIN/../lib' -o $@
$(BUILD_DIR) $(LIB_DIR) $(BIN_DIR):
mkdir -p $@
clean:
rm -rf $(BUILD_DIR) $(LIB_DIR) $(BIN_DIR)
.PHONY: all clean

231
README.md Normal file
View file

@ -0,0 +1,231 @@
# CUDA Slicer: GPU-Accelerated Slicing & Polygon Offsetting Library
[![Python Version](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue)](https://pypi.org/)
[![CUDA](https://img.shields.io/badge/CUDA-11.x%20%7C%2012.x-green.svg)](https://developer.nvidia.com/cuda-toolkit)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
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:
```bash
# Clone the repository
cd cudaslicer
# Install directly with pip
pip install .
```
#### B. Build Wheel (.whl)
```bash
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
```python
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)
```python
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
```python
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
```python
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}")
```
---
## 🛠️ Building Standalone C/C++ Libraries (Windows & Linux)
### Linux Build (`libcudaslicer_offset.so` & `.a`)
```bash
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:
```cmd
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:
```c
#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 `.so` and Windows `.dll` binaries as downloadable CI artifacts.
---
## 📄 License
MIT License. Free for commercial and research use.

BIN
benchy.stl Normal file

Binary file not shown.

87
examples/c_example.c Normal file
View file

@ -0,0 +1,87 @@
#include "gpu_offset.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main() {
printf("=== C API Example: Linking libcudaslicer_offset.so ===\n");
// 1. Initialize GPU
if (gpu_offset_init(0) != 0) {
fprintf(stderr, "Failed to initialize GPU\n");
return 1;
}
// 2. Create a test slice layer with a square polygon and a circular hole
GpuContour contours[2];
// Square exterior: 20mm x 20mm centered at origin
GpuPoint2D square_pts[4] = {
{ -10.0f, -10.0f },
{ 10.0f, -10.0f },
{ 10.0f, 10.0f },
{ -10.0f, 10.0f }
};
contours[0].points = square_pts;
contours[0].num_points = 4;
contours[0].is_hole = 0;
// Diamond interior hole
GpuPoint2D hole_pts[4] = {
{ 0.0f, -4.0f },
{ 4.0f, 0.0f },
{ 0.0f, 4.0f },
{ -4.0f, 0.0f }
};
contours[1].points = hole_pts;
contours[1].num_points = 4;
contours[1].is_hole = 1;
GpuSliceLayer layer;
layer.z_height = 0.20f;
layer.contours = contours;
layer.num_contours = 2;
// 3. Configure Offsets
float offset_dists[2] = { 0.45f, 0.90f };
GpuOffsetConfig config;
config.pitch_mm = 0.05f;
config.pad_mm = 1.0f;
config.offset_distances_mm = offset_dists;
config.num_offsets = 2;
config.use_gpu_rasterizer = 1;
// 4. Run GPU Offset
GpuLayerResult* results = NULL;
GpuOffsetStats stats = {0};
int ret = gpu_offset_polygon_layers(
&layer, 1,
-10.0f, -10.0f, 10.0f, 10.0f,
&config, &results, &stats
);
if (ret == 0 && results) {
printf("GPU Offset Successful!\n");
printf(" Rasterization: %.3f ms\n", stats.t_raster_ms);
printf(" H2D Transfer: %.3f ms\n", stats.t_h2d_ms);
printf(" GPU JFA Compute: %.3f ms\n", stats.t_gpu_jfa_ms);
printf(" D2H Transfer: %.3f ms\n", stats.t_d2h_ms);
printf(" Vectorization: %.3f ms\n", stats.t_vectorize_ms);
printf(" Total Time: %.3f ms\n", stats.t_total_pipeline_ms);
for (size_t o = 0; o < results[0].num_perimeters; ++o) {
printf(" Perimeter %zu (Offset -%.2f mm): %zu vector loops\n",
o + 1, results[0].perimeters[o].offset_distance_mm,
results[0].perimeters[o].num_contours);
}
gpu_offset_free_results(results, 1);
} else {
fprintf(stderr, "GPU Offset failed!\n");
}
gpu_offset_cleanup();
printf("=== Done ===\n");
return 0;
}

View file

@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""
Python ctypes wrapper demonstrating how to load and call libcudaslicer_offset.so
"""
import ctypes
import os
import numpy as np
# Load shared library
lib_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'lib', 'libcudaslicer_offset.so'))
lib = ctypes.CDLL(lib_path)
# Define C structures
class GpuPoint2D(ctypes.Structure):
_fields_ = [("x", ctypes.c_float), ("y", ctypes.c_float)]
class GpuContour(ctypes.Structure):
_fields_ = [
("points", ctypes.POINTER(GpuPoint2D)),
("num_points", ctypes.c_size_t),
("is_hole", ctypes.c_int)
]
class GpuSliceLayer(ctypes.Structure):
_fields_ = [
("contours", ctypes.POINTER(GpuContour)),
("num_contours", ctypes.c_size_t),
("z_height", ctypes.c_float)
]
class GpuOffsetPerimeter(ctypes.Structure):
_fields_ = [
("contours", ctypes.POINTER(GpuContour)),
("num_contours", ctypes.c_size_t),
("offset_distance_mm", ctypes.c_float)
]
class GpuLayerResult(ctypes.Structure):
_fields_ = [
("perimeters", ctypes.POINTER(GpuOffsetPerimeter)),
("num_perimeters", ctypes.c_size_t),
("z_height", ctypes.c_float)
]
class GpuOffsetConfig(ctypes.Structure):
_fields_ = [
("pitch_mm", ctypes.c_float),
("pad_mm", ctypes.c_float),
("offset_distances_mm", ctypes.POINTER(ctypes.c_float)),
("num_offsets", ctypes.c_size_t),
("use_gpu_rasterizer", ctypes.c_int)
]
class GpuOffsetStats(ctypes.Structure):
_fields_ = [
("t_mesh_slice_ms", ctypes.c_double),
("t_raster_ms", ctypes.c_double),
("t_h2d_ms", ctypes.c_double),
("t_gpu_jfa_ms", ctypes.c_double),
("t_gpu_thresh_ms", ctypes.c_double),
("t_d2h_ms", ctypes.c_double),
("t_vectorize_ms", ctypes.c_double),
("t_total_pipeline_ms", ctypes.c_double),
]
# Setup function signatures
lib.gpu_offset_init.argtypes = [ctypes.c_int]
lib.gpu_offset_init.restype = ctypes.c_int
lib.gpu_offset_cleanup.argtypes = []
lib.gpu_offset_cleanup.restype = None
lib.gpu_offset_polygon_layers.argtypes = [
ctypes.POINTER(GpuSliceLayer),
ctypes.c_size_t,
ctypes.c_float, ctypes.c_float, ctypes.c_float, ctypes.c_float,
ctypes.POINTER(GpuOffsetConfig),
ctypes.POINTER(ctypes.POINTER(GpuLayerResult)),
ctypes.POINTER(GpuOffsetStats)
]
lib.gpu_offset_polygon_layers.restype = ctypes.c_int
lib.gpu_offset_free_results.argtypes = [ctypes.POINTER(GpuLayerResult), ctypes.c_size_t]
lib.gpu_offset_free_results.restype = None
def main():
print(f"Loaded {lib_path}")
lib.gpu_offset_init(0)
# Create a 20x20mm square
square_pts = (GpuPoint2D * 4)(
GpuPoint2D(-10.0, -10.0),
GpuPoint2D( 10.0, -10.0),
GpuPoint2D( 10.0, 10.0),
GpuPoint2D(-10.0, 10.0)
)
cnt = GpuContour(square_pts, 4, 0)
layer = GpuSliceLayer((GpuContour * 1)(cnt), 1, 0.20)
offsets = (ctypes.c_float * 2)(0.45, 0.90)
cfg = GpuOffsetConfig(0.05, 1.0, offsets, 2, 1)
results_ptr = ctypes.POINTER(GpuLayerResult)()
stats = GpuOffsetStats()
ret = lib.gpu_offset_polygon_layers(
ctypes.byref(layer), 1,
-10.0, -10.0, 10.0, 10.0,
ctypes.byref(cfg),
ctypes.byref(results_ptr),
ctypes.byref(stats)
)
if ret == 0 and results_ptr:
res = results_ptr[0]
print(f"C Library returned {res.num_perimeters} perimeters in {stats.t_total_pipeline_ms:.2f} ms")
for o in range(res.num_perimeters):
perim = res.perimeters[o]
print(f" Perimeter {o+1} (-{perim.offset_distance_mm:.2f} mm): {perim.num_contours} contours")
lib.gpu_offset_free_results(results_ptr, 1)
lib.gpu_offset_cleanup()
if __name__ == '__main__':
main()

View file

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
CUDASlicer Python Quickstart & Integration Example
=================================================
Demonstrates how to use `cudaslicer` in a Python project with:
- first_layer_height
- perimeters_count and layer_width (Perimeter 1: -0.5w, Perimeter 2: -1.5w, etc.)
- default pitch_mm = 0.10
- outputs: perimeters and infill_mask (-perimeters_count * layer_width)
"""
import time
import numpy as np
import trimesh
import cudaslicer
def example_mask_offsetting():
print("--- 1. GPU Mask Offsetting ---")
# Create a 200x200 test grid with a square
grid = np.zeros((200, 200), dtype=np.uint8)
grid[50:150, 50:150] = 1
t0 = time.perf_counter()
# Generate 2 perimeters and infill mask with layer_width=0.45 mm, default pitch_mm=0.10 mm
perimeters, infill_mask = cudaslicer.offset_masks(
masks=grid,
perimeters_count=2,
layer_width=0.45,
pitch_mm=0.10 # default is 0.10
)
t1 = time.perf_counter()
print(f"Mask offset completed in {(t1 - t0)*1000:.3f} ms")
print(f"Original area (pixels): {grid.sum()}")
print(f"Perimeter 1 (-0.5*width = -0.225mm) area (pixels): {perimeters[0].sum()}")
print(f"Perimeter 2 (-1.5*width = -0.675mm) area (pixels): {perimeters[1].sum()}")
print(f"Infill mask (-2.0*width = -0.900mm) area (pixels): {infill_mask.sum()}\n")
def example_polygon_offsetting():
print("--- 2. Single Layer Polygon Offsetting ---")
# Define an L-shaped polygon
l_poly = np.array([
[0.0, 0.0],
[30.0, 0.0],
[30.0, 10.0],
[10.0, 10.0],
[10.0, 30.0],
[0.0, 30.0]
], dtype=np.float32)
t0 = time.perf_counter()
layer_res = cudaslicer.offset_polygons(
layers=[l_poly],
perimeters_count=2,
layer_width=0.45,
pitch_mm=0.10,
pad_mm=1.0,
return_infill_mask=True
)
t1 = time.perf_counter()
perimeters, infill_mask = layer_res # Unpack directly!
print(f"Polygon offsetting completed in {(t1 - t0)*1000:.3f} ms")
print(f" Perimeter 1 (-0.225mm): {len(perimeters[0])} loop(s), vertex count: {[len(c) for c in perimeters[0]]}")
print(f" Perimeter 2 (-0.675mm): {len(perimeters[1])} loop(s), vertex count: {[len(c) for c in perimeters[1]]}")
print(f" Infill contours (-0.900mm): {len(layer_res.infill_contours)} loop(s), vertex count: {[len(c) for c in layer_res.infill_contours]}")
if infill_mask is not None:
print(f" Infill mask raster shape: {infill_mask.shape}, active pixels: {infill_mask.sum()}")
print()
def example_mesh_slicing_and_offset():
print("--- 3. 100% GPU Direct 3D Mesh Slicing & Offsetting ---")
mesh = trimesh.load("benchy.stl")
print(f"Loaded mesh with {len(mesh.faces)} triangles. Running 100% GPU Slicing pipeline...")
t0 = time.perf_counter()
layers = cudaslicer.slice_mesh_and_offset(
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
pad_mm=1.5,
return_infill_mask=True
)
t1 = time.perf_counter()
total_ms = (t1 - t0) * 1000
print(f"100% GPU Slicing & Offsetting done in {total_ms:.2f} ms ({len(layers)} layers)")
print(f"Throughput: {len(layers) / (total_ms / 1000.0):.1f} layers/second!")
# Inspect first layer and a middle layer
layer_0 = layers[0]
print(f"\nLayer 0 (Z = {layer_0.z:.2f} mm - First Layer Height):")
perims_0, infill_mask_0 = layer_0
print(f" Perimeters: {len(perims_0)} passes (Outer: {len(perims_0[0])} loops, Inner: {len(perims_0[1])} loops)")
print(f" Infill Mask: {infill_mask_0.shape}, Infill Contours: {len(layer_0.infill_contours)} loop(s)")
mid_idx = len(layers) // 2
layer_mid = layers[mid_idx]
print(f"Layer {mid_idx} (Z = {layer_mid.z:.2f} mm):")
perims_mid, infill_mask_mid = layer_mid
print(f" Perimeters: {len(perims_mid)} passes (Outer: {len(perims_mid[0])} loops, Inner: {len(perims_mid[1])} loops)")
print(f" Infill Mask: {infill_mask_mid.shape}, Infill Contours: {len(layer_mid.infill_contours)} loop(s)\n")
if __name__ == "__main__":
if not cudaslicer.is_available():
print("Error: CUDA GPU is not available.")
exit(1)
print(f"CUDASlicer Version: {cudaslicer.__version__}")
example_mask_offsetting()
example_polygon_offsetting()
example_mesh_slicing_and_offset()

212
include/gpu_offset.h Normal file
View file

@ -0,0 +1,212 @@
#ifndef GPU_OFFSET_H
#define GPU_OFFSET_H
#ifdef _WIN32
#ifdef GPU_OFFSET_EXPORTS
#define GPU_OFFSET_API __declspec(dllexport)
#else
#define GPU_OFFSET_API __declspec(dllimport)
#endif
#else
#define GPU_OFFSET_API __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
#include <stdint.h>
/**
* @brief Status and Error Codes
*/
#define GPU_OFFSET_SUCCESS 0
#define GPU_OFFSET_ERR_GENERIC -1
#define GPU_OFFSET_ERR_NO_CUDA -2
#define GPU_OFFSET_ERR_INVALID_ARG -3
/**
* @brief Compute Backend Modes
*/
typedef enum {
GPU_OFFSET_BACKEND_AUTO = 0, // Auto: use CUDA if available, fallback to CPU
GPU_OFFSET_BACKEND_CUDA = 1, // Require CUDA: returns GPU_OFFSET_ERR_NO_CUDA if unavailable
GPU_OFFSET_BACKEND_CPU = 2 // Compatibility mode: force multi-threaded CPU execution
} GpuOffsetBackend;
/**
* @brief 2D Point structure (floating-point millimeters)
*/
typedef struct {
float x;
float y;
} GpuPoint2D;
/**
* @brief 2D Polygon Contour Loop
*/
typedef struct {
GpuPoint2D* points;
size_t num_points;
int is_hole; // 0 = exterior boundary, 1 = interior hole
} GpuContour;
/**
* @brief Sliced Layer containing one or more planar polygon loops
*/
typedef struct {
GpuContour* contours;
size_t num_contours;
float z_height;
} GpuSliceLayer;
/**
* @brief Resulting Offset Contours for a single offset perimeter
*/
typedef struct {
GpuContour* contours;
size_t num_contours;
float offset_distance_mm;
} GpuOffsetPerimeter;
/**
* @brief Infill region mask and boundary contours for a single sliced layer
*/
typedef struct {
GpuContour* contours; // Infill boundary contours (at -perimeters_count * layer_width)
size_t num_contours;
uint8_t* mask; // 2D binary raster mask (width * height uint8), or NULL if not exported
int width;
int height;
float offset_distance_mm; // Offset distance for infill (perimeters_count * layer_width)
} GpuInfillRegion;
/**
* @brief All offset perimeters and infill mask for a single sliced layer
*/
typedef struct {
GpuOffsetPerimeter* perimeters; // Array of perimeters (length = num_perimeters)
size_t num_perimeters; // perimeters_count
GpuInfillRegion infill; // Infill boundary contours & 2D mask
float z_height;
} GpuLayerResult;
/**
* @brief Configuration parameters for Offsetting & Slicing Engine
*/
typedef struct {
float pitch_mm; // Pixel raster pitch (default: 0.10 mm)
float pad_mm; // Bounding box padding margin in mm (default: 1.5 mm)
int perimeters_count; // Number of perimeter passes (e.g. 2, default: 2)
float layer_width; // Line/extrusion width in mm (e.g. 0.45 mm, default: 0.45 mm)
const float* offset_distances_mm; // Optional custom offset distances (if NULL, auto-generated from perimeters_count & layer_width)
size_t num_offsets; // Total offsets (auto-set if offset_distances_mm is NULL)
int use_gpu_rasterizer; // 1 = GPU-accelerated rasterization (when CUDA enabled), 0 = CPU multi-threaded
float first_layer_height; // First layer height in mm (default: 0.0f -> uses layer_height)
int export_infill_mask; // 1 = export binary 2D infill mask into GpuLayerResult (default: 1)
int backend; // GpuOffsetBackend: 0 = AUTO, 1 = CUDA, 2 = CPU (default: 0)
} GpuOffsetConfig;
/**
* @brief Microsecond-accurate timing breakdown
*/
typedef struct {
double t_mesh_slice_ms;
double t_raster_ms;
double t_h2d_ms;
double t_gpu_jfa_ms;
double t_gpu_thresh_ms;
double t_d2h_ms;
double t_vectorize_ms;
double t_total_pipeline_ms;
} GpuOffsetStats;
/**
* @brief Returns 1 if a functional CUDA-capable device is detected, 0 otherwise.
*/
GPU_OFFSET_API int gpu_offset_has_cuda(void);
/**
* @brief Set global default backend (0 = AUTO, 1 = CUDA, 2 = CPU).
*/
GPU_OFFSET_API int gpu_offset_set_backend(int backend);
/**
* @brief Get global default backend (0 = AUTO, 1 = CUDA, 2 = CPU).
*/
GPU_OFFSET_API int gpu_offset_get_backend(void);
/**
* @brief Initialize GPU device and allocate CUDA stream / warm up kernels.
*/
GPU_OFFSET_API int gpu_offset_init(int device_id);
/**
* @brief Core Distance Field & Offsetting on raw 2D/3D binary masks (CUDA or CPU fallback).
*/
GPU_OFFSET_API int gpu_offset_compute_masks(
const uint8_t* h_masks,
int width,
int height,
int num_layers,
float pitch_mm,
const float* offset_distances,
int num_offsets,
uint8_t* h_out_masks,
GpuOffsetStats* stats
);
/**
* @brief 3D Triangle structure for direct mesh slicing (vertices in millimeters)
*/
typedef struct {
float v0[3];
float v1[3];
float v2[3];
} GpuTriangle3D;
/**
* @brief End-to-end polygon offsetting: Polygons -> Raster -> Distance Field -> Vector Contours & Infill
*/
GPU_OFFSET_API int gpu_offset_polygon_layers(
const GpuSliceLayer* in_layers,
size_t num_layers,
float min_x, float min_y, float max_x, float max_y,
const GpuOffsetConfig* config,
GpuLayerResult** out_results,
GpuOffsetStats* stats
);
/**
* @brief End-to-End Mesh Slicing & Perimeter Offsetting:
* 3D Triangles -> Mesh Slicer -> Distance Field -> Vector Contours & Infill.
*/
GPU_OFFSET_API int gpu_offset_slice_mesh_and_offset(
const GpuTriangle3D* triangles,
size_t num_triangles,
float min_x, float min_y, float min_z,
float max_x, float max_y, float max_z,
float layer_height,
float first_layer_height,
const GpuOffsetConfig* config,
GpuLayerResult** out_results,
size_t* out_num_layers,
GpuOffsetStats* stats
);
/**
* @brief Free memory allocated for GpuLayerResult
*/
GPU_OFFSET_API void gpu_offset_free_results(GpuLayerResult* results, size_t num_layers);
/**
* @brief Release GPU context and cached buffers
*/
GPU_OFFSET_API void gpu_offset_cleanup();
#ifdef __cplusplus
}
#endif
#endif // GPU_OFFSET_H

13
pyproject.toml Normal file
View file

@ -0,0 +1,13 @@
[build-system]
requires = ["setuptools>=61.0", "wheel", "pybind11>=2.10.0", "cmake>=3.18"]
build-backend = "setuptools.build_meta"
[project]
name = "cudaslicer"
version = "1.0.0"
description = "GPU-Accelerated 2D/3D Slicing & Polygon Offsetting Library for Linux & Windows"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"numpy>=1.20.0"
]

View file

@ -0,0 +1,363 @@
"""
CUDASlicer: High-Performance Slicing & Polygon Offsetting Library
================================================================
Provides ultra-fast GPU distance-field (SDF/JFA) & multi-threaded CPU
polygon offsetting, binary mask transformations, and 3D mesh slicing
with automatic fallback and dynamic backend switching.
"""
from typing import List, Dict, Union, Any, Optional, Sequence, Tuple
import numpy as np
__version__ = "1.1.0"
# Try importing the compiled C++/CUDA pybind11 module
try:
from . import _cudaslicer_backend as _backend
_HAS_BACKEND = True
except ImportError:
_backend = None
_HAS_BACKEND = False
class LayerResult(dict):
"""
Dictionary holding slicing and offsetting results for a single layer.
Supports dictionary access, attribute access, and tuple unpacking:
`perimeters, infill_mask = layer`
"""
def __getattr__(self, key: str) -> Any:
try:
return self[key]
except KeyError:
raise AttributeError(f"'LayerResult' object has no attribute '{key}'")
def __setattr__(self, key: str, value: Any) -> None:
self[key] = value
def __iter__(self):
# Enables direct unpacking: perimeters, infill_mask = layer
yield self.get("perimeters", [])
yield self.get("infill_mask", None)
def has_cuda() -> bool:
"""
Check if a functional CUDA GPU device is available on this system.
Returns
-------
bool
True if CUDA-capable GPU is available, False otherwise.
"""
if not _HAS_BACKEND or _backend is None:
return False
try:
return bool(_backend.has_cuda())
except Exception:
return False
def is_cuda_available() -> bool:
"""Alias for has_cuda(). Returns True if CUDA GPU is functional."""
return has_cuda()
def is_available() -> bool:
"""
Check if the CUDASlicer backend (CUDA or CPU fallback) is available.
Returns
-------
bool
True if the native backend is functional.
"""
if not _HAS_BACKEND or _backend is None:
return False
try:
ret = _backend.init(0)
return ret == 0
except Exception:
return False
def get_cuda_device_count() -> int:
"""Return the number of available CUDA GPUs."""
return 1 if has_cuda() else 0
def set_backend(backend: str = "auto") -> None:
"""
Set the global default compute backend.
Parameters
----------
backend : str, default='auto'
Execution backend: 'auto' (use CUDA if available, fallback to CPU),
'cuda' (require CUDA; throws error if unavailable), or 'cpu' (compatibility mode).
"""
if not _HAS_BACKEND or _backend is None:
raise RuntimeError("CUDASlicer native backend extension is not loaded.")
_backend.set_backend(str(backend).lower())
def get_backend() -> str:
"""
Get the active global compute backend ('auto', 'cuda', or 'cpu').
"""
if not _HAS_BACKEND or _backend is None:
return "none"
return _backend.get_backend()
def offset_masks(
masks: np.ndarray,
perimeters_count: int = 2,
layer_width: float = 0.45,
pitch_mm: float = 0.10,
offset_distances: Optional[Sequence[float]] = None,
backend: Optional[str] = "auto"
) -> Tuple[np.ndarray, np.ndarray]:
"""
Compute Euclidean offset perimeter masks and infill mask (CUDA or CPU).
Parameters
----------
masks : np.ndarray
2D (H, W) or 3D (N, H, W) binary occupancy grid (uint8 or bool).
perimeters_count : int, default=2
Number of perimeter passes (Perimeter 1 at -0.5*width, Perimeter 2 at -1.5*width, etc.).
layer_width : float, default=0.45
Extrusion line width in millimeters.
pitch_mm : float, default=0.10
Grid pixel pitch in millimeters.
offset_distances : sequence of float, optional
Custom offset distances. If provided, overrides perimeters_count and layer_width.
backend : str, optional, default='auto'
Compute backend: 'auto', 'cuda', or 'cpu'.
Returns
-------
perimeters_masks : np.ndarray
Shape (perimeters_count, H, W) or (perimeters_count, N, H, W).
infill_mask : np.ndarray
Shape (H, W) or (N, H, W) at -perimeters_count * layer_width.
"""
if not _HAS_BACKEND or _backend is None:
raise RuntimeError("CUDASlicer backend extension (_cudaslicer_backend) is not available.")
masks_uint8 = np.ascontiguousarray(masks, dtype=np.uint8)
offsets_float = [float(d) for d in offset_distances] if offset_distances is not None else []
backend_str = str(backend) if backend is not None else "auto"
out_masks = _backend.offset_masks(
masks_uint8,
int(perimeters_count),
float(layer_width),
float(pitch_mm),
offsets_float,
backend_str
)
if offset_distances is not None:
return out_masks
# Separate perimeters and infill mask
perimeters_masks = out_masks[:perimeters_count]
infill_mask = out_masks[perimeters_count]
return perimeters_masks, infill_mask
def offset_polygons(
layers: Union[List[np.ndarray], List[Dict[str, Any]]],
perimeters_count: int = 2,
layer_width: float = 0.45,
pitch_mm: float = 0.10,
pad_mm: float = 1.5,
return_infill_mask: bool = True,
offset_distances: Optional[Sequence[float]] = None,
backend: Optional[str] = "auto"
) -> Union[LayerResult, List[LayerResult]]:
"""
Offset 2D polygon loops across one or more layers (CUDA or CPU fallback).
Parameters
----------
layers : list of np.ndarray OR list of dict
Input layer data. Can be:
1. Single layer with a list of (K, 2) NumPy polygon contours:
`[poly1, poly2, ...]`
2. Multi-layer list of layer dicts:
`[{"z": 0.2, "contours": [poly1, ...]}, {"z": 0.4, "contours": [...]}]`
perimeters_count : int, default=2
Number of perimeter passes (Perimeter 1: -0.5*width, Perimeter 2: -1.5*width, etc.).
layer_width : float, default=0.45
Extrusion line width in millimeters.
pitch_mm : float, default=0.10
Pixel raster pitch in millimeters.
pad_mm : float, default=1.5
Bounding box padding around the contours in millimeters.
return_infill_mask : bool, default=True
Whether to export 2D binary infill occupancy grid.
offset_distances : sequence of float, optional
Custom offset distances.
backend : str, optional, default='auto'
Compute backend: 'auto', 'cuda', or 'cpu'.
Returns
-------
LayerResult or list of LayerResult
If single layer: returns single LayerResult object.
If multi-layer: returns list of LayerResult objects.
"""
if not _HAS_BACKEND or _backend is None:
raise RuntimeError("CUDASlicer backend extension (_cudaslicer_backend) is not available.")
if not layers:
return []
is_single_layer_format = False
if isinstance(layers[0], (np.ndarray, list, tuple)) and not isinstance(layers[0], dict):
is_single_layer_format = True
formatted_layers = [{"z": 0.0, "contours": [np.ascontiguousarray(c, dtype=np.float32) for c in layers]}]
else:
formatted_layers = []
for l in layers:
z_val = float(l.get("z", l.get("z_height", 0.0)))
cnts = [np.ascontiguousarray(c, dtype=np.float32) for c in l.get("contours", [])]
formatted_layers.append({"z": z_val, "contours": cnts})
all_pts = []
for l in formatted_layers:
for c in l["contours"]:
if len(c) >= 3:
all_pts.append(c)
if not all_pts:
empty_res = LayerResult({"z": 0.0, "perimeters": [[] for _ in range(perimeters_count)], "infill_contours": [], "infill_mask": None})
return empty_res if is_single_layer_format else []
stacked = np.concatenate(all_pts, axis=0)
min_x, min_y = float(stacked[:, 0].min()), float(stacked[:, 1].min())
max_x, max_y = float(stacked[:, 0].max()), float(stacked[:, 1].max())
offsets_float = [float(d) for d in offset_distances] if offset_distances is not None else []
backend_str = str(backend) if backend is not None else "auto"
raw_results = _backend.offset_polygon_layers(
formatted_layers, min_x, min_y, max_x, max_y,
int(perimeters_count), float(layer_width),
float(pitch_mm), float(pad_mm),
bool(return_infill_mask), offsets_float,
backend_str
)
layer_results = [LayerResult(r) for r in raw_results]
if is_single_layer_format and layer_results:
return layer_results[0]
return layer_results
def slice_mesh_and_offset(
mesh_or_triangles: Any,
layer_height: float = 0.20,
first_layer_height: Optional[float] = None,
perimeters_count: int = 2,
layer_width: float = 0.45,
pitch_mm: float = 0.10,
pad_mm: float = 1.5,
return_infill_mask: bool = True,
offset_distances: Optional[Sequence[float]] = None,
backend: Optional[str] = "auto"
) -> List[LayerResult]:
"""
3D mesh slicing, rasterization, distance field, and perimeter offsetting (CUDA or CPU).
Parameters
----------
mesh_or_triangles : trimesh.Trimesh OR np.ndarray
Either a trimesh.Trimesh object, or a NumPy array of shape (N, 3, 3) or (N, 9) of triangle vertices.
layer_height : float, default=0.20
Z slicing layer height in millimeters.
first_layer_height : float, optional
First layer height in millimeters. Defaults to `layer_height` if None or <= 0.
perimeters_count : int, default=2
Number of perimeter passes (Perimeter 1: -0.5*width, Perimeter 2: -1.5*width, etc.).
layer_width : float, default=0.45
Extrusion line width in millimeters.
pitch_mm : float, default=0.10
Grid pixel pitch in millimeters.
pad_mm : float, default=1.5
Bounding box padding margin in millimeters.
return_infill_mask : bool, default=True
Whether to generate the 2D binary infill occupancy grid.
offset_distances : sequence of float, optional
Custom offset distances.
backend : str, optional, default='auto'
Compute backend: 'auto' (CUDA if available, else CPU), 'cuda' (require CUDA), or 'cpu'.
Returns
-------
list of LayerResult
List of LayerResult dictionaries containing:
- 'z': Layer Z height
- 'perimeters': List of perimeters (each containing list of (K, 2) contour arrays)
- 'infill_contours': Infill boundary contour loops
- 'infill_mask': 2D binary NumPy array (H, W) of the infill region
"""
if not _HAS_BACKEND or _backend is None:
raise RuntimeError("CUDASlicer backend extension (_cudaslicer_backend) is not available.")
# Handle Trimesh object
if hasattr(mesh_or_triangles, "triangles"):
tris = np.ascontiguousarray(mesh_or_triangles.triangles, dtype=np.float32)
bounds = mesh_or_triangles.bounds
min_x, min_y, min_z = float(bounds[0][0]), float(bounds[0][1]), float(bounds[0][2])
max_x, max_y, max_z = float(bounds[1][0]), float(bounds[1][1]), float(bounds[1][2])
else:
tris = np.ascontiguousarray(mesh_or_triangles, dtype=np.float32)
if tris.ndim == 2 and tris.shape[1] == 9:
tris_3d = tris.reshape(-1, 3, 3)
else:
tris_3d = tris
min_x = float(tris_3d[:, :, 0].min())
max_x = float(tris_3d[:, :, 0].max())
min_y = float(tris_3d[:, :, 1].min())
max_y = float(tris_3d[:, :, 1].max())
min_z = float(tris_3d[:, :, 2].min())
max_z = float(tris_3d[:, :, 2].max())
h_first = float(first_layer_height) if first_layer_height is not None and first_layer_height > 0 else float(layer_height)
offsets_float = [float(d) for d in offset_distances] if offset_distances is not None else []
backend_str = str(backend) if backend is not None else "auto"
raw_results = _backend.slice_mesh_and_offset(
tris, min_x, min_y, min_z, max_x, max_y, max_z,
float(layer_height), float(h_first),
int(perimeters_count), float(layer_width),
float(pitch_mm), float(pad_mm),
bool(return_infill_mask), offsets_float,
backend_str
)
return [LayerResult(r) for r in raw_results]
# Alias for convenience
slice_mesh = slice_mesh_and_offset
__all__ = [
"offset_masks",
"offset_polygons",
"slice_mesh_and_offset",
"slice_mesh",
"LayerResult",
"is_available",
"has_cuda",
"is_cuda_available",
"set_backend",
"get_backend",
"get_cuda_device_count",
"__version__",
]

View file

@ -0,0 +1,420 @@
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <pybind11/stl.h>
#include "gpu_offset.h"
#include <vector>
#include <iostream>
#include <string>
namespace py = pybind11;
static int parse_backend_str(const std::string& backend) {
if (backend.empty() || backend == "auto" || backend == "AUTO") return GPU_OFFSET_BACKEND_AUTO;
if (backend == "cuda" || backend == "gpu" || backend == "CUDA" || backend == "GPU") return GPU_OFFSET_BACKEND_CUDA;
if (backend == "cpu" || backend == "compatibility" || backend == "CPU") return GPU_OFFSET_BACKEND_CPU;
throw std::invalid_argument("Invalid backend '" + backend + "'. Allowed options: 'auto', 'cuda', 'cpu'.");
}
// 1. Direct NumPy Mask Offsetting
py::array_t<uint8_t> py_offset_masks(
py::array_t<uint8_t, py::array::c_style | py::array::forcecast> in_masks,
int perimeters_count,
float layer_width,
float pitch_mm,
const std::vector<float>& offset_distances,
const std::string& backend
) {
py::buffer_info buf = in_masks.request();
int num_layers = 1;
int height = 0;
int width = 0;
if (buf.ndim == 2) {
num_layers = 1;
height = (int)buf.shape[0];
width = (int)buf.shape[1];
} else if (buf.ndim == 3) {
num_layers = (int)buf.shape[0];
height = (int)buf.shape[1];
width = (int)buf.shape[2];
} else {
throw std::runtime_error("Input mask must be a 2D (H, W) or 3D (N, H, W) numpy array.");
}
std::vector<float> resolved_offsets;
if (!offset_distances.empty()) {
resolved_offsets = offset_distances;
} else {
int P = (perimeters_count >= 0) ? perimeters_count : 2;
float W = (layer_width > 0.0f) ? layer_width : 0.45f;
resolved_offsets.resize(P + 1);
for (int i = 0; i < P; ++i) {
resolved_offsets[i] = (i + 0.5f) * W;
}
resolved_offsets[P] = (float)P * W;
}
int num_offsets = (int)resolved_offsets.size();
if (num_offsets == 0) {
throw std::runtime_error("offset distances cannot be empty.");
}
std::vector<ssize_t> out_shape;
if (buf.ndim == 2) {
out_shape = { (ssize_t)num_offsets, (ssize_t)height, (ssize_t)width };
} else {
out_shape = { (ssize_t)num_offsets, (ssize_t)num_layers, (ssize_t)height, (ssize_t)width };
}
py::array_t<uint8_t> out_masks(out_shape);
py::buffer_info out_buf = out_masks.request();
const uint8_t* ptr_in = static_cast<const uint8_t*>(buf.ptr);
uint8_t* ptr_out = static_cast<uint8_t*>(out_buf.ptr);
int req_backend = parse_backend_str(backend);
int prev_backend = gpu_offset_get_backend();
if (!backend.empty()) {
gpu_offset_set_backend(req_backend);
}
GpuOffsetStats stats = {0};
int ret = gpu_offset_compute_masks(
ptr_in, width, height, num_layers, pitch_mm,
resolved_offsets.data(), num_offsets, ptr_out, &stats
);
if (!backend.empty()) {
gpu_offset_set_backend(prev_backend);
}
if (ret == GPU_OFFSET_ERR_NO_CUDA) {
throw std::runtime_error("CUDA backend was requested, but no functional CUDA-capable device was found.");
} else if (ret != GPU_OFFSET_SUCCESS) {
throw std::runtime_error("Offset computation failed in compute backend.");
}
return out_masks;
}
// 2. Direct Python Polygon Offsetting
py::list py_offset_polygon_layers(
py::list py_layers,
float min_x, float min_y, float max_x, float max_y,
int perimeters_count,
float layer_width,
float pitch_mm,
float pad_mm,
bool export_infill_mask,
const std::vector<float>& offset_distances,
const std::string& backend
) {
size_t num_layers = py_layers.size();
if (num_layers == 0) return py::list();
std::vector<GpuSliceLayer> c_layers(num_layers);
std::vector<std::vector<GpuContour>> c_contours_pool(num_layers);
std::vector<std::vector<std::vector<GpuPoint2D>>> c_points_pool(num_layers);
for (size_t l = 0; l < num_layers; ++l) {
py::dict layer_dict = py_layers[l].cast<py::dict>();
float z_h = layer_dict.contains("z") ? layer_dict["z"].cast<float>() : 0.0f;
py::list cnt_list = layer_dict["contours"].cast<py::list>();
size_t num_cnts = cnt_list.size();
c_layers[l].z_height = z_h;
c_layers[l].num_contours = num_cnts;
c_contours_pool[l].resize(num_cnts);
c_points_pool[l].resize(num_cnts);
for (size_t c = 0; c < num_cnts; ++c) {
py::array_t<float, py::array::c_style | py::array::forcecast> arr = cnt_list[c].cast<py::array_t<float>>();
py::buffer_info buf = arr.request();
size_t n_pts = buf.shape[0];
auto& pts = c_points_pool[l][c];
pts.resize(n_pts);
const float* raw_data = static_cast<const float*>(buf.ptr);
for (size_t p = 0; p < n_pts; ++p) {
pts[p].x = raw_data[p * 2 + 0];
pts[p].y = raw_data[p * 2 + 1];
}
c_contours_pool[l][c].points = pts.data();
c_contours_pool[l][c].num_points = n_pts;
c_contours_pool[l][c].is_hole = 0;
}
c_layers[l].contours = c_contours_pool[l].data();
}
GpuOffsetConfig config = {0};
config.pitch_mm = pitch_mm;
config.pad_mm = pad_mm;
config.perimeters_count = perimeters_count;
config.layer_width = layer_width;
config.export_infill_mask = export_infill_mask ? 1 : 0;
config.use_gpu_rasterizer = 1;
config.backend = parse_backend_str(backend);
if (!offset_distances.empty()) {
config.offset_distances_mm = offset_distances.data();
config.num_offsets = offset_distances.size();
} else {
config.offset_distances_mm = nullptr;
config.num_offsets = 0;
}
GpuLayerResult* results = nullptr;
GpuOffsetStats stats = {0};
int ret = gpu_offset_polygon_layers(
c_layers.data(), num_layers,
min_x, min_y, max_x, max_y,
&config, &results, &stats
);
if (ret == GPU_OFFSET_ERR_NO_CUDA) {
throw std::runtime_error("CUDA backend was requested, but no functional CUDA-capable device was found.");
} else if (ret != GPU_OFFSET_SUCCESS || !results) {
throw std::runtime_error("Polygon offsetting failed in compute backend.");
}
py::list py_layer_results;
for (size_t l = 0; l < num_layers; ++l) {
py::dict py_layer_dict;
py_layer_dict["z"] = results[l].z_height;
// Perimeters
py::list py_perimeters;
for (size_t o = 0; o < results[l].num_perimeters; ++o) {
py::dict py_perim_dict;
py_perim_dict["offset_mm"] = results[l].perimeters[o].offset_distance_mm;
py::list py_cnt_list;
for (size_t c = 0; c < results[l].perimeters[o].num_contours; ++c) {
const auto& cnt = results[l].perimeters[o].contours[c];
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_cnt_list.append(pts_arr);
}
py_perim_dict["contours"] = py_cnt_list;
py_perimeters.append(py_perim_dict);
}
py_layer_dict["perimeters"] = py_perimeters;
// Infill Contours
py::list py_infill_cnts;
for (size_t c = 0; c < results[l].infill.num_contours; ++c) {
const auto& cnt = results[l].infill.contours[c];
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_infill_cnts.append(pts_arr);
}
py_layer_dict["infill_contours"] = py_infill_cnts;
py_layer_dict["infill_offset_mm"] = results[l].infill.offset_distance_mm;
// Infill 2D Mask
if (results[l].infill.mask != nullptr) {
int H = results[l].infill.height;
int W = results[l].infill.width;
py::array_t<uint8_t> mask_arr({ (ssize_t)H, (ssize_t)W });
py::buffer_info mb = mask_arr.request();
memcpy(mb.ptr, results[l].infill.mask, (size_t)H * W * sizeof(uint8_t));
py_layer_dict["infill_mask"] = mask_arr;
}
py_layer_results.append(py_layer_dict);
}
gpu_offset_free_results(results, num_layers);
return py_layer_results;
}
// 3. Direct 3D Mesh Slicing & Offsetting
py::list py_slice_mesh_and_offset(
py::array_t<float, py::array::c_style | py::array::forcecast> triangles_arr,
float min_x, float min_y, float min_z,
float max_x, float max_y, float max_z,
float layer_height,
float first_layer_height,
int perimeters_count,
float layer_width,
float pitch_mm,
float pad_mm,
bool export_infill_mask,
const std::vector<float>& offset_distances,
const std::string& backend
) {
py::buffer_info buf = triangles_arr.request();
size_t num_tris = 0;
if (buf.ndim == 3 && buf.shape[1] == 3 && buf.shape[2] == 3) {
num_tris = buf.shape[0];
} else if (buf.ndim == 2 && buf.shape[1] == 9) {
num_tris = buf.shape[0];
} else {
throw std::runtime_error("Triangles array must be of shape (N, 3, 3) or (N, 9) float32.");
}
if (num_tris == 0) return py::list();
const GpuTriangle3D* tris_ptr = static_cast<const GpuTriangle3D*>(buf.ptr);
GpuOffsetConfig config = {0};
config.pitch_mm = pitch_mm;
config.pad_mm = pad_mm;
config.perimeters_count = perimeters_count;
config.layer_width = layer_width;
config.first_layer_height = first_layer_height;
config.export_infill_mask = export_infill_mask ? 1 : 0;
config.use_gpu_rasterizer = 1;
config.backend = parse_backend_str(backend);
if (!offset_distances.empty()) {
config.offset_distances_mm = offset_distances.data();
config.num_offsets = offset_distances.size();
} else {
config.offset_distances_mm = nullptr;
config.num_offsets = 0;
}
GpuLayerResult* results = nullptr;
size_t out_num_layers = 0;
GpuOffsetStats stats = {0};
int ret = gpu_offset_slice_mesh_and_offset(
tris_ptr, num_tris,
min_x, min_y, min_z, max_x, max_y, max_z,
layer_height, first_layer_height, &config,
&results, &out_num_layers, &stats
);
if (ret == GPU_OFFSET_ERR_NO_CUDA) {
throw std::runtime_error("CUDA backend was requested, but no functional CUDA-capable device was found.");
} else if (ret != GPU_OFFSET_SUCCESS || !results) {
throw std::runtime_error("Slice mesh and offset failed in compute backend.");
}
py::list py_layer_results;
for (size_t l = 0; l < out_num_layers; ++l) {
py::dict py_layer_dict;
py_layer_dict["z"] = results[l].z_height;
// Perimeters
py::list py_perimeters;
for (size_t o = 0; o < results[l].num_perimeters; ++o) {
py::dict py_perim_dict;
py_perim_dict["offset_mm"] = results[l].perimeters[o].offset_distance_mm;
py::list py_cnt_list;
for (size_t c = 0; c < results[l].perimeters[o].num_contours; ++c) {
const auto& cnt = results[l].perimeters[o].contours[c];
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_cnt_list.append(pts_arr);
}
py_perim_dict["contours"] = py_cnt_list;
py_perimeters.append(py_perim_dict);
}
py_layer_dict["perimeters"] = py_perimeters;
// Infill Contours
py::list py_infill_cnts;
for (size_t c = 0; c < results[l].infill.num_contours; ++c) {
const auto& cnt = results[l].infill.contours[c];
py::array_t<float> pts_arr({ (ssize_t)cnt.num_points, (ssize_t)2 });
auto r = pts_arr.mutable_unchecked<2>();
for (size_t p = 0; p < cnt.num_points; ++p) {
r(p, 0) = cnt.points[p].x;
r(p, 1) = cnt.points[p].y;
}
py_infill_cnts.append(pts_arr);
}
py_layer_dict["infill_contours"] = py_infill_cnts;
py_layer_dict["infill_offset_mm"] = results[l].infill.offset_distance_mm;
// Infill 2D Mask
if (results[l].infill.mask != nullptr) {
int H = results[l].infill.height;
int W = results[l].infill.width;
py::array_t<uint8_t> mask_arr({ (ssize_t)H, (ssize_t)W });
py::buffer_info mb = mask_arr.request();
memcpy(mb.ptr, results[l].infill.mask, (size_t)H * W * sizeof(uint8_t));
py_layer_dict["infill_mask"] = mask_arr;
}
py_layer_results.append(py_layer_dict);
}
gpu_offset_free_results(results, out_num_layers);
return py_layer_results;
}
PYBIND11_MODULE(_cudaslicer_backend, m) {
m.doc() = "GPU/CPU Slicing & SDF/JFA Offsetting C++/CUDA Python Extension";
m.def("has_cuda", &gpu_offset_has_cuda, "Returns 1 if a functional CUDA GPU is present, 0 otherwise.");
m.def("is_cuda_available", &gpu_offset_has_cuda, "Returns 1 if a functional CUDA GPU is present, 0 otherwise.");
m.def("set_backend", [](const std::string& b) {
int code = gpu_offset_set_backend(parse_backend_str(b));
if (code != GPU_OFFSET_SUCCESS) throw std::invalid_argument("Failed to set backend.");
}, py::arg("backend") = "auto", "Set global compute backend ('auto', 'cuda', 'cpu').");
m.def("get_backend", []() -> std::string {
int b = gpu_offset_get_backend();
if (b == GPU_OFFSET_BACKEND_CUDA) return "cuda";
if (b == GPU_OFFSET_BACKEND_CPU) return "cpu";
return "auto";
}, "Get global compute backend ('auto', 'cuda', 'cpu').");
m.def("init", &gpu_offset_init, py::arg("device_id") = 0, "Initialize CUDA runtime and allocate stream.");
m.def("cleanup", &gpu_offset_cleanup, "Release backend resources.");
m.def("offset_masks", &py_offset_masks,
py::arg("in_masks"),
py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f,
py::arg("pitch_mm") = 0.10f,
py::arg("offset_distances") = std::vector<float>(),
py::arg("backend") = "auto",
"Offset 2D (H,W) or 3D (N,H,W) binary occupancy numpy masks.");
m.def("offset_polygon_layers", &py_offset_polygon_layers,
py::arg("py_layers"), py::arg("min_x"), py::arg("min_y"),
py::arg("max_x"), py::arg("max_y"),
py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f,
py::arg("pitch_mm") = 0.10f,
py::arg("pad_mm") = 1.5f,
py::arg("export_infill_mask") = true,
py::arg("offset_distances") = std::vector<float>(),
py::arg("backend") = "auto",
"Compute polygon perimeter offsets and infill mask.");
m.def("slice_mesh_and_offset", &py_slice_mesh_and_offset,
py::arg("triangles_arr"), py::arg("min_x"), py::arg("min_y"), py::arg("min_z"),
py::arg("max_x"), py::arg("max_y"), py::arg("max_z"),
py::arg("layer_height") = 0.20f,
py::arg("first_layer_height") = 0.0f,
py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f,
py::arg("pitch_mm") = 0.10f,
py::arg("pad_mm") = 1.5f,
py::arg("export_infill_mask") = true,
py::arg("offset_distances") = std::vector<float>(),
py::arg("backend") = "auto",
"Mesh slicing, perimeter offsetting, and infill mask generation.");
}

62
setup.py Normal file
View file

@ -0,0 +1,62 @@
import os
import sys
import platform
import subprocess
from setuptools import setup, find_packages, Extension
from setuptools.command.build_ext import build_ext
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=""):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)
class CMakeBuild(build_ext):
def build_extension(self, ext):
extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
cmake_args = [
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}",
f"-DPYTHON_EXECUTABLE={sys.executable}",
f"-DPython3_EXECUTABLE={sys.executable}",
]
cfg = "Debug" if self.debug else "Release"
build_args = ["--config", cfg]
if platform.system() == "Windows":
cmake_args += [
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}",
"-A", "x64"
]
else:
cmake_args += [f"-DCMAKE_BUILD_TYPE={cfg}"]
build_args += ["--", f"-j{os.cpu_count() or 4}"]
build_temp = os.path.join(self.build_temp, ext.name)
os.makedirs(build_temp, exist_ok=True)
subprocess.check_call(["cmake", ext.sourcedir] + cmake_args, cwd=build_temp)
subprocess.check_call(["cmake", "--build", "."] + build_args, cwd=build_temp)
setup(
name="cudaslicer",
version="1.0.0",
author="CUDA Slicer Team",
description="GPU-Accelerated 2D/3D Slicing & Polygon Offsetting Library",
long_description=open("README.md").read() if os.path.exists("README.md") else "",
long_description_content_type="text/markdown",
package_dir={"": "python"},
packages=find_packages(where="python"),
ext_modules=[CMakeExtension("cudaslicer._cudaslicer_backend")],
cmdclass={"build_ext": CMakeBuild},
python_requires=">=3.8",
install_requires=[
"numpy>=1.20.0",
],
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: C++",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Topic :: Scientific/Engineering",
],
)

766
src/cpu_offset.cpp Normal file
View file

@ -0,0 +1,766 @@
#include "cpu_offset.h"
#include "vectorizer.h"
#include <iostream>
#include <vector>
#include <cmath>
#include <chrono>
#include <cstring>
#include <algorithm>
#include <thread>
// -----------------------------------------------------------------------------
// Fast Separable 2D Exact Euclidean Distance Transform on Binary Grid
// -----------------------------------------------------------------------------
// Pass 1 on binary column: fast forward + backward scan (O(N) exact)
static inline void edt_1d_binary_col(const uint8_t* col_mask, int height, int stride, float* col_g) {
const float INF = 1e12f;
// Forward pass
int last = -1000000;
for (int y = 0; y < height; ++y) {
if (col_mask[(size_t)y * stride] == 0) {
last = y;
}
float dy = (float)(y - last);
col_g[y] = (last >= 0) ? (dy * dy) : INF;
}
// Backward pass
last = 1000000;
for (int y = height - 1; y >= 0; --y) {
if (col_mask[(size_t)y * stride] == 0) {
last = y;
}
float dy = (float)(last - y);
float d2 = (last < height) ? (dy * dy) : INF;
if (d2 < col_g[y]) {
col_g[y] = d2;
}
}
}
// Pass 2 on rows: Parabolic Envelope 1D Squared Distance Transform (Felzenszwalb-Huttenlocher)
static inline void edt_1d_parabolic_row(const float* f, float* d, int* v, float* z, int n) {
int k = 0;
v[0] = 0;
z[0] = -1e20f;
z[1] = 1e20f;
for (int q = 1; q < n; ++q) {
float f_q = f[q];
if (f_q > 1e10f) continue;
float s = ((f_q + (float)q * q) - (f[v[k]] + (float)v[k] * v[k])) / (2.0f * q - 2.0f * v[k]);
while (s <= z[k]) {
k--;
s = ((f_q + (float)q * q) - (f[v[k]] + (float)v[k] * v[k])) / (2.0f * q - 2.0f * v[k]);
}
k++;
v[k] = q;
z[k] = s;
z[k + 1] = 1e20f;
}
k = 0;
for (int q = 0; q < n; ++q) {
while (z[k + 1] < (float)q) {
k++;
}
float dx = (float)(q - v[k]);
d[q] = dx * dx + f[v[k]];
}
}
static inline void edt_2d_layer(
const uint8_t* mask,
int width,
int height,
float* out_dist_sq,
std::vector<float>& g_col_buf,
std::vector<float>& row_f,
std::vector<float>& row_d,
std::vector<int>& v_buf,
std::vector<float>& z_buf
) {
// Pass 1: Columns
for (int x = 0; x < width; ++x) {
edt_1d_binary_col(mask + x, height, width, g_col_buf.data() + (size_t)x * height);
}
// Pass 2: Rows
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
row_f[x] = g_col_buf[(size_t)x * height + y];
}
edt_1d_parabolic_row(row_f.data(), row_d.data(), v_buf.data(), z_buf.data(), width);
for (int x = 0; x < width; ++x) {
out_dist_sq[(size_t)y * width + x] = row_d[x];
}
}
}
// -----------------------------------------------------------------------------
// CPU Multi-Threaded Polygon Rasterizer
// -----------------------------------------------------------------------------
struct CpuEdge2D {
float x1, y1;
float x2, y2;
};
void cpu_rasterize_polygon_layers_internal(
const GpuSliceLayer* in_layers,
size_t num_layers,
uint8_t* out_masks,
int width, int height,
float min_x, float min_y, float pitch_mm
) {
unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
std::vector<std::thread> workers;
auto worker_fn = [&](size_t start_layer, size_t end_layer) {
for (size_t l = start_layer; l < end_layer; ++l) {
uint8_t* mask = out_masks + l * ((size_t)width * height);
const GpuSliceLayer& layer = in_layers[l];
std::vector<CpuEdge2D> edges;
for (size_t c = 0; c < layer.num_contours; ++c) {
const GpuContour& cnt = layer.contours[c];
if (cnt.num_points < 3) continue;
for (size_t p = 0; p < cnt.num_points; ++p) {
size_t p_next = (p + 1) % cnt.num_points;
edges.push_back({ cnt.points[p].x, cnt.points[p].y,
cnt.points[p_next].x, cnt.points[p_next].y });
}
}
for (int y = 0; y < height; ++y) {
float py = min_y + (y + 0.5f) * pitch_mm;
std::vector<float> x_inters;
for (const auto& e : edges) {
if ((e.y1 <= py && e.y2 > py) || (e.y2 <= py && e.y1 > py)) {
float x_int = e.x1 + (py - e.y1) * (e.x2 - e.x1) / (e.y2 - e.y1);
x_inters.push_back(x_int);
}
}
std::sort(x_inters.begin(), x_inters.end());
for (size_t i = 0; i + 1 < x_inters.size(); i += 2) {
int x_start = std::max(0, (int)std::ceil((x_inters[i] - min_x) / pitch_mm - 0.5f));
int x_end = std::min(width, (int)std::floor((x_inters[i + 1] - min_x) / pitch_mm - 0.5f) + 1);
for (int x = x_start; x < x_end; ++x) {
mask[(size_t)y * width + x] = 1;
}
}
}
}
};
size_t chunk = (num_layers + num_threads - 1) / num_threads;
for (unsigned int t = 0; t < num_threads; ++t) {
size_t s = t * chunk;
size_t e = std::min(s + chunk, num_layers);
if (s < e) {
workers.emplace_back(worker_fn, s, e);
}
}
for (auto& w : workers) w.join();
}
// -----------------------------------------------------------------------------
// CPU Implementation: Core Mask Offsetting
// -----------------------------------------------------------------------------
int cpu_offset_compute_masks(
const uint8_t* in_masks,
int width,
int height,
int num_layers,
float pitch_mm,
const float* offset_distances,
int num_offsets,
uint8_t* out_masks,
GpuOffsetStats* stats
) {
if (!in_masks || !offset_distances || !out_masks || width <= 0 || height <= 0 || num_layers <= 0 || num_offsets <= 0) {
return GPU_OFFSET_ERR_INVALID_ARG;
}
auto t_start = std::chrono::high_resolution_clock::now();
size_t layer_pixels = (size_t)width * height;
size_t total_volume = (size_t)num_layers * layer_pixels;
unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
std::vector<std::thread> workers;
auto worker_fn = [&](size_t start_layer, size_t end_layer) {
std::vector<float> dist_sq(layer_pixels);
std::vector<float> g_col_buf(layer_pixels);
std::vector<float> row_f(width), row_d(width);
std::vector<int> v_buf(width + 1);
std::vector<float> z_buf(width + 2);
for (size_t l = start_layer; l < end_layer; ++l) {
const uint8_t* mask_layer = in_masks + l * layer_pixels;
// 1. Compute 2D EDT
edt_2d_layer(mask_layer, width, height, dist_sq.data(),
g_col_buf, row_f, row_d, v_buf, z_buf);
// 2. Thresholding for each offset distance
for (int o = 0; o < num_offsets; ++o) {
float thresh = offset_distances[o];
float thresh_sq_px = (thresh / pitch_mm) * (thresh / pitch_mm);
uint8_t* out_layer = out_masks + (size_t)o * total_volume + l * layer_pixels;
for (size_t i = 0; i < layer_pixels; ++i) {
out_layer[i] = (mask_layer[i] > 0 && dist_sq[i] >= thresh_sq_px) ? 1 : 0;
}
}
}
};
size_t chunk = (num_layers + num_threads - 1) / num_threads;
for (unsigned int t = 0; t < num_threads; ++t) {
size_t s = t * chunk;
size_t e = std::min(s + chunk, (size_t)num_layers);
if (s < e) workers.emplace_back(worker_fn, s, e);
}
for (auto& w : workers) w.join();
auto t_end = std::chrono::high_resolution_clock::now();
double total_ms = std::chrono::duration<double, std::milli>(t_end - t_start).count();
if (stats) {
memset(stats, 0, sizeof(GpuOffsetStats));
stats->t_gpu_jfa_ms = total_ms;
stats->t_total_pipeline_ms = total_ms;
}
return GPU_OFFSET_SUCCESS;
}
// -----------------------------------------------------------------------------
// CPU Implementation: Polygon Layers Offsetting
// -----------------------------------------------------------------------------
int cpu_offset_polygon_layers(
const GpuSliceLayer* in_layers,
size_t num_layers,
float min_x, float min_y, float max_x, float max_y,
const GpuOffsetConfig* config,
GpuLayerResult** out_results,
GpuOffsetStats* stats
) {
if (!in_layers || num_layers == 0 || !config || !out_results) return GPU_OFFSET_ERR_INVALID_ARG;
auto t_total_start = std::chrono::high_resolution_clock::now();
float pad = (config->pad_mm > 0.0f) ? config->pad_mm : 1.5f;
float pitch = (config->pitch_mm > 0.0f) ? config->pitch_mm : 0.10f;
float bounds_min_x = min_x - pad;
float bounds_min_y = min_y - pad;
float bounds_max_x = max_x + pad;
float bounds_max_y = max_y + pad;
int cols = (int)std::ceil((bounds_max_x - bounds_min_x) / pitch);
int rows = (int)std::ceil((bounds_max_y - bounds_min_y) / pitch);
size_t layer_pixels = (size_t)cols * rows;
size_t total_voxels = (size_t)num_layers * layer_pixels;
// Resolve Offsets (perimeters + infill)
int P = (config->perimeters_count >= 0) ? config->perimeters_count : 2;
float W = (config->layer_width > 0.0f) ? config->layer_width : 0.45f;
std::vector<float> resolved_offsets;
if (config->offset_distances_mm && config->num_offsets > 0) {
resolved_offsets.assign(config->offset_distances_mm, config->offset_distances_mm + config->num_offsets);
P = (config->perimeters_count > 0) ? config->perimeters_count : (int)resolved_offsets.size() - 1;
if (P < 1) P = (int)resolved_offsets.size();
} else {
resolved_offsets.resize(P + 1);
for (int i = 0; i < P; ++i) {
resolved_offsets[i] = (i + 0.5f) * W; // -0.5W, -1.5W, ...
}
resolved_offsets[P] = (float)P * W; // -P * W for infill
}
// 1. Multi-Threaded CPU Rasterization
auto t_rast_0 = std::chrono::high_resolution_clock::now();
std::vector<uint8_t> h_masks(total_voxels, 0);
cpu_rasterize_polygon_layers_internal(in_layers, num_layers, h_masks.data(),
cols, rows, bounds_min_x, bounds_min_y, pitch);
auto t_rast_1 = std::chrono::high_resolution_clock::now();
double t_raster_ms = std::chrono::duration<double, std::milli>(t_rast_1 - t_rast_0).count();
// 2. Multi-Threaded CPU EDT + Thresholding + Vectorization
GpuLayerResult* res_array = (GpuLayerResult*)malloc(num_layers * sizeof(GpuLayerResult));
memset(res_array, 0, num_layers * sizeof(GpuLayerResult));
unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
std::vector<std::thread> workers;
int row_stride_words = (cols + 31) / 32;
auto worker_fn = [&](size_t start_layer, size_t end_layer) {
int stride_w = cols + 1;
size_t max_edges = 2 * (size_t)(rows + 1) * stride_w;
std::vector<int> next_edge_buf(max_edges, -1);
std::vector<int> active_edges_buf;
active_edges_buf.reserve(cols * rows / 16);
std::vector<uint8_t> visited_buf(max_edges, 0);
std::vector<float> dist_sq(layer_pixels);
std::vector<float> g_col_buf(layer_pixels);
std::vector<float> row_f(cols), row_d(cols);
std::vector<int> v_buf(cols + 1);
std::vector<float> z_buf(cols + 2);
std::vector<unsigned int> bit_words((size_t)rows * row_stride_words);
for (size_t l = start_layer; l < end_layer; ++l) {
const uint8_t* mask_ptr = h_masks.data() + l * layer_pixels;
// Compute exact 2D EDT
edt_2d_layer(mask_ptr, cols, rows, dist_sq.data(),
g_col_buf, row_f, row_d, v_buf, z_buf);
res_array[l].z_height = in_layers[l].z_height;
res_array[l].num_perimeters = P;
res_array[l].perimeters = (GpuOffsetPerimeter*)malloc(P * sizeof(GpuOffsetPerimeter));
// Perimeter passes
for (int o = 0; o < P; ++o) {
float thresh = resolved_offsets[o];
float thresh_sq_px = (thresh / pitch) * (thresh / pitch);
// Bit-pack thresholded mask
for (int y = 0; y < rows; ++y) {
const uint8_t* row_m = mask_ptr + (size_t)y * cols;
const float* row_d2 = dist_sq.data() + (size_t)y * cols;
unsigned int* row_w = bit_words.data() + (size_t)y * row_stride_words;
for (int w = 0; w < row_stride_words; ++w) {
unsigned int packed = 0;
int x_base = w * 32;
for (int b = 0; b < 32; ++b) {
int x = x_base + b;
if (x < cols) {
if (row_m[x] > 0 && row_d2[x] >= thresh_sq_px) {
packed |= (1u << b);
}
}
}
row_w[w] = packed;
}
}
// Marching squares vectorization
std::vector<GpuContour> contours;
vectorize_bitpack_layer(bit_words.data(), cols, rows, row_stride_words,
bounds_min_x, bounds_min_y, pitch,
next_edge_buf, active_edges_buf, visited_buf, contours);
res_array[l].perimeters[o].offset_distance_mm = resolved_offsets[o];
res_array[l].perimeters[o].num_contours = contours.size();
if (!contours.empty()) {
res_array[l].perimeters[o].contours = (GpuContour*)malloc(contours.size() * sizeof(GpuContour));
memcpy(res_array[l].perimeters[o].contours, contours.data(), contours.size() * sizeof(GpuContour));
} else {
res_array[l].perimeters[o].contours = nullptr;
}
}
// Infill pass (index P)
float infill_thresh = resolved_offsets[P];
float infill_thresh_sq_px = (infill_thresh / pitch) * (infill_thresh / pitch);
for (int y = 0; y < rows; ++y) {
const uint8_t* row_m = mask_ptr + (size_t)y * cols;
const float* row_d2 = dist_sq.data() + (size_t)y * cols;
unsigned int* row_w = bit_words.data() + (size_t)y * row_stride_words;
for (int w = 0; w < row_stride_words; ++w) {
unsigned int packed = 0;
int x_base = w * 32;
for (int b = 0; b < 32; ++b) {
int x = x_base + b;
if (x < cols) {
if (row_m[x] > 0 && row_d2[x] >= infill_thresh_sq_px) {
packed |= (1u << b);
}
}
}
row_w[w] = packed;
}
}
std::vector<GpuContour> infill_contours;
vectorize_bitpack_layer(bit_words.data(), cols, rows, row_stride_words,
bounds_min_x, bounds_min_y, pitch,
next_edge_buf, active_edges_buf, visited_buf, infill_contours);
res_array[l].infill.offset_distance_mm = resolved_offsets[P];
res_array[l].infill.width = cols;
res_array[l].infill.height = rows;
res_array[l].infill.num_contours = infill_contours.size();
if (!infill_contours.empty()) {
res_array[l].infill.contours = (GpuContour*)malloc(infill_contours.size() * sizeof(GpuContour));
memcpy(res_array[l].infill.contours, infill_contours.data(), infill_contours.size() * sizeof(GpuContour));
} else {
res_array[l].infill.contours = nullptr;
}
if (config->export_infill_mask) {
uint8_t* infill_mask = (uint8_t*)malloc((size_t)cols * rows);
for (int y = 0; y < rows; ++y) {
const unsigned int* row_words = bit_words.data() + (size_t)y * row_stride_words;
uint8_t* row_out = infill_mask + (size_t)y * cols;
int x_out = 0;
for (int w = 0; w < row_stride_words && x_out < cols; ++w) {
unsigned int word = row_words[w];
for (int b = 0; b < 32 && x_out < cols; ++b, ++x_out) {
row_out[x_out] = (uint8_t)((word >> b) & 1u);
}
}
}
res_array[l].infill.mask = infill_mask;
} else {
res_array[l].infill.mask = nullptr;
}
}
};
size_t chunk = (num_layers + num_threads - 1) / num_threads;
for (unsigned int t = 0; t < num_threads; ++t) {
size_t s = t * chunk;
size_t e = std::min(s + chunk, num_layers);
if (s < e) workers.emplace_back(worker_fn, s, e);
}
for (auto& w : workers) w.join();
auto t_total_end = std::chrono::high_resolution_clock::now();
double t_total_ms = std::chrono::duration<double, std::milli>(t_total_end - t_total_start).count();
if (stats) {
memset(stats, 0, sizeof(GpuOffsetStats));
stats->t_raster_ms = t_raster_ms;
stats->t_gpu_jfa_ms = t_total_ms - t_raster_ms; // EDT + Vectorize
stats->t_total_pipeline_ms = t_total_ms;
}
*out_results = res_array;
return GPU_OFFSET_SUCCESS;
}
// -----------------------------------------------------------------------------
// CPU Implementation: Direct 3D Mesh Slicing & Offsetting
// -----------------------------------------------------------------------------
int cpu_offset_slice_mesh_and_offset(
const GpuTriangle3D* triangles,
size_t num_triangles,
float min_x, float min_y, float min_z,
float max_x, float max_y, float max_z,
float layer_height,
float first_layer_height,
const GpuOffsetConfig* config,
GpuLayerResult** out_results,
size_t* out_num_layers,
GpuOffsetStats* stats
) {
if (!triangles || num_triangles == 0 || !config || !out_results) return GPU_OFFSET_ERR_INVALID_ARG;
auto t_total_start = std::chrono::high_resolution_clock::now();
float pad = (config->pad_mm > 0.0f) ? config->pad_mm : 1.5f;
float pitch = (config->pitch_mm > 0.0f) ? config->pitch_mm : 0.10f;
float bounds_min_x = min_x - pad;
float bounds_min_y = min_y - pad;
float bounds_max_x = max_x + pad;
float bounds_max_y = max_y + pad;
int cols = (int)std::ceil((bounds_max_x - bounds_min_x) / pitch);
int rows = (int)std::ceil((bounds_max_y - bounds_min_y) / pitch);
float h_first = first_layer_height;
if (h_first <= 0.0f) h_first = config->first_layer_height;
if (h_first <= 0.0f) h_first = layer_height;
float z_first = min_z + h_first;
int num_layers = 0;
if (max_z >= z_first) {
num_layers = (int)std::floor((max_z - z_first) / layer_height) + 1;
}
if (num_layers <= 0) return GPU_OFFSET_ERR_INVALID_ARG;
if (out_num_layers) *out_num_layers = num_layers;
// Resolve Offsets (perimeters + infill)
int P = (config->perimeters_count >= 0) ? config->perimeters_count : 2;
float W = (config->layer_width > 0.0f) ? config->layer_width : 0.45f;
std::vector<float> resolved_offsets;
if (config->offset_distances_mm && config->num_offsets > 0) {
resolved_offsets.assign(config->offset_distances_mm, config->offset_distances_mm + config->num_offsets);
P = (config->perimeters_count > 0) ? config->perimeters_count : (int)resolved_offsets.size() - 1;
if (P < 1) P = (int)resolved_offsets.size();
} else {
resolved_offsets.resize(P + 1);
for (int i = 0; i < P; ++i) {
resolved_offsets[i] = (i + 0.5f) * W; // -0.5W, -1.5W, ...
}
resolved_offsets[P] = (float)P * W; // -P * W for infill
}
int row_stride_words = (cols + 31) / 32;
size_t layer_pixels = (size_t)cols * rows;
// 1. Spatial Triangle Indexing into layers
auto t_idx_0 = std::chrono::high_resolution_clock::now();
std::vector<std::vector<uint32_t>> layer_tri_indices(num_layers);
for (uint32_t i = 0; i < (uint32_t)num_triangles; ++i) {
const GpuTriangle3D& tri = triangles[i];
float tz_min = std::min({ tri.v0[2], tri.v1[2], tri.v2[2] });
float tz_max = std::max({ tri.v0[2], tri.v1[2], tri.v2[2] });
int l_start = (tz_min <= z_first) ? 0 : (int)std::ceil((tz_min - z_first) / layer_height);
int l_end = (int)std::floor((tz_max - z_first) / layer_height);
l_start = std::max(0, l_start);
l_end = std::min(num_layers - 1, l_end);
for (int l = l_start; l <= l_end; ++l) {
layer_tri_indices[l].push_back(i);
}
}
auto t_idx_1 = std::chrono::high_resolution_clock::now();
double t_idx_ms = std::chrono::duration<double, std::milli>(t_idx_1 - t_idx_0).count();
// 2. Parallel Layer Slicing, Parity Scan, EDT, Thresholding, Vectorization
GpuLayerResult* res_array = (GpuLayerResult*)malloc(num_layers * sizeof(GpuLayerResult));
memset(res_array, 0, num_layers * sizeof(GpuLayerResult));
unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
std::vector<std::thread> workers;
auto worker_fn = [&](size_t start_layer, size_t end_layer) {
int stride_w = cols + 1;
size_t max_edges = 2 * (size_t)(rows + 1) * stride_w;
std::vector<int> next_edge_buf(max_edges, -1);
std::vector<int> active_edges_buf;
active_edges_buf.reserve(cols * rows / 16);
std::vector<uint8_t> visited_buf(max_edges, 0);
std::vector<unsigned int> parity_words((size_t)rows * row_stride_words, 0);
std::vector<uint8_t> layer_mask(layer_pixels, 0);
std::vector<float> dist_sq(layer_pixels);
std::vector<float> g_col_buf(layer_pixels);
std::vector<float> row_f(cols), row_d(cols);
std::vector<int> v_buf(cols + 1);
std::vector<float> z_buf(cols + 2);
std::vector<unsigned int> bit_words((size_t)rows * row_stride_words);
for (size_t l = start_layer; l < end_layer; ++l) {
float z0 = z_first + l * layer_height;
std::fill(parity_words.begin(), parity_words.end(), 0u);
// Slicing & Parity Rasterization for Layer l
const auto& tri_indices = layer_tri_indices[l];
for (uint32_t t_idx : tri_indices) {
const GpuTriangle3D& tri = triangles[t_idx];
const float* v[3] = { tri.v0, tri.v1, tri.v2 };
float pts_x[3];
float pts_y[3];
int count = 0;
for (int e = 0; e < 3; ++e) {
int next = (e + 1) % 3;
float za = v[e][2];
float zb = v[next][2];
if ((za <= z0 && zb >= z0) || (zb <= z0 && za >= z0)) {
if (std::abs(zb - za) > 1e-7f) {
float t = (z0 - za) / (zb - za);
pts_x[count] = v[e][0] + t * (v[next][0] - v[e][0]);
pts_y[count] = v[e][1] + t * (v[next][1] - v[e][1]);
count++;
if (count == 2) break;
}
}
}
if (count != 2) continue;
float gx0 = (pts_x[0] - bounds_min_x) / pitch - 0.5f;
float gy0 = (pts_y[0] - bounds_min_y) / pitch - 0.5f;
float gx1 = (pts_x[1] - bounds_min_x) / pitch - 0.5f;
float gy1 = (pts_y[1] - bounds_min_y) / pitch - 0.5f;
if (gy0 > gy1) {
std::swap(gx0, gx1);
std::swap(gy0, gy1);
}
float dy = gy1 - gy0;
if (dy < 1e-6f) continue;
int y_start = (int)std::ceil(gy0);
int y_end = (int)std::floor(gy1 - 1e-6f);
if (y_start < 0) y_start = 0;
if (y_end >= rows) y_end = rows - 1;
for (int y = y_start; y <= y_end; ++y) {
float t = ((float)y - gy0) / dy;
float gx = gx0 + t * (gx1 - gx0);
int x_int = (int)std::round(gx + 0.5f);
if (x_int < 0) x_int = 0;
if (x_int >= cols) x_int = cols - 1;
int w = x_int / 32;
int b = x_int % 32;
parity_words[(size_t)y * row_stride_words + w] ^= (1u << b);
}
}
// Scan Parity Words into Binary Mask
for (int y = 0; y < rows; ++y) {
const unsigned int* row_p = parity_words.data() + (size_t)y * row_stride_words;
uint8_t* row_m = layer_mask.data() + (size_t)y * cols;
uint8_t inside = 0;
int x_out = 0;
for (int w = 0; w < row_stride_words && x_out < cols; ++w) {
unsigned int word = row_p[w];
for (int b = 0; b < 32 && x_out < cols; ++b, ++x_out) {
inside ^= (uint8_t)((word >> b) & 1u);
row_m[x_out] = inside;
}
}
}
// Compute exact 2D EDT
edt_2d_layer(layer_mask.data(), cols, rows, dist_sq.data(),
g_col_buf, row_f, row_d, v_buf, z_buf);
res_array[l].z_height = z0;
res_array[l].num_perimeters = P;
res_array[l].perimeters = (GpuOffsetPerimeter*)malloc(P * sizeof(GpuOffsetPerimeter));
// Perimeters
for (int o = 0; o < P; ++o) {
float thresh = resolved_offsets[o];
float thresh_sq_px = (thresh / pitch) * (thresh / pitch);
for (int y = 0; y < rows; ++y) {
const uint8_t* row_m = layer_mask.data() + (size_t)y * cols;
const float* row_d2 = dist_sq.data() + (size_t)y * cols;
unsigned int* row_w = bit_words.data() + (size_t)y * row_stride_words;
for (int w = 0; w < row_stride_words; ++w) {
unsigned int packed = 0;
int x_base = w * 32;
for (int b = 0; b < 32; ++b) {
int x = x_base + b;
if (x < cols) {
if (row_m[x] > 0 && row_d2[x] >= thresh_sq_px) {
packed |= (1u << b);
}
}
}
row_w[w] = packed;
}
}
std::vector<GpuContour> contours;
vectorize_bitpack_layer(bit_words.data(), cols, rows, row_stride_words,
bounds_min_x, bounds_min_y, pitch,
next_edge_buf, active_edges_buf, visited_buf, contours);
res_array[l].perimeters[o].offset_distance_mm = resolved_offsets[o];
res_array[l].perimeters[o].num_contours = contours.size();
if (!contours.empty()) {
res_array[l].perimeters[o].contours = (GpuContour*)malloc(contours.size() * sizeof(GpuContour));
memcpy(res_array[l].perimeters[o].contours, contours.data(), contours.size() * sizeof(GpuContour));
} else {
res_array[l].perimeters[o].contours = nullptr;
}
}
// Infill pass
float infill_thresh = resolved_offsets[P];
float infill_thresh_sq_px = (infill_thresh / pitch) * (infill_thresh / pitch);
for (int y = 0; y < rows; ++y) {
const uint8_t* row_m = layer_mask.data() + (size_t)y * cols;
const float* row_d2 = dist_sq.data() + (size_t)y * cols;
unsigned int* row_w = bit_words.data() + (size_t)y * row_stride_words;
for (int w = 0; w < row_stride_words; ++w) {
unsigned int packed = 0;
int x_base = w * 32;
for (int b = 0; b < 32; ++b) {
int x = x_base + b;
if (x < cols) {
if (row_m[x] > 0 && row_d2[x] >= infill_thresh_sq_px) {
packed |= (1u << b);
}
}
}
row_w[w] = packed;
}
}
std::vector<GpuContour> infill_contours;
vectorize_bitpack_layer(bit_words.data(), cols, rows, row_stride_words,
bounds_min_x, bounds_min_y, pitch,
next_edge_buf, active_edges_buf, visited_buf, infill_contours);
res_array[l].infill.offset_distance_mm = resolved_offsets[P];
res_array[l].infill.width = cols;
res_array[l].infill.height = rows;
res_array[l].infill.num_contours = infill_contours.size();
if (!infill_contours.empty()) {
res_array[l].infill.contours = (GpuContour*)malloc(infill_contours.size() * sizeof(GpuContour));
memcpy(res_array[l].infill.contours, infill_contours.data(), infill_contours.size() * sizeof(GpuContour));
} else {
res_array[l].infill.contours = nullptr;
}
if (config->export_infill_mask) {
uint8_t* infill_mask = (uint8_t*)malloc((size_t)cols * rows);
for (int y = 0; y < rows; ++y) {
const unsigned int* row_words = bit_words.data() + (size_t)y * row_stride_words;
uint8_t* row_out = infill_mask + (size_t)y * cols;
int x_out = 0;
for (int w = 0; w < row_stride_words && x_out < cols; ++w) {
unsigned int word = row_words[w];
for (int b = 0; b < 32 && x_out < cols; ++b, ++x_out) {
row_out[x_out] = (uint8_t)((word >> b) & 1u);
}
}
}
res_array[l].infill.mask = infill_mask;
} else {
res_array[l].infill.mask = nullptr;
}
}
};
size_t chunk = (num_layers + num_threads - 1) / num_threads;
for (unsigned int t = 0; t < num_threads; ++t) {
size_t s = t * chunk;
size_t e = std::min(s + chunk, (size_t)num_layers);
if (s < e) workers.emplace_back(worker_fn, s, e);
}
for (auto& w : workers) w.join();
auto t_total_end = std::chrono::high_resolution_clock::now();
double t_total_ms = std::chrono::duration<double, std::milli>(t_total_end - t_total_start).count();
if (stats) {
memset(stats, 0, sizeof(GpuOffsetStats));
stats->t_mesh_slice_ms = t_idx_ms;
stats->t_raster_ms = t_idx_ms;
stats->t_total_pipeline_ms = t_total_ms;
}
*out_results = res_array;
return GPU_OFFSET_SUCCESS;
}

67
src/cpu_offset.h Normal file
View file

@ -0,0 +1,67 @@
#ifndef CPU_OFFSET_H
#define CPU_OFFSET_H
#include "gpu_offset.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
void cpu_rasterize_polygon_layers_internal(
const GpuSliceLayer* in_layers,
size_t num_layers,
uint8_t* out_masks,
int width, int height,
float min_x, float min_y, float pitch_mm
);
/**
* @brief CPU fallback for core Euclidean distance transform and offsetting on raw masks.
*/
int cpu_offset_compute_masks(
const uint8_t* in_masks,
int width,
int height,
int num_layers,
float pitch_mm,
const float* offset_distances,
int num_offsets,
uint8_t* out_masks,
GpuOffsetStats* stats
);
/**
* @brief CPU fallback for 2D polygon layers offsetting.
*/
int cpu_offset_polygon_layers(
const GpuSliceLayer* in_layers,
size_t num_layers,
float min_x, float min_y, float max_x, float max_y,
const GpuOffsetConfig* config,
GpuLayerResult** out_results,
GpuOffsetStats* stats
);
/**
* @brief CPU fallback for direct 3D mesh slicing, rasterization, distance transform, and offsetting.
*/
int cpu_offset_slice_mesh_and_offset(
const GpuTriangle3D* triangles,
size_t num_triangles,
float min_x, float min_y, float min_z,
float max_x, float max_y, float max_z,
float layer_height,
float first_layer_height,
const GpuOffsetConfig* config,
GpuLayerResult** out_results,
size_t* out_num_layers,
GpuOffsetStats* stats
);
#ifdef __cplusplus
}
#endif
#endif // CPU_OFFSET_H

1059
src/gpu_offset.cu Normal file

File diff suppressed because it is too large Load diff

292
src/main_bench.cpp Normal file
View file

@ -0,0 +1,292 @@
#include "gpu_offset.h"
#include "stl_slicer.h"
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
#include <chrono>
#include <cmath>
#include <cstdlib>
int main(int argc, char** argv) {
std::string stl_path = "benchy.stl";
float layer_height = 0.20f;
float first_layer_height = 0.20f;
float pitch_mm = 0.10f;
int perimeters_count = 2;
float layer_width = 0.45f;
int runs = 3;
bool run_prusa = true;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--stl" && i + 1 < argc) stl_path = argv[++i];
else if (arg == "--layer-height" && i + 1 < argc) layer_height = std::stof(argv[++i]);
else if (arg == "--first-layer-height" && i + 1 < argc) first_layer_height = std::stof(argv[++i]);
else if (arg == "--perimeters" && i + 1 < argc) perimeters_count = std::stoi(argv[++i]);
else if (arg == "--layer-width" && i + 1 < argc) layer_width = std::stof(argv[++i]);
else if (arg == "--pitch" && i + 1 < argc) pitch_mm = std::stof(argv[++i]);
else if (arg == "--runs" && i + 1 < argc) runs = std::stoi(argv[++i]);
else if (arg == "--no-prusa") run_prusa = false;
}
std::cout << "================================================================================" << std::endl;
std::cout << " HIGH-PERFORMANCE MESH SLICING & OFFSETTING BENCHMARK (CUDA & CPU)" << std::endl;
std::cout << "================================================================================" << std::endl;
int has_cuda = gpu_offset_has_cuda();
std::cout << "[Device Capability] CUDA Available: " << (has_cuda ? "YES (GPU Accelerated)" : "NO (CPU Fallback)") << std::endl;
// 1. Initialize Engine
if (gpu_offset_init(0) != GPU_OFFSET_SUCCESS) {
std::cerr << "[Fatal Error] Failed to initialize backend" << std::endl;
return 1;
}
// 2. Load STL Mesh
std::cout << "\n[Step 1] Loading Binary STL Mesh: " << stl_path << std::endl;
auto t_load_0 = std::chrono::high_resolution_clock::now();
StlMesh mesh;
if (!load_stl_binary(stl_path, mesh)) {
return 1;
}
auto t_load_1 = std::chrono::high_resolution_clock::now();
double t_load_ms = std::chrono::duration<double, std::milli>(t_load_1 - t_load_0).count();
std::cout << " -> Loaded " << mesh.triangles.size() << " triangles in " << t_load_ms << " ms" << std::endl;
std::cout << " -> Bounds: [" << mesh.min_bound.x << ", " << mesh.min_bound.y << ", " << mesh.min_bound.z
<< "] to [" << mesh.max_bound.x << ", " << mesh.max_bound.y << ", " << mesh.max_bound.z << "]" << std::endl;
// 3. Slice Mesh in C++ (Hybrid Baseline)
std::cout << "\n[Step 2] Slicing Mesh with Multi-Threaded CPU Engine at " << layer_height << " mm Layer Height (First Layer: " << first_layer_height << " mm)" << std::endl;
std::vector<GpuSliceLayer> slice_layers;
std::vector<float> z_levels;
double t_slice_cpu_ms = 0;
slice_stl_mesh(mesh, layer_height, first_layer_height, slice_layers, z_levels, t_slice_cpu_ms);
size_t num_layers = slice_layers.size();
std::cout << " -> Sliced " << num_layers << " layers in " << t_slice_cpu_ms << " ms ("
<< (t_slice_cpu_ms / num_layers) << " ms/layer)" << std::endl;
// 4. Configure Offsetting Engine
GpuOffsetConfig config_gpu = {0};
config_gpu.pitch_mm = pitch_mm;
config_gpu.pad_mm = 1.5f;
config_gpu.perimeters_count = perimeters_count;
config_gpu.layer_width = layer_width;
config_gpu.first_layer_height = first_layer_height;
config_gpu.export_infill_mask = 1;
config_gpu.use_gpu_rasterizer = 1;
config_gpu.backend = GPU_OFFSET_BACKEND_CUDA;
GpuOffsetConfig config_cpu = config_gpu;
config_cpu.backend = GPU_OFFSET_BACKEND_CPU;
float bounds_min_x = mesh.min_bound.x - config_gpu.pad_mm;
float bounds_min_y = mesh.min_bound.y - config_gpu.pad_mm;
float bounds_max_x = mesh.max_bound.x + config_gpu.pad_mm;
float bounds_max_y = mesh.max_bound.y + config_gpu.pad_mm;
int cols = (int)std::ceil((bounds_max_x - bounds_min_x) / pitch_mm);
int rows = (int)std::ceil((bounds_max_y - bounds_min_y) / pitch_mm);
size_t total_voxels = (size_t)num_layers * cols * rows;
std::cout << "\n[Step 3] Grid Configuration (Pitch: " << pitch_mm << " mm, Line Width: " << layer_width << " mm)" << std::endl;
std::cout << " -> Grid Size: " << cols << " x " << rows << " = " << (cols * rows) << " px/layer" << std::endl;
std::cout << " -> Total 3D Voxel Volume: " << total_voxels << " voxels ("
<< (total_voxels / (1024.0 * 1024.0)) << " MB uint8)" << std::endl;
// 5. Benchmark Hybrid Pipeline: CPU Slicing + GPU Offsetting (if CUDA available)
GpuOffsetStats avg_hybrid = {0};
if (has_cuda) {
std::cout << "\n[Step 4] Benchmarking Pipeline A: Multi-Threaded CPU Slicing + GPU Offsetting (" << runs << " runs)..." << std::endl;
for (int r = 0; r < runs; ++r) {
GpuOffsetStats stats = {0};
GpuLayerResult* results = nullptr;
gpu_offset_polygon_layers(
slice_layers.data(), num_layers,
mesh.min_bound.x, mesh.min_bound.y,
mesh.max_bound.x, mesh.max_bound.y,
&config_gpu, &results, &stats
);
avg_hybrid.t_raster_ms += stats.t_raster_ms;
avg_hybrid.t_h2d_ms += stats.t_h2d_ms;
avg_hybrid.t_gpu_jfa_ms += stats.t_gpu_jfa_ms;
avg_hybrid.t_gpu_thresh_ms += stats.t_gpu_thresh_ms;
avg_hybrid.t_d2h_ms += stats.t_d2h_ms;
avg_hybrid.t_vectorize_ms += stats.t_vectorize_ms;
avg_hybrid.t_total_pipeline_ms += stats.t_total_pipeline_ms;
gpu_offset_free_results(results, num_layers);
}
avg_hybrid.t_raster_ms /= runs;
avg_hybrid.t_h2d_ms /= runs;
avg_hybrid.t_gpu_jfa_ms /= runs;
avg_hybrid.t_gpu_thresh_ms /= runs;
avg_hybrid.t_d2h_ms /= runs;
avg_hybrid.t_vectorize_ms /= runs;
avg_hybrid.t_total_pipeline_ms /= runs;
}
// 6. Benchmark 100% GPU Slicing Pipeline
std::vector<GpuTriangle3D> gpu_tris(mesh.triangles.size());
for (size_t i = 0; i < mesh.triangles.size(); ++i) {
const auto& t = mesh.triangles[i];
gpu_tris[i].v0[0] = t.v0.x; gpu_tris[i].v0[1] = t.v0.y; gpu_tris[i].v0[2] = t.v0.z;
gpu_tris[i].v1[0] = t.v1.x; gpu_tris[i].v1[1] = t.v1.y; gpu_tris[i].v1[2] = t.v1.z;
gpu_tris[i].v2[0] = t.v2.x; gpu_tris[i].v2[1] = t.v2.y; gpu_tris[i].v2[2] = t.v2.z;
}
GpuOffsetStats avg_gpu_slice = {0};
size_t gpu_num_layers = 0;
if (has_cuda) {
std::cout << "\n[Step 5] Benchmarking Pipeline B: 100% Direct GPU Mesh Slicing & Offsetting (" << runs << " runs)..." << std::endl;
for (int r = 0; r < runs; ++r) {
GpuOffsetStats stats = {0};
GpuLayerResult* results = nullptr;
int ret = gpu_offset_slice_mesh_and_offset(
gpu_tris.data(), gpu_tris.size(),
mesh.min_bound.x, mesh.min_bound.y, mesh.min_bound.z,
mesh.max_bound.x, mesh.max_bound.y, mesh.max_bound.z,
layer_height, first_layer_height, &config_gpu, &results, &gpu_num_layers, &stats
);
if (ret != GPU_OFFSET_SUCCESS) {
std::cerr << "[Error] GPU direct slice failed" << std::endl;
return 1;
}
avg_gpu_slice.t_h2d_ms += stats.t_h2d_ms;
avg_gpu_slice.t_mesh_slice_ms += stats.t_mesh_slice_ms;
avg_gpu_slice.t_gpu_jfa_ms += stats.t_gpu_jfa_ms;
avg_gpu_slice.t_gpu_thresh_ms += stats.t_gpu_thresh_ms;
avg_gpu_slice.t_d2h_ms += stats.t_d2h_ms;
avg_gpu_slice.t_vectorize_ms += stats.t_vectorize_ms;
avg_gpu_slice.t_total_pipeline_ms += stats.t_total_pipeline_ms;
gpu_offset_free_results(results, gpu_num_layers);
}
avg_gpu_slice.t_h2d_ms /= runs;
avg_gpu_slice.t_mesh_slice_ms /= runs;
avg_gpu_slice.t_gpu_jfa_ms /= runs;
avg_gpu_slice.t_gpu_thresh_ms /= runs;
avg_gpu_slice.t_d2h_ms /= runs;
avg_gpu_slice.t_vectorize_ms /= runs;
avg_gpu_slice.t_total_pipeline_ms /= runs;
}
// 7. Benchmark 100% CPU Compatibility Pipeline
std::cout << "\n[Step 6] Benchmarking Pipeline C: 100% CPU Compatibility Direct Slicing & Offsetting (" << runs << " runs)..." << std::endl;
GpuOffsetStats avg_cpu_slice = {0};
size_t cpu_num_layers = 0;
for (int r = 0; r < runs; ++r) {
GpuOffsetStats stats = {0};
GpuLayerResult* results = nullptr;
int ret = gpu_offset_slice_mesh_and_offset(
gpu_tris.data(), gpu_tris.size(),
mesh.min_bound.x, mesh.min_bound.y, mesh.min_bound.z,
mesh.max_bound.x, mesh.max_bound.y, mesh.max_bound.z,
layer_height, first_layer_height, &config_cpu, &results, &cpu_num_layers, &stats
);
if (ret != GPU_OFFSET_SUCCESS) {
std::cerr << "[Error] CPU direct slice failed" << std::endl;
return 1;
}
avg_cpu_slice.t_mesh_slice_ms += stats.t_mesh_slice_ms;
avg_cpu_slice.t_total_pipeline_ms += stats.t_total_pipeline_ms;
gpu_offset_free_results(results, cpu_num_layers);
}
avg_cpu_slice.t_mesh_slice_ms /= runs;
avg_cpu_slice.t_total_pipeline_ms /= runs;
// 8. PrusaSlicer Benchmark
double t_prusa_ms = 0;
if (run_prusa) {
std::cout << "\n[Step 7] Running PrusaSlicer Headless Baseline Benchmark..." << std::endl;
std::string cmd = "./bin/prusa-slicer --slice --layer-height=0.2 --perimeters=2 --fill-density=0% --output=prusa_bench.gcode " + stl_path + " > /dev/null 2>&1";
auto t_p0 = std::chrono::high_resolution_clock::now();
int prusa_ret = std::system(cmd.c_str());
auto t_p1 = std::chrono::high_resolution_clock::now();
if (prusa_ret == 0) {
t_prusa_ms = std::chrono::duration<double, std::milli>(t_p1 - t_p0).count();
std::cout << " -> PrusaSlicer completed in " << t_prusa_ms << " ms (" << (t_prusa_ms / 1000.0) << " s)" << std::endl;
}
}
// 9. Summary Table
double total_gpu_slice_ms = t_load_ms + avg_gpu_slice.t_total_pipeline_ms;
double total_cpu_slice_ms = t_load_ms + avg_cpu_slice.t_total_pipeline_ms;
std::cout << "\n" << std::string(88, '=') << std::endl;
std::cout << " BENCHMARK COMPARISON: 100% GPU vs 100% CPU FALLBACK vs PRUSASLICER" << std::endl;
std::cout << std::string(88, '=') << std::endl;
std::cout << std::left << std::setw(54) << "Pipeline / Execution Stage"
<< " | " << std::right << std::setw(11) << "Time (ms)"
<< " | " << std::right << std::setw(14) << "Rate (layer/s)" << std::endl;
std::cout << std::string(88, '-') << std::endl;
auto print_row = [&](const std::string& name, double ms) {
double rate = ms > 0 ? (num_layers / (ms / 1000.0)) : 0.0;
std::cout << std::left << std::setw(54) << name
<< " | " << std::right << std::setw(8) << std::fixed << std::setprecision(2) << ms << " ms"
<< " | " << std::right << std::setw(14) << std::fixed << std::setprecision(1) << rate << std::endl;
};
if (has_cuda) {
std::cout << ">> PIPELINE B: 100% DIRECT GPU MESH SLICING & OFFSETTING <<" << std::endl;
print_row(" 0. STL Mesh Load (Binary C++ Parser)", t_load_ms);
print_row(" 1. Triangle Vertex Buffer PCIe Upload (H2D)", avg_gpu_slice.t_h2d_ms);
print_row(" 2. GPU Direct Mesh Slicing & Rasterization", avg_gpu_slice.t_mesh_slice_ms);
print_row(" 3. GPU Batched 3D CUDA JFA Distance Transform", avg_gpu_slice.t_gpu_jfa_ms);
print_row(" 4. GPU Multi-Perimeter Kernel Thresholding", avg_gpu_slice.t_gpu_thresh_ms);
print_row(" 5. Device-to-Host (D2H) Offset Mask Download", avg_gpu_slice.t_d2h_ms);
print_row(" 6. Multi-Threaded C++ Vectorization (Marching Sq)", avg_gpu_slice.t_vectorize_ms);
std::cout << std::string(88, '-') << std::endl;
print_row("TOTAL 100% GPU PIPELINE (STL -> Sliced Contours)", total_gpu_slice_ms);
std::cout << std::string(88, '-') << std::endl;
}
std::cout << ">> PIPELINE C: 100% MULTI-THREADED CPU FALLBACK PIPELINE <<" << std::endl;
print_row(" 0. STL Mesh Load (Binary C++ Parser)", t_load_ms);
print_row(" 1. Spatial Indexing + CPU Direct Slicing & Raster", avg_cpu_slice.t_mesh_slice_ms);
print_row(" 2. CPU 2D Fast EDT + Thresholding + Vectorization", avg_cpu_slice.t_total_pipeline_ms - avg_cpu_slice.t_mesh_slice_ms);
std::cout << std::string(88, '-') << std::endl;
print_row("TOTAL 100% CPU PIPELINE (STL -> Sliced Contours)", total_cpu_slice_ms);
std::cout << std::string(88, '-') << std::endl;
if (t_prusa_ms > 0) {
std::cout << ">> BASELINE: PRUSASLICER HEADLESS CLI <<" << std::endl;
print_row("PRUSASLICER CLI (Mesh -> Perimeters & G-Code)", t_prusa_ms);
std::cout << std::string(88, '=') << std::endl;
std::cout << "\n>>> SPEEDUP SUMMARY:" << std::endl;
if (has_cuda) {
std::cout << " * 100% GPU Slicing Pipeline : " << std::fixed << std::setprecision(2)
<< (t_prusa_ms / total_gpu_slice_ms) << "x FASTER than PrusaSlicer ("
<< std::fixed << std::setprecision(1) << (num_layers / (total_gpu_slice_ms / 1000.0))
<< " vs " << (num_layers / (t_prusa_ms / 1000.0)) << " layers/sec)" << std::endl;
}
std::cout << " * 100% CPU Fallback Pipeline: " << std::fixed << std::setprecision(2)
<< (t_prusa_ms / total_cpu_slice_ms) << "x FASTER than PrusaSlicer ("
<< std::fixed << std::setprecision(1) << (num_layers / (total_cpu_slice_ms / 1000.0))
<< " vs " << (num_layers / (t_prusa_ms / 1000.0)) << " layers/sec)" << std::endl;
}
std::cout << std::string(88, '=') << std::endl;
// Cleanup
free_slice_layers(slice_layers);
gpu_offset_cleanup();
return 0;
}

273
src/stl_slicer.cpp Normal file
View file

@ -0,0 +1,273 @@
#include "stl_slicer.h"
#include <fstream>
#include <iostream>
#include <cmath>
#include <chrono>
#include <algorithm>
#include <unordered_map>
#include <thread>
#pragma pack(push, 1)
struct StlHeader {
uint8_t header[80];
uint32_t num_triangles;
};
struct StlTriangleRaw {
float normal[3];
float v0[3];
float v1[3];
float v2[3];
uint16_t attribute_byte_count;
};
#pragma pack(pop)
bool load_stl_binary(const std::string& filepath, StlMesh& mesh) {
std::ifstream file(filepath, std::ios::binary);
if (!file) {
std::cerr << "[Error] Cannot open STL file: " << filepath << std::endl;
return false;
}
StlHeader header;
file.read(reinterpret_cast<char*>(&header), sizeof(StlHeader));
if (!file) {
std::cerr << "[Error] Failed to read STL header" << std::endl;
return false;
}
mesh.triangles.resize(header.num_triangles);
mesh.min_bound = { 1e30f, 1e30f, 1e30f };
mesh.max_bound = { -1e30f, -1e30f, -1e30f };
std::vector<StlTriangleRaw> raw_tris(header.num_triangles);
file.read(reinterpret_cast<char*>(raw_tris.data()), header.num_triangles * sizeof(StlTriangleRaw));
for (size_t i = 0; i < header.num_triangles; ++i) {
Triangle3D& tri = mesh.triangles[i];
tri.normal = { raw_tris[i].normal[0], raw_tris[i].normal[1], raw_tris[i].normal[2] };
tri.v0 = { raw_tris[i].v0[0], raw_tris[i].v0[1], raw_tris[i].v0[2] };
tri.v1 = { raw_tris[i].v1[0], raw_tris[i].v1[1], raw_tris[i].v1[2] };
tri.v2 = { raw_tris[i].v2[0], raw_tris[i].v2[1], raw_tris[i].v2[2] };
// Update bounds
const Vec3* verts[3] = { &tri.v0, &tri.v1, &tri.v2 };
for (int v = 0; v < 3; ++v) {
mesh.min_bound.x = std::min(mesh.min_bound.x, verts[v]->x);
mesh.min_bound.y = std::min(mesh.min_bound.y, verts[v]->y);
mesh.min_bound.z = std::min(mesh.min_bound.z, verts[v]->z);
mesh.max_bound.x = std::max(mesh.max_bound.x, verts[v]->x);
mesh.max_bound.y = std::max(mesh.max_bound.y, verts[v]->y);
mesh.max_bound.z = std::max(mesh.max_bound.z, verts[v]->z);
}
}
return true;
}
// Intersect a triangle with a horizontal plane Z = z0
static bool intersect_triangle_plane(const Triangle3D& tri, float z0, LineSegment2D& out_seg) {
float z_min = std::min(std::min(tri.v0.z, tri.v1.z), tri.v2.z);
float z_max = std::max(std::max(tri.v0.z, tri.v1.z), tri.v2.z);
if (z0 < z_min || z0 > z_max || (z_min == z_max)) return false;
const Vec3* v[3] = { &tri.v0, &tri.v1, &tri.v2 };
GpuPoint2D pts[3];
int count = 0;
for (int i = 0; i < 3; ++i) {
int next = (i + 1) % 3;
float za = v[i]->z;
float zb = v[next]->z;
if ((za <= z0 && zb >= z0) || (zb <= z0 && za >= z0)) {
if (std::abs(zb - za) > 1e-7f) {
float t = (z0 - za) / (zb - za);
pts[count].x = v[i]->x + t * (v[next]->x - v[i]->x);
pts[count].y = v[i]->y + t * (v[next]->y - v[i]->y);
count++;
if (count == 2) break;
}
}
}
if (count == 2) {
float dx = pts[1].x - pts[0].x;
float dy = pts[1].y - pts[0].y;
if (dx * dx + dy * dy > 1e-8f) {
out_seg.p0 = pts[0];
out_seg.p1 = pts[1];
return true;
}
}
return false;
}
// Quantized 2D point key for spatial hashing
struct PointKey {
int64_t x, y;
bool operator==(const PointKey& o) const { return x == o.x && y == o.y; }
};
struct PointKeyHash {
size_t operator()(const PointKey& k) const {
return (size_t)(k.x * 73856093 ^ k.y * 19349663);
}
};
static inline PointKey make_point_key(const GpuPoint2D& p, float scale = 1000.0f) {
return PointKey{ (int64_t)std::round(p.x * scale), (int64_t)std::round(p.y * scale) };
}
// Link unordered 2D line segments into closed polygon loops
static void link_segments_into_contours(const std::vector<LineSegment2D>& segments, std::vector<GpuContour>& out_contours) {
if (segments.empty()) return;
std::unordered_multimap<PointKey, size_t, PointKeyHash> start_map;
std::vector<bool> used(segments.size(), false);
for (size_t i = 0; i < segments.size(); ++i) {
start_map.insert({ make_point_key(segments[i].p0), i });
}
for (size_t i = 0; i < segments.size(); ++i) {
if (used[i]) continue;
std::vector<GpuPoint2D> loop;
loop.push_back(segments[i].p0);
loop.push_back(segments[i].p1);
used[i] = true;
GpuPoint2D current_end = segments[i].p1;
GpuPoint2D loop_start = segments[i].p0;
while (true) {
PointKey key = make_point_key(current_end);
auto range = start_map.equal_range(key);
size_t next_idx = (size_t)-1;
for (auto it = range.first; it != range.second; ++it) {
if (!used[it->second]) {
next_idx = it->second;
break;
}
}
if (next_idx != (size_t)-1) {
used[next_idx] = true;
current_end = segments[next_idx].p1;
// Check if loop has closed
PointKey end_key = make_point_key(current_end);
PointKey start_key = make_point_key(loop_start);
if (end_key == start_key || loop.size() > 50000) {
break;
}
loop.push_back(current_end);
} else {
break;
}
}
if (loop.size() >= 3) {
GpuContour c;
c.num_points = loop.size();
c.points = (GpuPoint2D*)malloc(c.num_points * sizeof(GpuPoint2D));
for (size_t p = 0; p < c.num_points; ++p) {
c.points[p] = loop[p];
}
c.is_hole = 0; // Can be determined by signed area if needed
out_contours.push_back(c);
}
}
}
bool slice_stl_mesh(
const StlMesh& mesh,
float layer_height,
float first_layer_height,
std::vector<GpuSliceLayer>& out_layers,
std::vector<float>& z_levels,
double& slice_time_ms
) {
auto t0 = std::chrono::high_resolution_clock::now();
float z_min = mesh.min_bound.z;
float z_max = mesh.max_bound.z;
float h_first = (first_layer_height > 0.0f) ? first_layer_height : layer_height;
z_levels.clear();
for (float z = z_min + h_first; z <= z_max; z += layer_height) {
z_levels.push_back(z);
}
size_t num_layers = z_levels.size();
out_layers.resize(num_layers);
// Parallel multi-threaded slicing across layers
unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency());
std::vector<std::thread> workers;
auto worker_fn = [&](size_t start_idx, size_t end_idx) {
for (size_t l = start_idx; l < end_idx; ++l) {
float z0 = z_levels[l];
std::vector<LineSegment2D> segments;
segments.reserve(1000);
for (const auto& tri : mesh.triangles) {
LineSegment2D seg;
if (intersect_triangle_plane(tri, z0, seg)) {
segments.push_back(seg);
}
}
std::vector<GpuContour> contours;
link_segments_into_contours(segments, contours);
out_layers[l].z_height = z0;
out_layers[l].num_contours = contours.size();
if (!contours.empty()) {
out_layers[l].contours = (GpuContour*)malloc(contours.size() * sizeof(GpuContour));
for (size_t c = 0; c < contours.size(); ++c) {
out_layers[l].contours[c] = contours[c];
}
} else {
out_layers[l].contours = nullptr;
}
}
};
size_t chunk = (num_layers + num_threads - 1) / num_threads;
for (unsigned int t = 0; t < num_threads; ++t) {
size_t start_idx = t * chunk;
size_t end_idx = std::min(start_idx + chunk, num_layers);
if (start_idx < end_idx) {
workers.emplace_back(worker_fn, start_idx, end_idx);
}
}
for (auto& w : workers) {
w.join();
}
auto t1 = std::chrono::high_resolution_clock::now();
slice_time_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
return true;
}
void free_slice_layers(std::vector<GpuSliceLayer>& layers) {
for (auto& layer : layers) {
if (layer.contours) {
for (size_t c = 0; c < layer.num_contours; ++c) {
if (layer.contours[c].points) {
free(layer.contours[c].points);
}
}
free(layer.contours);
layer.contours = nullptr;
}
}
layers.clear();
}

44
src/stl_slicer.h Normal file
View file

@ -0,0 +1,44 @@
#ifndef STL_SLICER_H
#define STL_SLICER_H
#include "gpu_offset.h"
#include <vector>
#include <string>
struct Vec3 {
float x, y, z;
};
struct Triangle3D {
Vec3 normal;
Vec3 v0, v1, v2;
};
struct LineSegment2D {
GpuPoint2D p0;
GpuPoint2D p1;
};
struct StlMesh {
std::vector<Triangle3D> triangles;
Vec3 min_bound;
Vec3 max_bound;
};
// Load binary STL file into memory
bool load_stl_binary(const std::string& filepath, StlMesh& mesh);
// Slices STL mesh into horizontal layers
bool slice_stl_mesh(
const StlMesh& mesh,
float layer_height,
float first_layer_height,
std::vector<GpuSliceLayer>& out_layers,
std::vector<float>& z_levels,
double& slice_time_ms
);
// Free memory allocated for vector layers
void free_slice_layers(std::vector<GpuSliceLayer>& layers);
#endif // STL_SLICER_H

289
src/vectorizer.h Normal file
View file

@ -0,0 +1,289 @@
#ifndef VECTORIZER_H
#define VECTORIZER_H
#include "gpu_offset.h"
#include <vector>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <cstdint>
inline int get_h_edge_id(int x, int y, int stride_w) {
return 2 * (y * stride_w + x) + 0;
}
inline int get_v_edge_id(int x, int y, int stride_w) {
return 2 * (y * stride_w + x) + 1;
}
inline GpuPoint2D get_edge_point(int edge_id, int stride_w, float min_x, float min_y, float pitch_mm) {
int type = edge_id & 1;
int cell = edge_id >> 1;
int y = cell / stride_w;
int x = cell % stride_w;
if (type == 0) {
return GpuPoint2D{ min_x + (x + 0.5f) * pitch_mm, min_y + (float)y * pitch_mm };
} else {
return GpuPoint2D{ min_x + (float)x * pitch_mm, min_y + (y + 0.5f) * pitch_mm };
}
}
static inline void vectorize_mask_layer(
const uint8_t* mask,
int width, int height,
float min_x, float min_y, float pitch_mm,
std::vector<int>& next_edge_buf,
std::vector<int>& active_edges_buf,
std::vector<uint8_t>& visited_buf,
std::vector<GpuContour>& out_contours
) {
int stride_w = width + 1;
active_edges_buf.clear();
auto add_directed_edge = [&](int e0, int e1) {
next_edge_buf[e0] = e1;
visited_buf[e0] = 0;
active_edges_buf.push_back(e0);
};
for (int y = 0; y < height - 1; ++y) {
const uint8_t* row0 = mask + y * width;
const uint8_t* row1 = mask + (y + 1) * width;
for (int x = 0; x < width - 1; ++x) {
int c0 = row0[x];
int c1 = row0[x + 1];
int c2 = row1[x + 1];
int c3 = row1[x];
int case_idx = (c0) | (c1 << 1) | (c2 << 2) | (c3 << 3);
if (case_idx == 0 || case_idx == 15) continue;
int e_bottom = get_h_edge_id(x, y, stride_w);
int e_top = get_h_edge_id(x, y + 1, stride_w);
int e_left = get_v_edge_id(x, y, stride_w);
int e_right = get_v_edge_id(x + 1, y, stride_w);
switch (case_idx) {
case 1: add_directed_edge(e_left, e_bottom); break;
case 2: add_directed_edge(e_bottom, e_right); break;
case 3: add_directed_edge(e_left, e_right); break;
case 4: add_directed_edge(e_right, e_top); break;
case 5: add_directed_edge(e_left, e_top); add_directed_edge(e_bottom, e_right); break;
case 6: add_directed_edge(e_bottom, e_top); break;
case 7: add_directed_edge(e_left, e_top); break;
case 8: add_directed_edge(e_top, e_left); break;
case 9: add_directed_edge(e_top, e_bottom); break;
case 10: add_directed_edge(e_bottom, e_left); add_directed_edge(e_top, e_right); break;
case 11: add_directed_edge(e_top, e_right); break;
case 12: add_directed_edge(e_right, e_left); break;
case 13: add_directed_edge(e_right, e_bottom); break;
case 14: add_directed_edge(e_bottom, e_left); break;
}
}
}
if (active_edges_buf.empty()) return;
std::vector<GpuPoint2D> loop_pts;
loop_pts.reserve(1000);
for (int start_e : active_edges_buf) {
if (visited_buf[start_e]) continue;
loop_pts.clear();
int cur = start_e;
while (cur != -1 && !visited_buf[cur]) {
visited_buf[cur] = 1;
GpuPoint2D pt = get_edge_point(cur, stride_w, min_x, min_y, pitch_mm);
size_t n = loop_pts.size();
if (n >= 2) {
float dx1 = loop_pts[n-1].x - loop_pts[n-2].x;
float dy1 = loop_pts[n-1].y - loop_pts[n-2].y;
float dx2 = pt.x - loop_pts[n-1].x;
float dy2 = pt.y - loop_pts[n-1].y;
float cross = dx1 * dy2 - dy1 * dx2;
if (std::abs(cross) < 1e-6f) {
loop_pts[n-1] = pt; // simplify collinear vertex
} else {
loop_pts.push_back(pt);
}
} else {
loop_pts.push_back(pt);
}
cur = next_edge_buf[cur];
if (cur == start_e) break;
if (loop_pts.size() > 50000) break;
}
if (loop_pts.size() >= 3) {
size_t n = loop_pts.size();
float dx1 = loop_pts[0].x - loop_pts[n-1].x;
float dy1 = loop_pts[0].y - loop_pts[n-1].y;
float dx2 = loop_pts[1].x - loop_pts[0].x;
float dy2 = loop_pts[1].y - loop_pts[0].y;
if (std::abs(dx1 * dy2 - dy1 * dx2) < 1e-6f) {
loop_pts.erase(loop_pts.begin());
}
if (loop_pts.size() >= 3) {
GpuContour c;
c.num_points = loop_pts.size();
c.points = (GpuPoint2D*)malloc(c.num_points * sizeof(GpuPoint2D));
memcpy(c.points, loop_pts.data(), c.num_points * sizeof(GpuPoint2D));
c.is_hole = 0;
out_contours.push_back(c);
}
}
}
// Reset thread-local edge table for zero re-allocation overhead
for (int e : active_edges_buf) {
next_edge_buf[e] = -1;
visited_buf[e] = 0;
}
}
static inline void vectorize_bitpack_layer(
const unsigned int* bit_words,
int width, int height, int row_stride_words,
float min_x, float min_y, float pitch_mm,
std::vector<int>& next_edge_buf,
std::vector<int>& active_edges_buf,
std::vector<uint8_t>& visited_buf,
std::vector<GpuContour>& out_contours
) {
int stride_w = width + 1;
active_edges_buf.clear();
auto add_directed_edge = [&](int e0, int e1) {
next_edge_buf[e0] = e1;
visited_buf[e0] = 0;
active_edges_buf.push_back(e0);
};
for (int y = 0; y < height - 1; ++y) {
const unsigned int* row0 = bit_words + (size_t)y * row_stride_words;
const unsigned int* row1 = bit_words + (size_t)(y + 1) * row_stride_words;
for (int w = 0; w < row_stride_words; ++w) {
unsigned int w0 = row0[w];
unsigned int w1 = row1[w];
unsigned int w0_next = (w + 1 < row_stride_words) ? row0[w + 1] : 0;
unsigned int w1_next = (w + 1 < row_stride_words) ? row1[w + 1] : 0;
if ((w0 == 0 && w1 == 0 && w0_next == 0 && w1_next == 0) ||
(w0 == 0xFFFFFFFF && w1 == 0xFFFFFFFF && (w0_next & 1) && (w1_next & 1))) {
continue;
}
int x_base = w * 32;
for (int b = 0; b < 32; ++b) {
int x = x_base + b;
if (x >= width - 1) break;
int c0 = (w0 >> b) & 1;
int c1 = (b < 31) ? ((w0 >> (b + 1)) & 1) : (w0_next & 1);
int c2 = (b < 31) ? ((w1 >> (b + 1)) & 1) : (w1_next & 1);
int c3 = (w1 >> b) & 1;
int case_idx = c0 | (c1 << 1) | (c2 << 2) | (c3 << 3);
if (case_idx == 0 || case_idx == 15) continue;
int e_bottom = get_h_edge_id(x, y, stride_w);
int e_top = get_h_edge_id(x, y + 1, stride_w);
int e_left = get_v_edge_id(x, y, stride_w);
int e_right = get_v_edge_id(x + 1, y, stride_w);
switch (case_idx) {
case 1: add_directed_edge(e_left, e_bottom); break;
case 2: add_directed_edge(e_bottom, e_right); break;
case 3: add_directed_edge(e_left, e_right); break;
case 4: add_directed_edge(e_right, e_top); break;
case 5: add_directed_edge(e_left, e_top); add_directed_edge(e_bottom, e_right); break;
case 6: add_directed_edge(e_bottom, e_top); break;
case 7: add_directed_edge(e_left, e_top); break;
case 8: add_directed_edge(e_top, e_left); break;
case 9: add_directed_edge(e_top, e_bottom); break;
case 10: add_directed_edge(e_bottom, e_left); add_directed_edge(e_top, e_right); break;
case 11: add_directed_edge(e_top, e_right); break;
case 12: add_directed_edge(e_right, e_left); break;
case 13: add_directed_edge(e_right, e_bottom); break;
case 14: add_directed_edge(e_bottom, e_left); break;
}
}
}
}
if (active_edges_buf.empty()) return;
std::vector<GpuPoint2D> loop_pts;
loop_pts.reserve(1000);
for (int start_e : active_edges_buf) {
if (visited_buf[start_e]) continue;
loop_pts.clear();
int cur = start_e;
while (cur != -1 && !visited_buf[cur]) {
visited_buf[cur] = 1;
GpuPoint2D pt = get_edge_point(cur, stride_w, min_x, min_y, pitch_mm);
size_t n = loop_pts.size();
if (n >= 2) {
float dx1 = loop_pts[n-1].x - loop_pts[n-2].x;
float dy1 = loop_pts[n-1].y - loop_pts[n-2].y;
float dx2 = pt.x - loop_pts[n-1].x;
float dy2 = pt.y - loop_pts[n-1].y;
float cross = dx1 * dy2 - dy1 * dx2;
if (std::abs(cross) < 1e-6f) {
loop_pts[n-1] = pt; // simplify collinear vertex
} else {
loop_pts.push_back(pt);
}
} else {
loop_pts.push_back(pt);
}
cur = next_edge_buf[cur];
if (cur == start_e) break;
if (loop_pts.size() > 50000) break;
}
if (loop_pts.size() >= 3) {
size_t n = loop_pts.size();
float dx1 = loop_pts[0].x - loop_pts[n-1].x;
float dy1 = loop_pts[0].y - loop_pts[n-1].y;
float dx2 = loop_pts[1].x - loop_pts[0].x;
float dy2 = loop_pts[1].y - loop_pts[0].y;
if (std::abs(dx1 * dy2 - dy1 * dx2) < 1e-6f) {
loop_pts.erase(loop_pts.begin());
}
if (loop_pts.size() >= 3) {
GpuContour c;
c.num_points = loop_pts.size();
c.points = (GpuPoint2D*)malloc(c.num_points * sizeof(GpuPoint2D));
memcpy(c.points, loop_pts.data(), c.num_points * sizeof(GpuPoint2D));
c.is_hole = 0;
out_contours.push_back(c);
}
}
}
// Reset thread-local edge table for zero re-allocation overhead
for (int e : active_edges_buf) {
next_edge_buf[e] = -1;
visited_buf[e] = 0;
}
}
#endif // VECTORIZER_H