This commit is contained in:
Nicolas Predella 2026-09-26 16:15:44 +03:00
parent 1a66dce5c4
commit 7f4948fa03
13 changed files with 456 additions and 623 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.18) cmake_minimum_required(VERSION 3.18)
project(cudaslicer LANGUAGES C CXX CUDA) project(cudaslicer VERSION 1.0.0 LANGUAGES C CXX CUDA)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)

View file

@ -45,4 +45,7 @@ $(BUILD_DIR) $(LIB_DIR) $(BIN_DIR):
clean: clean:
rm -rf $(BUILD_DIR) $(LIB_DIR) $(BIN_DIR) rm -rf $(BUILD_DIR) $(LIB_DIR) $(BIN_DIR)
.PHONY: all clean wheel:
python3 setup.py bdist_wheel
.PHONY: all clean wheel

View file

@ -141,6 +141,80 @@ perims, infill_mask = layer_0
print(f"Layer 0 (Z = {layer_0.z} mm): {len(perims)} perimeters, infill mask: {infill_mask.shape}") print(f"Layer 0 (Z = {layer_0.z} mm): {len(perims)} perimeters, infill mask: {infill_mask.shape}")
``` ```
#### E. GPU Signed Distance Fields (SDF), Boolean CSG & Gyroid Infill
Execute boolean CSG operations (Union, Difference, Intersection, Smooth Blending) and 3D procedural infill directly on GPU SDF stacks with zero CPU memory roundtrips:
```python
import trimesh
import cudaslicer
mesh_a = trimesh.load("benchy.stl")
cutter_box = trimesh.creation.box(extents=(12.0, 60.0, 25.0))
# 1. Direct GPU slice to continuous SDF stacks
sdf_a = cudaslicer.slice_mesh_to_sdf(mesh_a, layer_height=0.20, pitch_mm=0.10)
sdf_b = cudaslicer.slice_mesh_to_sdf(cutter_box, layer_height=0.20, pitch_mm=0.10)
# 2. Boolean CSG subtraction entirely in GPU VRAM
sdf_carved = sdf_a.difference(sdf_b.offset(delta_mm=0.5))
# 3. GPU procedural 3D gyroid infill generation
sdf_infill = sdf_carved.generate_infill(pattern="gyroid", cell_size_mm=4.0, density=0.25)
# 4. Direct vectorization with sub-pixel LERP and RDP polyline simplification
layers = sdf_infill.vectorize(
perimeters_count=2,
layer_width=0.45,
subpixel_lerp=True, # Natural arbitrary angles (no 45° quantization)
simplify_tolerance_mm=0.02 # RDP polyline decimation
)
```
---
## 📐 Sub-Pixel Linear Interpolation (LERP) vs. Standard Marching Squares
### The Problem: 45° & 90° Angle Quantization in Standard Marching Squares
Standard discrete Marching Squares (and binary mask polygon extractors) evaluates binary cell states (0 or 1 at each voxel corner). When an edge intersects a boundary, it snaps the intersection point to the **geometric midpoint** of the grid cell edge ($t = 0.5$):
```
Discrete Marching Squares (Snap to t = 0.5):
(x, y+1) +---------+ (x+1, y+1)
| * | <- Midpoint snapping forces line segments to connect
| / | only at (0.5, 0), (0, 0.5), (1, 0.5), etc.
| * | <- Result: angles restricted to 0°, 45°, 90°, 135°!
(x, y) +---------+ (x+1, y)
```
Because segment vectors between grid edge midpoints are strictly constrained to Cartesian diagonals, diagonal walls and curves are quantized into jagged 45° chamfers and stair-stepped facets.
---
### The Solution: Continuous SDF Linear Interpolation (LERP)
In CUDASlicer, the vectorizer uses continuous floating-point Signed Distance Field (SDF) values ($d_0, d_1$) at the cell vertices to solve for the exact sub-pixel boundary crossing where the distance field equals the target isovalue ($iso\_val = 0.0$ for outer perimeter, $-d$ for offset contours):
$$t = \frac{iso\_val - d_0}{d_1 - d_0}, \quad t \in [0.0, 1.0]$$
```
Continuous SDF LERP (Sub-Pixel Placement):
(x, y+1) +---------+ (x+1, y+1)
| |
| * | <- Exact root crossing t = (0 - d0) / (d1 - d0)
| / | <- Arbitrary natural angles (e.g. 12.3°, 37.8°, smooth curves)
(x, y) +-----*---+ (x+1, y)
```
- **Horizontal Edge:** $P_x = \text{min\_x} + (x + t) \cdot \text{pitch}, \quad P_y = \text{min\_y} + y \cdot \text{pitch}$
- **Vertical Edge:** $P_x = \text{min\_x} + x \cdot \text{pitch}, \quad P_y = \text{min\_y} + (y + t) \cdot \text{pitch}$
Since $t$ varies continuously in $[0, 1]$, vertices fall at their true Euclidean geometric boundaries, eliminating jagged 45° artifacts.
---
### Polyline Simplification & Decimation
1. **On-the-Fly Collinear Decimation:** During loop traversal, collinear vertices ($\Delta x_1 \Delta y_2 - \Delta y_1 \Delta x_2 \approx 0$) along straight spans are pruned on the fly with zero memory overhead.
2. **Ramer-Douglas-Peucker (RDP) Polyline Simplification:** Traversed closed loops are post-processed with `simplify_contour_rdp(loop, tolerance_mm)` (default tolerance `0.02 mm`), removing sub-pixel jitter while preserving smooth curved contours and exact sharp corners.
--- ---
## 🛠️ Building Standalone C/C++ Libraries (Windows & Linux) ## 🛠️ Building Standalone C/C++ Libraries (Windows & Linux)

View file

@ -36,7 +36,7 @@ NC='\033[0m' # No Color
DO_CLEAN=false DO_CLEAN=false
DO_TEST=false DO_TEST=false
DO_INSTALL=false DO_INSTALL=false
DO_WHEEL=false DO_WHEEL=true
BUILD_TYPE="Release" BUILD_TYPE="Release"
BUILD_C_ONLY=false BUILD_C_ONLY=false
NUM_JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) NUM_JOBS=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
@ -60,8 +60,13 @@ while [[ $# -gt 0 ]]; do
DO_WHEEL=true DO_WHEEL=true
shift shift
;; ;;
--no-wheel)
DO_WHEEL=false
shift
;;
--c-only) --c-only)
BUILD_C_ONLY=true BUILD_C_ONLY=true
DO_WHEEL=false
shift shift
;; ;;
--debug) --debug)
@ -84,7 +89,8 @@ while [[ $# -gt 0 ]]; do
echo " --clean Remove build directory, libraries, and binaries first" echo " --clean Remove build directory, libraries, and binaries first"
echo " --test Run unit tests and benchmarks upon completion" echo " --test Run unit tests and benchmarks upon completion"
echo " --install Install Python package into active environment via pip" echo " --install Install Python package into active environment via pip"
echo " --wheel Build standalone Python .whl distribution package in dist/" echo " --wheel Build standalone Python .whl distribution package in dist/ (default: enabled)"
echo " --no-wheel Skip building Python .whl distribution package"
echo " --c-only Build only C/C++ libraries (lib/ and bin/)" echo " --c-only Build only C/C++ libraries (lib/ and bin/)"
echo " --debug Compile with Debug flags" echo " --debug Compile with Debug flags"
echo " --release Compile with Release flags (default)" echo " --release Compile with Release flags (default)"
@ -173,9 +179,9 @@ if [ "$BUILD_C_ONLY" = false ]; then
echo -e " * Python Backend: ${CYAN}python/cudaslicer/_cudaslicer_backend*.so${NC}" echo -e " * Python Backend: ${CYAN}python/cudaslicer/_cudaslicer_backend*.so${NC}"
fi fi
# Optional Python Wheel Package Build # Python Wheel Package Build
if [ "$DO_WHEEL" = true ]; then if [ "$DO_WHEEL" = true ]; then
echo -e "\n${BLUE}[Optional] Building Python Wheel distribution (.whl)...${NC}" echo -e "\n${BLUE}[5/5] Building Python Wheel package (.whl)...${NC}"
mkdir -p dist mkdir -p dist
python3 setup.py bdist_wheel python3 setup.py bdist_wheel
WHL_FILE=$(ls -t dist/*.whl 2>/dev/null | head -n 1 || true) WHL_FILE=$(ls -t dist/*.whl 2>/dev/null | head -n 1 || true)
@ -193,7 +199,7 @@ fi
# Optional Tests & Benchmarks # Optional Tests & Benchmarks
if [ "$DO_TEST" = true ]; then if [ "$DO_TEST" = true ]; then
echo -e "\n${BLUE}[5/5] Running Test Suite & Benchmarks...${NC}" echo -e "\n${BLUE}[Test Suite] Running Tests & Benchmarks...${NC}"
echo -e "\n--- [A] Running Python GPU SDF & CSG Unit Tests ---" echo -e "\n--- [A] Running Python GPU SDF & CSG Unit Tests ---"
python3 tests_and_tools/test_gpu_sdf.py python3 tests_and_tools/test_gpu_sdf.py

View file

@ -40,16 +40,7 @@ typedef enum {
GPU_SDF_LAYER_MATCH_INTERSECTION = 2 // Output only exact matching layer Z heights GPU_SDF_LAYER_MATCH_INTERSECTION = 2 // Output only exact matching layer Z heights
} GpuSdfLayerMatchMode; } GpuSdfLayerMatchMode;
/**
* @brief Procedural 3D Infill Pattern Types
*/
typedef enum {
GPU_SDF_INFILL_SOLID = 0,
GPU_SDF_INFILL_GRID = 1,
GPU_SDF_INFILL_GYROID = 2,
GPU_SDF_INFILL_SCHWARZ_P = 3,
GPU_SDF_INFILL_SCHWARZ_D = 4
} GpuSdfInfillPattern;
/** /**
* @brief Spatial metadata for a 2D/3D GPU Signed Distance Field stack * @brief Spatial metadata for a 2D/3D GPU Signed Distance Field stack
@ -180,16 +171,7 @@ GPU_SDF_API GpuSdfStackHandle gpu_sdf_shell(
float thickness_mm float thickness_mm
); );
/**
* @brief Generate a procedural 3D infill pattern intersected with the interior region.
*/
GPU_SDF_API GpuSdfStackHandle gpu_sdf_generate_infill(
GpuSdfStackHandle stack,
GpuSdfInfillPattern pattern,
float cell_size_mm,
float density_ratio,
float wall_offset_mm
);
/** /**
* @brief Vectorize GPU SDF stack directly to polygon perimeter contours and infill mask. * @brief Vectorize GPU SDF stack directly to polygon perimeter contours and infill mask.

View file

@ -4,10 +4,20 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "cudaslicer" name = "cudaslicer"
version = "1.1.0" version = "1.0.0"
description = "GPU-Accelerated 2D/3D Slicing, Signed Distance Fields (SDF) & Polygon Offsetting Library" description = "GPU-Accelerated 2D/3D Slicing, Signed Distance Fields (SDF) & Polygon Offsetting Library"
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.8"
authors = [
{ name = "CUDA Slicer Team" }
]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: C++",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Topic :: Scientific/Engineering",
]
dependencies = [ dependencies = [
"numpy>=1.20.0" "numpy>=1.20.0"
] ]

View file

@ -9,7 +9,7 @@ with automatic fallback and dynamic backend switching.
from typing import List, Dict, Union, Any, Optional, Sequence, Tuple from typing import List, Dict, Union, Any, Optional, Sequence, Tuple
import numpy as np import numpy as np
__version__ = "1.1.0" __version__ = "1.0.0"
# Try importing the compiled C++/CUDA pybind11 module # Try importing the compiled C++/CUDA pybind11 module
try: try:
@ -269,7 +269,9 @@ def slice_mesh_and_offset(
pad_mm: float = 1.5, pad_mm: float = 1.5,
return_infill_mask: bool = True, return_infill_mask: bool = True,
offset_distances: Optional[Sequence[float]] = None, offset_distances: Optional[Sequence[float]] = None,
backend: Optional[str] = "auto" backend: Optional[str] = "auto",
simplify_tolerance_mm: float = 0.0,
subpixel_lerp: bool = True
) -> List[LayerResult]: ) -> List[LayerResult]:
""" """
3D mesh slicing, rasterization, distance field, and perimeter offsetting (CUDA or CPU). 3D mesh slicing, rasterization, distance field, and perimeter offsetting (CUDA or CPU).
@ -296,6 +298,10 @@ def slice_mesh_and_offset(
Custom offset distances. Custom offset distances.
backend : str, optional, default='auto' backend : str, optional, default='auto'
Compute backend: 'auto' (CUDA if available, else CPU), 'cuda' (require CUDA), or 'cpu'. Compute backend: 'auto' (CUDA if available, else CPU), 'cuda' (require CUDA), or 'cpu'.
simplify_tolerance_mm : float, default=0.0
Ramer-Douglas-Peucker (RDP) polygon simplification tolerance in mm (0.0 to disable).
subpixel_lerp : bool, default=True
Continuous distance-field sub-pixel linear interpolation for smooth arbitrary angles.
Returns Returns
------- -------
@ -338,7 +344,9 @@ def slice_mesh_and_offset(
int(perimeters_count), float(layer_width), int(perimeters_count), float(layer_width),
float(pitch_mm), float(pad_mm), float(pitch_mm), float(pad_mm),
bool(return_infill_mask), offsets_float, bool(return_infill_mask), offsets_float,
backend_str backend_str,
float(simplify_tolerance_mm),
bool(subpixel_lerp)
) )
return [LayerResult(r) for r in raw_results] return [LayerResult(r) for r in raw_results]
@ -412,15 +420,7 @@ class SdfStack:
"""Hollow / shell the volume with specified wall thickness on GPU.""" """Hollow / shell the volume with specified wall thickness on GPU."""
return SdfStack(self._stack.shell(float(thickness_mm))) return SdfStack(self._stack.shell(float(thickness_mm)))
def generate_infill(
self,
pattern: str = "gyroid",
cell_size_mm: float = 5.0,
density: float = 0.20,
wall_offset_mm: float = 1.0
) -> "SdfStack":
"""Generate continuous 3D procedural lattice (gyroid, schwarz_p, schwarz_d, grid) on GPU."""
return SdfStack(self._stack.generate_infill(str(pattern), float(cell_size_mm), float(density), float(wall_offset_mm)))
def vectorize( def vectorize(
self, self,

View file

@ -252,7 +252,9 @@ py::list py_slice_mesh_and_offset(
float pad_mm, float pad_mm,
bool export_infill_mask, bool export_infill_mask,
const std::vector<float>& offset_distances, const std::vector<float>& offset_distances,
const std::string& backend const std::string& backend,
float simplify_tolerance_mm,
bool subpixel_lerp
) { ) {
py::buffer_info buf = triangles_arr.request(); py::buffer_info buf = triangles_arr.request();
size_t num_tris = 0; size_t num_tris = 0;
@ -275,6 +277,8 @@ py::list py_slice_mesh_and_offset(
config.layer_width = layer_width; config.layer_width = layer_width;
config.first_layer_height = first_layer_height; config.first_layer_height = first_layer_height;
config.export_infill_mask = export_infill_mask ? 1 : 0; config.export_infill_mask = export_infill_mask ? 1 : 0;
config.simplify_tolerance_mm = simplify_tolerance_mm;
config.subpixel_lerp = subpixel_lerp ? 1 : 0;
config.use_gpu_rasterizer = 1; config.use_gpu_rasterizer = 1;
config.backend = parse_backend_str(backend); config.backend = parse_backend_str(backend);
@ -505,24 +509,6 @@ public:
return std::make_shared<PyGpuSdfStack>(res); return std::make_shared<PyGpuSdfStack>(res);
} }
std::shared_ptr<PyGpuSdfStack> generate_infill(
const std::string& pattern_str = "gyroid",
float cell_size_mm = 5.0f,
float density_ratio = 0.20f,
float wall_offset_mm = 1.0f
) const {
GpuSdfInfillPattern pat = GPU_SDF_INFILL_GYROID;
if (pattern_str == "gyroid") pat = GPU_SDF_INFILL_GYROID;
else if (pattern_str == "schwarz_p" || pattern_str == "schwarz-p") pat = GPU_SDF_INFILL_SCHWARZ_P;
else if (pattern_str == "schwarz_d" || pattern_str == "schwarz-d") pat = GPU_SDF_INFILL_SCHWARZ_D;
else if (pattern_str == "grid") pat = GPU_SDF_INFILL_GRID;
else throw std::invalid_argument("Unknown infill pattern: " + pattern_str);
GpuSdfStackHandle res = gpu_sdf_generate_infill(handle_, pat, cell_size_mm, density_ratio, wall_offset_mm);
if (!res) throw std::runtime_error("SDF infill generation failed.");
return std::make_shared<PyGpuSdfStack>(res);
}
py::list vectorize( py::list vectorize(
int perimeters_count = 2, int perimeters_count = 2,
float layer_width = 0.45f, float layer_width = 0.45f,
@ -760,6 +746,8 @@ PYBIND11_MODULE(_cudaslicer_backend, m) {
py::arg("export_infill_mask") = true, py::arg("export_infill_mask") = true,
py::arg("offset_distances") = std::vector<float>(), py::arg("offset_distances") = std::vector<float>(),
py::arg("backend") = "auto", py::arg("backend") = "auto",
py::arg("simplify_tolerance_mm") = 0.0f,
py::arg("subpixel_lerp") = true,
"Mesh slicing, perimeter offsetting, and infill mask generation."); "Mesh slicing, perimeter offsetting, and infill mask generation.");
// GPU SDF Stack Factory Functions // GPU SDF Stack Factory Functions
@ -798,12 +786,6 @@ PYBIND11_MODULE(_cudaslicer_backend, m) {
.def("op_difference", &PyGpuSdfStack::op_difference, py::arg("other"), py::arg("blend_radius_mm") = 0.0f, "Compute A \\ B on GPU.") .def("op_difference", &PyGpuSdfStack::op_difference, py::arg("other"), py::arg("blend_radius_mm") = 0.0f, "Compute A \\ B on GPU.")
.def("offset", &PyGpuSdfStack::offset, py::arg("delta_mm"), "Offset/dilate (delta > 0) or inset/erode (delta < 0) the SDF.") .def("offset", &PyGpuSdfStack::offset, py::arg("delta_mm"), "Offset/dilate (delta > 0) or inset/erode (delta < 0) the SDF.")
.def("shell", &PyGpuSdfStack::shell, py::arg("thickness_mm"), "Hollow/shell the SDF with specified wall thickness.") .def("shell", &PyGpuSdfStack::shell, py::arg("thickness_mm"), "Hollow/shell the SDF with specified wall thickness.")
.def("generate_infill", &PyGpuSdfStack::generate_infill,
py::arg("pattern_str") = "gyroid",
py::arg("cell_size_mm") = 5.0f,
py::arg("density_ratio") = 0.20f,
py::arg("wall_offset_mm") = 1.0f,
"Intersect interior volume with 3D procedural infill lattice directly on GPU.")
.def("vectorize", &PyGpuSdfStack::vectorize, .def("vectorize", &PyGpuSdfStack::vectorize,
py::arg("perimeters_count") = 2, py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f, py::arg("layer_width") = 0.45f,

View file

@ -39,24 +39,9 @@ class CMakeBuild(build_ext):
setup( setup(
name="cudaslicer", name="cudaslicer",
version="1.1.0", 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"}, package_dir={"": "python"},
packages=find_packages(where="python"), packages=find_packages(where="python"),
ext_modules=[CMakeExtension("cudaslicer._cudaslicer_backend")], ext_modules=[CMakeExtension("cudaslicer._cudaslicer_backend")],
cmdclass={"build_ext": CMakeBuild}, 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",
],
) )

View file

@ -532,7 +532,7 @@ int cpu_offset_slice_mesh_and_offset(
auto t_idx_1 = std::chrono::high_resolution_clock::now(); 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(); 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 // 2. Parallel Layer Slicing, Parity Scan, Exact Segment SDF, Thresholding, Vectorization
GpuLayerResult* res_array = (GpuLayerResult*)malloc(num_layers * sizeof(GpuLayerResult)); GpuLayerResult* res_array = (GpuLayerResult*)malloc(num_layers * sizeof(GpuLayerResult));
memset(res_array, 0, num_layers * sizeof(GpuLayerResult)); memset(res_array, 0, num_layers * sizeof(GpuLayerResult));
@ -549,16 +549,18 @@ int cpu_offset_slice_mesh_and_offset(
std::vector<unsigned int> parity_words((size_t)rows * row_stride_words, 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<uint8_t> layer_mask(layer_pixels, 0);
std::vector<float> dist_sq(layer_pixels); std::vector<float> layer_sdf(layer_pixels);
std::vector<float> g_col_buf(layer_pixels);
std::vector<float> row_f(cols), row_d(cols); struct CpuSegment2D {
std::vector<int> v_buf(cols + 1); float x0, y0, x1, y1;
std::vector<float> z_buf(cols + 2); };
std::vector<unsigned int> bit_words((size_t)rows * row_stride_words); std::vector<CpuSegment2D> layer_segs;
layer_segs.reserve(256);
for (size_t l = start_layer; l < end_layer; ++l) { for (size_t l = start_layer; l < end_layer; ++l) {
float z0 = z_first + l * layer_height; float z0 = z_first + l * layer_height;
std::fill(parity_words.begin(), parity_words.end(), 0u); std::fill(parity_words.begin(), parity_words.end(), 0u);
layer_segs.clear();
// Slicing & Parity Rasterization for Layer l // Slicing & Parity Rasterization for Layer l
const auto& tri_indices = layer_tri_indices[l]; const auto& tri_indices = layer_tri_indices[l];
@ -588,29 +590,33 @@ int cpu_offset_slice_mesh_and_offset(
if (count != 2) continue; if (count != 2) continue;
float gx0 = (pts_x[0] - bounds_min_x) / pitch - 0.5f; layer_segs.push_back({ pts_x[0], pts_y[0], pts_x[1], pts_y[1] });
float gy0 = (pts_y[0] - bounds_min_y) / pitch - 0.5f;
float gx1 = (pts_x[1] - bounds_min_x) / pitch - 0.5f; float gx0 = (pts_x[0] - bounds_min_x) / pitch;
float gy1 = (pts_y[1] - bounds_min_y) / pitch - 0.5f; float gy0 = (pts_y[0] - bounds_min_y) / pitch;
float gx1 = (pts_x[1] - bounds_min_x) / pitch;
float gy1 = (pts_y[1] - bounds_min_y) / pitch;
if (gy0 == gy1) continue;
if (gy0 > gy1) { if (gy0 > gy1) {
std::swap(gx0, gx1); std::swap(gx0, gx1);
std::swap(gy0, gy1); std::swap(gy0, gy1);
} }
float dy = gy1 - gy0;
if (dy < 1e-6f) continue;
int y_start = (int)std::ceil(gy0); int y_start = (int)std::ceil(gy0);
int y_end = (int)std::floor(gy1 - 1e-6f); int y_end = (int)std::ceil(gy1) - 1;
if (y_start < 0) y_start = 0; if (y_start < 0) y_start = 0;
if (y_end >= rows) y_end = rows - 1; if (y_end >= rows) y_end = rows - 1;
float dy = gy1 - gy0;
if (dy < 1e-7f) continue;
for (int y = y_start; y <= y_end; ++y) { for (int y = y_start; y <= y_end; ++y) {
float t = ((float)y - gy0) / dy; float t = ((float)y - gy0) / dy;
float gx = gx0 + t * (gx1 - gx0); float gx = gx0 + t * (gx1 - gx0);
int x_int = (int)std::round(gx + 0.5f); int x_int = (int)std::ceil(gx);
if (x_int < 0) x_int = 0; if (x_int < 0) x_int = 0;
if (x_int >= cols) x_int = cols - 1; if (x_int >= cols) x_int = cols - 1;
@ -635,43 +641,46 @@ int cpu_offset_slice_mesh_and_offset(
} }
} }
// Compute exact 2D EDT // Compute exact continuous signed distance field from analytical layer segments
edt_2d_layer(layer_mask.data(), cols, rows, dist_sq.data(), int num_segs = (int)layer_segs.size();
g_col_buf, row_f, row_d, v_buf, z_buf); for (int y = 0; y < rows; ++y) {
float py = bounds_min_y + (float)y * pitch;
for (int x = 0; x < cols; ++x) {
float px = bounds_min_x + (float)x * pitch;
float min_d2 = 1e20f;
for (int s = 0; s < num_segs; ++s) {
float sx0 = layer_segs[s].x0;
float sy0 = layer_segs[s].y0;
float sx1 = layer_segs[s].x1;
float sy1 = layer_segs[s].y1;
float dx = sx1 - sx0;
float dy = sy1 - sy0;
float l2 = dx * dx + dy * dy;
float t = (l2 > 1e-12f) ? ((px - sx0) * dx + (py - sy0) * dy) / l2 : 0.0f;
t = std::max(0.0f, std::min(1.0f, t));
float qx = sx0 + t * dx;
float qy = sy0 + t * dy;
float d2 = (px - qx) * (px - qx) + (py - qy) * (py - qy);
if (d2 < min_d2) min_d2 = d2;
}
float d = sqrtf(min_d2);
uint8_t m = layer_mask[(size_t)y * cols + x];
layer_sdf[(size_t)y * cols + x] = (m > 0) ? d : -d;
}
}
res_array[l].z_height = z0; res_array[l].z_height = z0;
res_array[l].num_perimeters = P; res_array[l].num_perimeters = P;
res_array[l].perimeters = (GpuOffsetPerimeter*)malloc(P * sizeof(GpuOffsetPerimeter)); res_array[l].perimeters = (GpuOffsetPerimeter*)malloc(P * sizeof(GpuOffsetPerimeter));
// Perimeters // Perimeters with continuous SDF and sub-pixel LERP
for (int o = 0; o < P; ++o) { for (int o = 0; o < P; ++o) {
float thresh = resolved_offsets[o]; float iso_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; std::vector<GpuContour> contours;
vectorize_bitpack_layer(bit_words.data(), cols, rows, row_stride_words, vectorize_sdf_layer(layer_sdf.data(), cols, rows, bounds_min_x, bounds_min_y, pitch,
bounds_min_x, bounds_min_y, pitch, iso_thresh, config->simplify_tolerance_mm, config->subpixel_lerp,
next_edge_buf, active_edges_buf, visited_buf, contours); 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].offset_distance_mm = resolved_offsets[o];
res_array[l].perimeters[o].num_contours = contours.size(); res_array[l].perimeters[o].num_contours = contours.size();
@ -684,35 +693,13 @@ int cpu_offset_slice_mesh_and_offset(
} }
// Infill pass // Infill pass
float infill_thresh = resolved_offsets[P]; float infill_iso_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; std::vector<GpuContour> infill_contours;
vectorize_bitpack_layer(bit_words.data(), cols, rows, row_stride_words, vectorize_sdf_layer(layer_sdf.data(), cols, rows, bounds_min_x, bounds_min_y, pitch,
bounds_min_x, bounds_min_y, pitch, infill_iso_thresh, config->simplify_tolerance_mm, config->subpixel_lerp,
next_edge_buf, active_edges_buf, visited_buf, infill_contours); next_edge_buf, active_edges_buf, visited_buf, infill_contours);
res_array[l].infill.offset_distance_mm = resolved_offsets[P]; res_array[l].infill.offset_distance_mm = infill_iso_thresh;
res_array[l].infill.width = cols; res_array[l].infill.width = cols;
res_array[l].infill.height = rows; res_array[l].infill.height = rows;
res_array[l].infill.num_contours = infill_contours.size(); res_array[l].infill.num_contours = infill_contours.size();
@ -724,17 +711,9 @@ int cpu_offset_slice_mesh_and_offset(
} }
if (config->export_infill_mask) { if (config->export_infill_mask) {
uint8_t* infill_mask = (uint8_t*)malloc((size_t)cols * rows); uint8_t* infill_mask = (uint8_t*)malloc(layer_pixels);
for (int y = 0; y < rows; ++y) { for (size_t i = 0; i < layer_pixels; ++i) {
const unsigned int* row_words = bit_words.data() + (size_t)y * row_stride_words; infill_mask[i] = (layer_sdf[i] >= infill_iso_thresh) ? 1 : 0;
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; res_array[l].infill.mask = infill_mask;
} else { } else {

View file

@ -1,4 +1,5 @@
#include "gpu_offset.h" #include "gpu_offset.h"
#include "gpu_sdf.h"
#include "cpu_offset.h" #include "cpu_offset.h"
#include "vectorizer.h" #include "vectorizer.h"
#include <cuda_runtime.h> #include <cuda_runtime.h>
@ -763,295 +764,25 @@ GPU_OFFSET_API int gpu_offset_slice_mesh_and_offset(
if (gpu_offset_init(0) != GPU_OFFSET_SUCCESS) return GPU_OFFSET_ERR_GENERIC; if (gpu_offset_init(0) != GPU_OFFSET_SUCCESS) return GPU_OFFSET_ERR_GENERIC;
} }
auto t_total_start = std::chrono::high_resolution_clock::now();
float pad = (config->pad_mm > 0.0f) ? config->pad_mm : 1.5f; 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 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; float h_first = first_layer_height;
if (h_first <= 0.0f) h_first = config->first_layer_height; if (h_first <= 0.0f) h_first = config->first_layer_height;
if (h_first <= 0.0f) h_first = layer_height; if (h_first <= 0.0f) h_first = layer_height;
float z_first = min_z + h_first; GpuSdfStackHandle stack = gpu_sdf_from_mesh(
int num_layers = 0; triangles, num_triangles,
if (max_z >= z_first) { min_x, min_y, min_z, max_x, max_y, max_z,
num_layers = (int)std::floor((max_z - z_first) / layer_height) + 1; layer_height, h_first,
pitch, pad
);
if (!stack) {
return GPU_OFFSET_ERR_GENERIC;
} }
if (num_layers <= 0) return GPU_OFFSET_ERR_INVALID_ARG; int ret = gpu_sdf_vectorize(stack, config, out_results, out_num_layers, stats);
if (out_num_layers) *out_num_layers = num_layers; gpu_sdf_destroy(stack);
return ret;
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;
}
resolved_offsets[P] = (float)P * W;
}
size_t num_offsets = resolved_offsets.size();
int row_stride_words = (cols + 31) / 32;
size_t total_words = (size_t)num_layers * rows * row_stride_words;
size_t layer_pixels = (size_t)cols * rows;
size_t total_volume = (size_t)num_layers * layer_pixels;
size_t total_bit_words = num_offsets * total_words;
size_t total_bit_bytes = total_bit_words * sizeof(unsigned int);
cudaEvent_t ev_start, ev_h2d_done, ev_slice_done, ev_jfa_done, ev_thresh_done, ev_d2h_done;
CUDA_CHECK(cudaEventCreate(&ev_start));
CUDA_CHECK(cudaEventCreate(&ev_h2d_done));
CUDA_CHECK(cudaEventCreate(&ev_slice_done));
CUDA_CHECK(cudaEventCreate(&ev_jfa_done));
CUDA_CHECK(cudaEventCreate(&ev_thresh_done));
CUDA_CHECK(cudaEventCreate(&ev_d2h_done));
GpuTriangle3D* d_triangles;
unsigned int* d_parity;
uint8_t* d_masks;
int* d_seeds_a;
int* d_seeds_b;
unsigned int* d_out_bit_words;
float* d_thresholds;
size_t tri_bytes = num_triangles * sizeof(GpuTriangle3D);
CUDA_CHECK(cudaMalloc(&d_triangles, tri_bytes));
CUDA_CHECK(cudaMalloc(&d_parity, total_words * sizeof(unsigned int)));
CUDA_CHECK(cudaMalloc(&d_masks, total_volume * sizeof(uint8_t)));
CUDA_CHECK(cudaMalloc(&d_seeds_a, total_volume * sizeof(int)));
CUDA_CHECK(cudaMalloc(&d_seeds_b, total_volume * sizeof(int)));
CUDA_CHECK(cudaMalloc(&d_out_bit_words, total_bit_bytes));
CUDA_CHECK(cudaMalloc(&d_thresholds, num_offsets * sizeof(float)));
unsigned int* h_pinned_bit_words = nullptr;
CUDA_CHECK(cudaMallocHost(&h_pinned_bit_words, total_bit_bytes));
// 1. Upload Triangles & Thresholds (H2D)
CUDA_CHECK(cudaEventRecord(ev_start, g_cuda_stream));
CUDA_CHECK(cudaMemcpyAsync(d_triangles, triangles, tri_bytes, cudaMemcpyHostToDevice, g_cuda_stream));
CUDA_CHECK(cudaMemcpyAsync(d_thresholds, resolved_offsets.data(),
num_offsets * sizeof(float), cudaMemcpyHostToDevice, g_cuda_stream));
CUDA_CHECK(cudaEventRecord(ev_h2d_done, g_cuda_stream));
// 2. GPU Direct Mesh Slicing & Scanline Rasterization
CUDA_CHECK(cudaMemsetAsync(d_parity, 0, total_words * sizeof(unsigned int), g_cuda_stream));
int block_sz = 256;
int grid_sz = (num_triangles + block_sz - 1) / block_sz;
gpu_mesh_slice_raster_kernel<<<grid_sz, block_sz, 0, g_cuda_stream>>>(
d_triangles, num_triangles, min_z, h_first, layer_height, num_layers,
bounds_min_x, bounds_min_y, pitch, cols, rows, row_stride_words, d_parity
);
dim3 scan_grid(rows, num_layers);
gpu_parity_scan_to_mask_kernel<<<scan_grid, 1, 0, g_cuda_stream>>>(
d_parity, d_masks, cols, rows, num_layers, row_stride_words
);
CUDA_CHECK(cudaEventRecord(ev_slice_done, g_cuda_stream));
// 3. Batched 3D GPU JFA Distance Transform
dim3 jfa_block(16, 16, 1);
dim3 jfa_grid((cols + 15) / 16, (rows + 15) / 16, num_layers);
batched_jfa_init_kernel<<<jfa_grid, jfa_block, 0, g_cuda_stream>>>(
d_masks, d_seeds_a, cols, rows, num_layers
);
float max_offset_d = 0.0f;
for (size_t o = 0; o < num_offsets; ++o) {
if (resolved_offsets[o] > max_offset_d) {
max_offset_d = resolved_offsets[o];
}
}
int max_radius_px = (int)std::ceil(max_offset_d / pitch) + 2;
int step = 1;
while (step * 2 <= max_radius_px && step < 512) step *= 2;
if (step < 8) step = 8;
int* in_seeds = d_seeds_a;
int* out_seeds = d_seeds_b;
while (step >= 1) {
batched_jfa_step_kernel<<<jfa_grid, jfa_block, 0, g_cuda_stream>>>(
in_seeds, out_seeds, cols, rows, num_layers, step
);
std::swap(in_seeds, out_seeds);
step /= 2;
}
batched_jfa_step_kernel<<<jfa_grid, jfa_block, 0, g_cuda_stream>>>(
in_seeds, out_seeds, cols, rows, num_layers, 2
);
std::swap(in_seeds, out_seeds);
batched_jfa_step_kernel<<<jfa_grid, jfa_block, 0, g_cuda_stream>>>(
in_seeds, out_seeds, cols, rows, num_layers, 1
);
std::swap(in_seeds, out_seeds);
CUDA_CHECK(cudaEventRecord(ev_jfa_done, g_cuda_stream));
// 4. GPU Fused Multi-Perimeter Offset Thresholding & 32-bit Bit-Packing
dim3 thresh_block(32, 1, 1);
dim3 thresh_grid((row_stride_words + 31) / 32, rows, num_layers);
batched_jfa_threshold_bitpack_kernel<<<thresh_grid, thresh_block, 0, g_cuda_stream>>>(
in_seeds, d_masks, d_out_bit_words,
cols, rows, num_layers,
pitch, d_thresholds, (int)num_offsets, row_stride_words
);
CUDA_CHECK(cudaEventRecord(ev_thresh_done, g_cuda_stream));
// 5. Ultra-Fast D2H Transfer using Pinned Host Memory DMA
CUDA_CHECK(cudaMemcpyAsync(h_pinned_bit_words, d_out_bit_words, total_bit_bytes,
cudaMemcpyDeviceToHost, g_cuda_stream));
CUDA_CHECK(cudaEventRecord(ev_d2h_done, g_cuda_stream));
CUDA_CHECK(cudaStreamSynchronize(g_cuda_stream));
// 6. Multi-Threaded C++ Bit-Packed Vectorization
auto t_vec_0 = std::chrono::high_resolution_clock::now();
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 vec_worker = [&](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);
for (size_t l = start_layer; l < end_layer; ++l) {
res_array[l].z_height = z_first + l * layer_height;
res_array[l].num_perimeters = P;
res_array[l].perimeters = (GpuOffsetPerimeter*)malloc(P * sizeof(GpuOffsetPerimeter));
for (int o = 0; o < P; ++o) {
size_t offset_layer_word_idx = (o * num_layers + l) * ((size_t)rows * row_stride_words);
const unsigned int* bit_ptr = h_pinned_bit_words + offset_layer_word_idx;
std::vector<GpuContour> contours;
vectorize_bitpack_layer(bit_ptr, 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
size_t infill_offset_layer_word_idx = (P * num_layers + l) * ((size_t)rows * row_stride_words);
const unsigned int* infill_bit_ptr = h_pinned_bit_words + infill_offset_layer_word_idx;
std::vector<GpuContour> infill_contours;
vectorize_bitpack_layer(infill_bit_ptr, 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 = infill_bit_ptr + (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(vec_worker, s, e);
}
for (auto& w : workers) w.join();
auto t_vec_1 = std::chrono::high_resolution_clock::now();
double t_vec_ms = std::chrono::duration<double, std::milli>(t_vec_1 - t_vec_0).count();
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) {
float h2d_ms = 0, slice_ms = 0, jfa_ms = 0, thresh_ms = 0, d2h_ms = 0;
cudaEventElapsedTime(&h2d_ms, ev_start, ev_h2d_done);
cudaEventElapsedTime(&slice_ms, ev_h2d_done, ev_slice_done);
cudaEventElapsedTime(&jfa_ms, ev_slice_done, ev_jfa_done);
cudaEventElapsedTime(&thresh_ms, ev_jfa_done, ev_thresh_done);
cudaEventElapsedTime(&d2h_ms, ev_thresh_done, ev_d2h_done);
stats->t_h2d_ms = h2d_ms;
stats->t_mesh_slice_ms = slice_ms;
stats->t_raster_ms = slice_ms;
stats->t_gpu_jfa_ms = jfa_ms;
stats->t_gpu_thresh_ms = thresh_ms;
stats->t_d2h_ms = d2h_ms;
stats->t_vectorize_ms = t_vec_ms;
stats->t_total_pipeline_ms = t_total_ms;
}
// Free CUDA & Pinned resources
cudaFree(d_triangles);
cudaFree(d_parity);
cudaFree(d_masks);
cudaFree(d_seeds_a);
cudaFree(d_seeds_b);
cudaFree(d_out_bit_words);
cudaFree(d_thresholds);
cudaFreeHost(h_pinned_bit_words);
cudaEventDestroy(ev_start);
cudaEventDestroy(ev_h2d_done);
cudaEventDestroy(ev_slice_done);
cudaEventDestroy(ev_jfa_done);
cudaEventDestroy(ev_thresh_done);
cudaEventDestroy(ev_d2h_done);
*out_results = res_array;
return GPU_OFFSET_SUCCESS;
} else { } else {
return cpu_offset_slice_mesh_and_offset(triangles, num_triangles, min_x, min_y, min_z, max_x, max_y, max_z, return cpu_offset_slice_mesh_and_offset(triangles, num_triangles, min_x, min_y, min_z, max_x, max_y, max_z,
layer_height, first_layer_height, config, out_results, out_num_layers, stats); layer_height, first_layer_height, config, out_results, out_num_layers, stats);

View file

@ -345,61 +345,7 @@ __global__ void gpu_sdf_shell_kernel(
out_sdf[idx] = half_t - fabsf(d - half_t); out_sdf[idx] = half_t - fabsf(d - half_t);
} }
__global__ void gpu_sdf_infill_kernel(
const float* __restrict__ in_sdf,
float* __restrict__ out_sdf,
int width, int height, int num_layers,
int32_t min_x_um, int32_t min_y_um, int32_t pitch_um,
const int32_t* __restrict__ z_heights_um,
int pattern,
float cell_size_mm,
float density_ratio,
float wall_offset_mm
) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int l = blockIdx.z;
if (x >= width || y >= height || l >= num_layers) return;
size_t idx = ((size_t)l * height + y) * width + x;
float dist = in_sdf[idx];
// Outside boundary or within shell wall offset -> retain wall distance
if (dist <= wall_offset_mm) {
out_sdf[idx] = dist;
return;
}
float world_x = (float)(min_x_um + x * pitch_um) * 0.001f;
float world_y = (float)(min_y_um + y * pitch_um) * 0.001f;
float world_z = (float)(z_heights_um[l]) * 0.001f;
float cell_sz = (cell_size_mm > 0.1f) ? cell_size_mm : 5.0f;
float freq = 6.28318530718f / cell_sz;
float px = world_x * freq;
float py = world_y * freq;
float pz = world_z * freq;
float field = 0.0f;
if (pattern == GPU_SDF_INFILL_GYROID) {
field = sinf(px) * cosf(py) + sinf(py) * cosf(pz) + sinf(pz) * cosf(px);
} else if (pattern == GPU_SDF_INFILL_SCHWARZ_P) {
field = cosf(px) + cosf(py) + cosf(pz);
} else if (pattern == GPU_SDF_INFILL_SCHWARZ_D) {
field = cosf(px)*cosf(py)*cosf(pz) - sinf(px)*sinf(py)*sinf(pz);
} else if (pattern == GPU_SDF_INFILL_GRID) {
float fx = fabsf(fmodf(world_x, cell_sz) - cell_sz * 0.5f);
float fy = fabsf(fmodf(world_y, cell_sz) - cell_sz * 0.5f);
field = fminf(fx, fy) / (cell_sz * 0.5f) - 0.5f;
}
float threshold = (density_ratio - 0.5f) * 1.5f;
// Combine lattice with the infill bounding region
float lattice_sdf = (field >= threshold) ? 1.0f : -1.0f;
out_sdf[idx] = fminf(dist - wall_offset_mm, lattice_sdf);
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Direct Bitpack Vectorizer Kernel from SDF // Direct Bitpack Vectorizer Kernel from SDF
@ -597,16 +543,210 @@ GPU_SDF_API GpuSdfStackHandle gpu_sdf_from_masks(
return stack; return stack;
} }
// Forward declare GPU mesh rasterizer helper from gpu_offset.cu struct GpuSegment2D {
extern int gpu_slice_mesh_to_device_mask( float x0, y0;
const GpuTriangle3D* triangles, float x1, y1;
};
__global__ void gpu_mesh_slice_raster_and_segments_kernel(
const GpuTriangle3D* __restrict__ triangles,
size_t num_triangles, size_t num_triangles,
float min_z, float h_first, float layer_height, int num_layers, float z_min_base,
float bounds_min_x, float bounds_min_y, float pitch, float first_layer_height,
int cols, int rows, float layer_height,
uint8_t* d_masks, int num_layers,
cudaStream_t stream float bounds_min_x,
); float bounds_min_y,
float pitch_mm,
int cols,
int rows,
int row_stride_words,
unsigned int* __restrict__ parity_words,
GpuSegment2D* __restrict__ layer_segments,
int* __restrict__ layer_seg_counts,
int max_segs_per_layer
) {
size_t i = (size_t)blockIdx.x * blockDim.x + threadIdx.x;
if (i >= num_triangles) return;
GpuTriangle3D tri = triangles[i];
float tz_min = fminf(tri.v0[2], fminf(tri.v1[2], tri.v2[2]));
float tz_max = fmaxf(tri.v0[2], fmaxf(tri.v1[2], tri.v2[2]));
float z_first = z_min_base + first_layer_height;
int l_start = (tz_min <= z_first) ? 0 : (int)ceilf((tz_min - z_first) / layer_height);
int l_end = (int)floorf((tz_max - z_first) / layer_height);
if (l_start < 0) l_start = 0;
if (l_end >= num_layers) l_end = num_layers - 1;
if (l_start > l_end) return;
const float* v[3] = { tri.v0, tri.v1, tri.v2 };
for (int l = l_start; l <= l_end; ++l) {
float z0 = z_first + l * layer_height;
if (z0 < tz_min || z0 > tz_max) continue;
float pts_x[3];
float pts_y[3];
int count = 0;
#pragma unroll
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 (fabsf(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;
if (layer_segments && layer_seg_counts) {
int seg_idx = atomicAdd(&layer_seg_counts[l], 1);
if (seg_idx < max_segs_per_layer) {
GpuSegment2D seg;
seg.x0 = pts_x[0];
seg.y0 = pts_y[0];
seg.x1 = pts_x[1];
seg.y1 = pts_y[1];
layer_segments[(size_t)l * max_segs_per_layer + seg_idx] = seg;
}
}
float gx0 = (pts_x[0] - bounds_min_x) / pitch_mm;
float gy0 = (pts_y[0] - bounds_min_y) / pitch_mm;
float gx1 = (pts_x[1] - bounds_min_x) / pitch_mm;
float gy1 = (pts_y[1] - bounds_min_y) / pitch_mm;
if (gy0 == gy1) continue;
if (gy0 > gy1) {
float tx = gx0; gx0 = gx1; gx1 = tx;
float ty = gy0; gy0 = gy1; gy1 = ty;
}
int y_start = (int)ceilf(gy0);
int y_end = (int)ceilf(gy1) - 1;
if (y_start < 0) y_start = 0;
if (y_end >= rows) y_end = rows - 1;
float dy = gy1 - gy0;
if (dy < 1e-7f) continue;
size_t layer_word_offset = (size_t)l * rows * row_stride_words;
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)ceilf(gx);
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;
size_t w_idx = layer_word_offset + (size_t)y * row_stride_words + w;
atomicXor(&parity_words[w_idx], (1u << b));
}
}
}
__global__ void gpu_parity_scan_to_mask_sdf_kernel(
const unsigned int* __restrict__ parity_words,
uint8_t* __restrict__ masks,
int cols,
int rows,
int num_layers,
int row_stride_words
) {
int y = blockIdx.x;
int l = blockIdx.y;
if (y >= rows || l >= num_layers) return;
size_t layer_word_offset = (size_t)l * rows * row_stride_words;
size_t layer_pixel_offset = (size_t)l * rows * cols;
size_t row_pixel_offset = layer_pixel_offset + (size_t)y * cols;
uint8_t inside = 0;
int x_out = 0;
for (int w = 0; w < row_stride_words; ++w) {
unsigned int word = parity_words[layer_word_offset + (size_t)y * row_stride_words + w];
for (int b = 0; b < 32 && x_out < cols; ++b, ++x_out) {
inside ^= (uint8_t)((word >> b) & 1u);
masks[row_pixel_offset + x_out] = inside;
}
}
}
__global__ void gpu_compute_exact_mesh_sdf_kernel(
const uint8_t* __restrict__ masks,
const GpuSegment2D* __restrict__ layer_segments,
const int* __restrict__ layer_seg_counts,
int max_segs_per_layer,
float* __restrict__ out_sdf,
int cols,
int rows,
int num_layers,
float bounds_min_x,
float bounds_min_y,
float pitch_mm
) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int l = blockIdx.z;
if (x >= cols || y >= rows || l >= num_layers) return;
size_t layer_pixel_offset = (size_t)l * rows * cols;
size_t idx = layer_pixel_offset + (size_t)y * cols + x;
int seg_count = layer_seg_counts[l];
if (seg_count > max_segs_per_layer) seg_count = max_segs_per_layer;
if (seg_count == 0) {
out_sdf[idx] = -1e5f;
return;
}
float px = bounds_min_x + (float)x * pitch_mm;
float py = bounds_min_y + (float)y * pitch_mm;
float min_d2 = 1e10f;
const GpuSegment2D* segs = layer_segments + (size_t)l * max_segs_per_layer;
for (int s = 0; s < seg_count; ++s) {
GpuSegment2D seg = segs[s];
float vx = seg.x1 - seg.x0;
float vy = seg.y1 - seg.y0;
float wx = px - seg.x0;
float wy = py - seg.y0;
float len_sq = vx * vx + vy * vy;
float t = (len_sq > 1e-10f) ? (wx * vx + wy * vy) / len_sq : 0.0f;
if (t < 0.0f) t = 0.0f;
else if (t > 1.0f) t = 1.0f;
float qx = seg.x0 + t * vx;
float qy = seg.y0 + t * vy;
float dx = px - qx;
float dy = py - qy;
float d2 = dx * dx + dy * dy;
if (d2 < min_d2) min_d2 = d2;
}
float d = sqrtf(min_d2);
uint8_t m = masks[idx];
out_sdf[idx] = (m > 0) ? d : -d;
}
GPU_SDF_API GpuSdfStackHandle gpu_sdf_from_mesh( GPU_SDF_API GpuSdfStackHandle gpu_sdf_from_mesh(
const GpuTriangle3D* triangles, const GpuTriangle3D* triangles,
@ -656,107 +796,67 @@ GPU_SDF_API GpuSdfStackHandle gpu_sdf_from_mesh(
GpuSdfStackHandle stack = gpu_sdf_create(&meta, z_heights_um.data()); GpuSdfStackHandle stack = gpu_sdf_create(&meta, z_heights_um.data());
if (!stack) return nullptr; if (!stack) return nullptr;
int row_stride_words = (cols + 31) / 32;
size_t total_words = (size_t)num_layers * rows * row_stride_words;
size_t total_volume = (size_t)num_layers * rows * cols; size_t total_volume = (size_t)num_layers * rows * cols;
size_t tri_bytes = num_triangles * sizeof(GpuTriangle3D); size_t tri_bytes = num_triangles * sizeof(GpuTriangle3D);
size_t seed_bytes = total_volume * sizeof(int);
int max_segs_per_layer = std::min(8192, (int)num_triangles);
if (max_segs_per_layer < 256) max_segs_per_layer = 256;
size_t total_segs = (size_t)num_layers * max_segs_per_layer;
GpuTriangle3D* d_triangles = nullptr; GpuTriangle3D* d_triangles = nullptr;
unsigned int* d_parity = nullptr;
uint8_t* d_masks = nullptr; uint8_t* d_masks = nullptr;
int* d_seeds_ext_a = nullptr; GpuSegment2D* d_layer_segments = nullptr;
int* d_seeds_ext_b = nullptr; int* d_layer_seg_counts = nullptr;
int* d_seeds_int_a = nullptr;
int* d_seeds_int_b = nullptr;
if (cudaMalloc(&d_triangles, tri_bytes) != cudaSuccess || if (cudaMalloc(&d_triangles, tri_bytes) != cudaSuccess ||
cudaMalloc(&d_parity, total_words * sizeof(unsigned int)) != cudaSuccess ||
cudaMalloc(&d_masks, total_volume * sizeof(uint8_t)) != cudaSuccess || cudaMalloc(&d_masks, total_volume * sizeof(uint8_t)) != cudaSuccess ||
cudaMalloc(&d_seeds_ext_a, seed_bytes) != cudaSuccess || cudaMalloc(&d_layer_segments, total_segs * sizeof(GpuSegment2D)) != cudaSuccess ||
cudaMalloc(&d_seeds_ext_b, seed_bytes) != cudaSuccess || cudaMalloc(&d_layer_seg_counts, num_layers * sizeof(int)) != cudaSuccess) {
cudaMalloc(&d_seeds_int_a, seed_bytes) != cudaSuccess ||
cudaMalloc(&d_seeds_int_b, seed_bytes) != cudaSuccess) {
if (d_triangles) cudaFree(d_triangles); if (d_triangles) cudaFree(d_triangles);
if (d_parity) cudaFree(d_parity);
if (d_masks) cudaFree(d_masks); if (d_masks) cudaFree(d_masks);
if (d_seeds_ext_a) cudaFree(d_seeds_ext_a); if (d_layer_segments) cudaFree(d_layer_segments);
if (d_seeds_ext_b) cudaFree(d_seeds_ext_b); if (d_layer_seg_counts) cudaFree(d_layer_seg_counts);
if (d_seeds_int_a) cudaFree(d_seeds_int_a);
if (d_seeds_int_b) cudaFree(d_seeds_int_b);
gpu_sdf_destroy(stack); gpu_sdf_destroy(stack);
return nullptr; return nullptr;
} }
cudaMemsetAsync(d_parity, 0, total_words * sizeof(unsigned int), stack->stream);
cudaMemsetAsync(d_layer_seg_counts, 0, num_layers * sizeof(int), stack->stream);
cudaMemcpyAsync(d_triangles, triangles, tri_bytes, cudaMemcpyHostToDevice, stack->stream); cudaMemcpyAsync(d_triangles, triangles, tri_bytes, cudaMemcpyHostToDevice, stack->stream);
int slice_ret = gpu_slice_mesh_to_device_mask( int block_sz = 256;
int grid_sz = (num_triangles + block_sz - 1) / block_sz;
gpu_mesh_slice_raster_and_segments_kernel<<<grid_sz, block_sz, 0, stack->stream>>>(
d_triangles, num_triangles, min_z, h_first, layer_height_mm, num_layers, d_triangles, num_triangles, min_z, h_first, layer_height_mm, num_layers,
bounds_min_x, bounds_min_y, pitch, cols, rows, d_masks, stack->stream bounds_min_x, bounds_min_y, pitch, cols, rows, row_stride_words,
); d_parity, d_layer_segments, d_layer_seg_counts, max_segs_per_layer
if (slice_ret != GPU_OFFSET_SUCCESS) {
cudaFree(d_triangles);
cudaFree(d_masks);
cudaFree(d_seeds_ext_a);
cudaFree(d_seeds_ext_b);
cudaFree(d_seeds_int_a);
cudaFree(d_seeds_int_b);
gpu_sdf_destroy(stack);
return nullptr;
}
// Run Batched 3D JFA Dual Direction Signed Distance
dim3 block(16, 16, 1);
dim3 grid((cols + 15) / 16, (rows + 15) / 16, num_layers);
jfa_init_interior_exterior_kernel<<<grid, block, 0, stack->stream>>>(
d_masks, d_seeds_ext_a, d_seeds_int_a, cols, rows, num_layers
); );
int max_dim = std::max(cols, rows); dim3 scan_grid(rows, num_layers);
int step = 1; gpu_parity_scan_to_mask_sdf_kernel<<<scan_grid, 1, 0, stack->stream>>>(
while (step * 2 < max_dim) step *= 2; d_parity, d_masks, cols, rows, num_layers, row_stride_words
int* in_seeds_ext = d_seeds_ext_a;
int* out_seeds_ext = d_seeds_ext_b;
int* in_seeds_int = d_seeds_int_a;
int* out_seeds_int = d_seeds_int_b;
while (step >= 1) {
jfa_step_dual_kernel<<<grid, block, 0, stack->stream>>>(
in_seeds_ext, out_seeds_ext,
in_seeds_int, out_seeds_int,
cols, rows, num_layers, step
);
std::swap(in_seeds_ext, out_seeds_ext);
std::swap(in_seeds_int, out_seeds_int);
step /= 2;
}
jfa_step_dual_kernel<<<grid, block, 0, stack->stream>>>(
in_seeds_ext, out_seeds_ext,
in_seeds_int, out_seeds_int,
cols, rows, num_layers, 2
); );
std::swap(in_seeds_ext, out_seeds_ext);
std::swap(in_seeds_int, out_seeds_int);
jfa_step_dual_kernel<<<grid, block, 0, stack->stream>>>( dim3 exact_block(16, 16, 1);
in_seeds_ext, out_seeds_ext, dim3 exact_grid((cols + 15) / 16, (rows + 15) / 16, num_layers);
in_seeds_int, out_seeds_int, gpu_compute_exact_mesh_sdf_kernel<<<exact_grid, exact_block, 0, stack->stream>>>(
cols, rows, num_layers, 1 d_masks, d_layer_segments, d_layer_seg_counts, max_segs_per_layer,
); stack->d_sdf, cols, rows, num_layers,
std::swap(in_seeds_ext, out_seeds_ext); bounds_min_x, bounds_min_y, pitch
std::swap(in_seeds_int, out_seeds_int);
fuse_signed_sdf_kernel<<<grid, block, 0, stack->stream>>>(
d_masks, in_seeds_ext, in_seeds_int, stack->d_sdf,
cols, rows, num_layers, pitch
); );
cudaStreamSynchronize(stack->stream); cudaStreamSynchronize(stack->stream);
cudaFree(d_triangles); cudaFree(d_triangles);
cudaFree(d_parity);
cudaFree(d_masks); cudaFree(d_masks);
cudaFree(d_seeds_ext_a); cudaFree(d_layer_segments);
cudaFree(d_seeds_ext_b); cudaFree(d_layer_seg_counts);
cudaFree(d_seeds_int_a);
cudaFree(d_seeds_int_b);
return stack; return stack;
} }
@ -1002,35 +1102,6 @@ GPU_SDF_API GpuSdfStackHandle gpu_sdf_shell(
return out_stack; return out_stack;
} }
GPU_SDF_API GpuSdfStackHandle gpu_sdf_generate_infill(
GpuSdfStackHandle stack,
GpuSdfInfillPattern pattern,
float cell_size_mm,
float density_ratio,
float wall_offset_mm
) {
if (!stack) return nullptr;
std::vector<int32_t> z_um(stack->meta.num_layers);
gpu_sdf_get_layer_z_um(stack, z_um.data());
GpuSdfStackHandle out_stack = gpu_sdf_create(&stack->meta, z_um.data());
if (!out_stack) return nullptr;
dim3 block(16, 16, 1);
dim3 grid((stack->meta.width + 15) / 16, (stack->meta.height + 15) / 16, (unsigned int)stack->meta.num_layers);
gpu_sdf_infill_kernel<<<grid, block, 0, out_stack->stream>>>(
stack->d_sdf, out_stack->d_sdf,
stack->meta.width, stack->meta.height, (int)stack->meta.num_layers,
stack->meta.min_x_um, stack->meta.min_y_um, stack->meta.pitch_um,
stack->d_z_um, (int)pattern, cell_size_mm, density_ratio, wall_offset_mm
);
cudaStreamSynchronize(out_stack->stream);
return out_stack;
}
GPU_SDF_API int gpu_sdf_vectorize( GPU_SDF_API int gpu_sdf_vectorize(
GpuSdfStackHandle stack, GpuSdfStackHandle stack,
const GpuOffsetConfig* config, const GpuOffsetConfig* config,
@ -1068,9 +1139,7 @@ GPU_SDF_API int gpu_sdf_vectorize(
size_t num_offsets = resolved_offsets.size(); size_t num_offsets = resolved_offsets.size();
float simplify_tol = config->simplify_tolerance_mm; float simplify_tol = config->simplify_tolerance_mm;
if (simplify_tol <= 0.0f && config->subpixel_lerp != 0) { int subpixel_lerp = config->subpixel_lerp;
simplify_tol = 0.02f; // default 0.02 mm (20 µm) tolerance
}
size_t total_voxels = (size_t)num_layers * rows * cols; size_t total_voxels = (size_t)num_layers * rows * cols;
size_t sdf_bytes = total_voxels * sizeof(float); size_t sdf_bytes = total_voxels * sizeof(float);
@ -1113,7 +1182,7 @@ GPU_SDF_API int gpu_sdf_vectorize(
std::vector<GpuContour> contours; std::vector<GpuContour> contours;
vectorize_sdf_layer(layer_sdf_ptr, cols, rows, bounds_min_x, bounds_min_y, pitch_mm, vectorize_sdf_layer(layer_sdf_ptr, cols, rows, bounds_min_x, bounds_min_y, pitch_mm,
iso_thresh, simplify_tol, iso_thresh, simplify_tol, subpixel_lerp,
next_edge_buf, active_edges_buf, visited_buf, contours); next_edge_buf, active_edges_buf, visited_buf, contours);
res_array[l].perimeters[o].offset_distance_mm = iso_thresh; res_array[l].perimeters[o].offset_distance_mm = iso_thresh;
@ -1126,11 +1195,11 @@ GPU_SDF_API int gpu_sdf_vectorize(
} }
} }
// Infill // Infill Contours (Innermost offset boundary)
float infill_iso_thresh = resolved_offsets[P]; float infill_iso_thresh = resolved_offsets[P];
std::vector<GpuContour> infill_contours; std::vector<GpuContour> infill_contours;
vectorize_sdf_layer(layer_sdf_ptr, cols, rows, bounds_min_x, bounds_min_y, pitch_mm, vectorize_sdf_layer(layer_sdf_ptr, cols, rows, bounds_min_x, bounds_min_y, pitch_mm,
infill_iso_thresh, simplify_tol, infill_iso_thresh, simplify_tol, subpixel_lerp,
next_edge_buf, active_edges_buf, visited_buf, infill_contours); next_edge_buf, active_edges_buf, visited_buf, infill_contours);
res_array[l].infill.offset_distance_mm = infill_iso_thresh; res_array[l].infill.offset_distance_mm = infill_iso_thresh;

View file

@ -143,12 +143,16 @@ static inline std::vector<GpuPoint2D> simplify_contour_rdp(
} }
} }
std::vector<bool> keep(n, false); std::vector<GpuPoint2D> closed_pts = loop_pts;
closed_pts.push_back(loop_pts[0]);
std::vector<bool> keep(n + 1, false);
keep[0] = true; keep[0] = true;
keep[split_idx] = true; keep[split_idx] = true;
keep[n] = true;
rdp_simplify_recursive(loop_pts, 0, split_idx, eps_sq, keep); rdp_simplify_recursive(closed_pts, 0, split_idx, eps_sq, keep);
rdp_simplify_recursive(loop_pts, split_idx, n - 1, eps_sq, keep); rdp_simplify_recursive(closed_pts, split_idx, n, eps_sq, keep);
std::vector<GpuPoint2D> simplified; std::vector<GpuPoint2D> simplified;
simplified.reserve(n); simplified.reserve(n);
@ -164,6 +168,7 @@ static inline void vectorize_sdf_layer(
float min_x, float min_y, float pitch_mm, float min_x, float min_y, float pitch_mm,
float iso_val, float iso_val,
float simplify_tolerance_mm, float simplify_tolerance_mm,
int subpixel_lerp,
std::vector<int>& next_edge_buf, std::vector<int>& next_edge_buf,
std::vector<int>& active_edges_buf, std::vector<int>& active_edges_buf,
std::vector<uint8_t>& visited_buf, std::vector<uint8_t>& visited_buf,
@ -253,7 +258,12 @@ static inline void vectorize_sdf_layer(
while (cur != -1 && !visited_buf[cur]) { while (cur != -1 && !visited_buf[cur]) {
visited_buf[cur] = 1; visited_buf[cur] = 1;
GpuPoint2D pt = get_sdf_edge_point(cur, sdf, width, height, min_x, min_y, pitch_mm, iso_val); GpuPoint2D pt;
if (subpixel_lerp) {
pt = get_sdf_edge_point(cur, sdf, width, height, min_x, min_y, pitch_mm, iso_val);
} else {
pt = get_edge_point(cur, stride_w, min_x, min_y, pitch_mm);
}
size_t n = loop_pts.size(); size_t n = loop_pts.size();
if (n >= 2) { if (n >= 2) {
@ -274,11 +284,13 @@ static inline void vectorize_sdf_layer(
cur = next_edge_buf[cur]; cur = next_edge_buf[cur];
if (cur == start_e) break; if (cur == start_e) break;
if (loop_pts.size() > 50000) break; if (loop_pts.size() > 100000) break;
} }
if (loop_pts.size() >= 3) { if (loop_pts.size() >= 3) {
std::vector<GpuPoint2D> simplified = simplify_contour_rdp(loop_pts, simplify_tolerance_mm); std::vector<GpuPoint2D> simplified = (simplify_tolerance_mm > 1e-6f)
? simplify_contour_rdp(loop_pts, simplify_tolerance_mm)
: loop_pts;
if (simplified.size() >= 3) { if (simplified.size() >= 3) {
GpuContour c; GpuContour c;
c.num_points = simplified.size(); c.num_points = simplified.size();