cudaslicer

This commit is contained in:
Nicolas Predella 2026-09-26 14:20:07 +03:00
parent 1019a91e34
commit 9cc37e1128
9 changed files with 2431 additions and 1 deletions

View file

@ -31,6 +31,7 @@ include_directories(
# -----------------------------------------------------------------------------
add_library(cudaslicer_offset SHARED
src/gpu_offset.cu
src/gpu_sdf.cu
src/cpu_offset.cpp
)
set_target_properties(cudaslicer_offset PROPERTIES
@ -45,6 +46,7 @@ target_link_libraries(cudaslicer_offset PUBLIC cudart)
# -----------------------------------------------------------------------------
add_library(cudaslicer_offset_static STATIC
src/gpu_offset.cu
src/gpu_sdf.cu
src/cpu_offset.cpp
)
set_target_properties(cudaslicer_offset_static PROPERTIES

View file

@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
GPU-Resident Signed Distance Field (SDF) CSG & Infill Example
=============================================================
Demonstrates chaining 3D mesh slicing, Boolean CSG operations (Union, Intersection,
Difference, Smooth Blend), constant-distance offsetting, volume shelling, procedural
3D gyroid infill generation, and contour vectorization entirely in GPU VRAM without
host-to-device memory roundtrips.
"""
import os
import sys
import time
import numpy as np
# Ensure local build is in path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "python"))
import cudaslicer
import trimesh
def main():
print("=" * 70)
print(" CUDASlicer: GPU Signed Distance Field (SDF) & CSG Engine")
print("=" * 70)
if not cudaslicer.has_cuda():
print("[Error] CUDA-capable GPU is required for this demo.")
return
# 1. Load or generate 3D models
stl_path = os.path.join(os.path.dirname(__file__), "..", "benchy.stl")
if os.path.exists(stl_path):
print(f"\n[1] Loading main model: {stl_path}")
mesh_a = trimesh.load(stl_path)
else:
print("\n[1] Generating synthetic main cylinder...")
mesh_a = trimesh.creation.cylinder(radius=15.0, height=30.0)
# Generate a modifier box volume to subtract from the model
print("[1] Generating modifier cutting box...")
mesh_b = trimesh.creation.box(extents=(12.0, 60.0, 25.0))
mesh_b.apply_translation((0.0, 0.0, 15.0))
# 2. Direct GPU Slicing to SDF Stacks
layer_height_mm = 0.20
pitch_mm = 0.10
t0 = time.perf_counter()
sdf_a = cudaslicer.slice_mesh_to_sdf(mesh_a, layer_height=layer_height_mm, pitch_mm=pitch_mm)
sdf_b = cudaslicer.slice_mesh_to_sdf(mesh_b, layer_height=layer_height_mm, pitch_mm=pitch_mm)
t_slice = (time.perf_counter() - t0) * 1000.0
print(f"\n[2] Direct GPU Slicing & Signed Distance Generation ({t_slice:.2f} ms):")
print(f" - Main Model Stack: {sdf_a}")
print(f" Layer Z range (µm): {sdf_a.layer_z_um[0]} µm to {sdf_a.layer_z_um[-1]} µm ({len(sdf_a.layer_z_um)} layers)")
print(f" - Modifier Box Stack: {sdf_b}")
print(f" Layer Z range (µm): {sdf_b.layer_z_um[0]} µm to {sdf_b.layer_z_um[-1]} µm ({len(sdf_b.layer_z_um)} layers)")
# 3. GPU-Resident Boolean Operations (Zero CPU transfers)
t0 = time.perf_counter()
# Chained expression: (Model A \ (Modifier B + 0.5mm offset))
modifier_expanded = sdf_b.offset(delta_mm=0.5)
sdf_carved = sdf_a.difference(modifier_expanded)
t_csg = (time.perf_counter() - t0) * 1000.0
print(f"\n[3] Chained GPU CSG (Carve / Subtraction) ({t_csg:.2f} ms):")
print(f" - Result Stack: {sdf_carved}")
print(f" - Automatic spatial bounding grid expansion: {sdf_carved.shape}")
# 4. Procedural 3D Gyroid Infill Generation on GPU
t0 = time.perf_counter()
sdf_gyroid = sdf_carved.generate_infill(
pattern="gyroid",
cell_size_mm=4.0,
density=0.25,
wall_offset_mm=1.20
)
t_infill = (time.perf_counter() - t0) * 1000.0
print(f"\n[4] GPU Procedural Gyroid Infill Generation ({t_infill:.2f} ms):")
print(f" - Infill Stack: {sdf_gyroid}")
# 5. Vectorize directly to polygon perimeters and infill contours
t0 = time.perf_counter()
layer_results = sdf_gyroid.vectorize(
perimeters_count=2,
layer_width=0.45,
export_infill_mask=True
)
t_vec = (time.perf_counter() - t0) * 1000.0
total_contours = sum(
sum(len(p["contours"]) for p in l.perimeters) + len(l.infill_contours)
for l in layer_results
)
print(f"\n[5] Vectorization to Slicer Contours ({t_vec:.2f} ms):")
print(f" - Total Layers Output: {len(layer_results)}")
print(f" - Total Vector Contours Extracted: {total_contours}")
# Sample mid layer
mid_idx = len(layer_results) // 2
sample_layer = layer_results[mid_idx]
print(f"\n[6] Sample Mid Layer (Layer {mid_idx}):")
print(f" - Z Height: {sample_layer.z:.3f} mm ({sample_layer.z_um} µm)")
print(f" - Perimeters: {len(sample_layer.perimeters)}")
for i, p in enumerate(sample_layer.perimeters):
print(f" * Perimeter {i+1} (offset {p['offset_mm']:.2f} mm): {len(p['contours'])} contour loop(s)")
print(f" - Infill Contours: {len(sample_layer.infill_contours)} contour loop(s)")
print("\n" + "=" * 70)
print(" Pipeline completed successfully with 100% GPU-resident computation!")
print("=" * 70)
if __name__ == "__main__":
main()

View file

@ -105,6 +105,8 @@ typedef struct {
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)
float simplify_tolerance_mm; // RDP simplification tolerance in mm (default: 0.02 mm, 0 to disable)
int subpixel_lerp; // 1 = Continuous SDF sub-pixel LERP vectorization (default: 1), 0 = discrete bitpack
int backend; // GpuOffsetBackend: 0 = AUTO, 1 = CUDA, 2 = CPU (default: 0)
} GpuOffsetConfig;

209
include/gpu_sdf.h Normal file
View file

@ -0,0 +1,209 @@
#ifndef GPU_SDF_H
#define GPU_SDF_H
#ifdef _WIN32
#ifdef GPU_OFFSET_EXPORTS
#define GPU_SDF_API __declspec(dllexport)
#else
#define GPU_SDF_API __declspec(dllimport)
#endif
#else
#define GPU_SDF_API __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
#include <stdint.h>
#include "gpu_offset.h"
/**
* @brief CSG Operation Types for GPU SDF combinations
*/
typedef enum {
GPU_SDF_OP_UNION = 0, // A U B: max(d_A, d_B)
GPU_SDF_OP_INTERSECTION = 1, // A ^ B: min(d_A, d_B)
GPU_SDF_OP_DIFFERENCE = 2, // A \ B: min(d_A, -d_B)
GPU_SDF_OP_SMOOTH_UNION = 3, // Smooth union with blend radius k
GPU_SDF_OP_SMOOTH_INTERSECT= 4, // Smooth intersection with blend radius k
GPU_SDF_OP_SMOOTH_DIFF = 5 // Smooth difference with blend radius k
} GpuSdfOp;
/**
* @brief Layer matching policy when operating on two stacks
*/
typedef enum {
GPU_SDF_LAYER_MATCH_A_DOMINANT = 0, // Keep layers matching stack A (resample B at A's Z)
GPU_SDF_LAYER_MATCH_UNION = 1, // Output union of all unique layer Z heights
GPU_SDF_LAYER_MATCH_INTERSECTION = 2 // Output only exact matching layer Z heights
} 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
* All spatial coordinates and pitches are stored in integer micrometers (µm)
*/
typedef struct {
int32_t min_x_um; // Minimum X bound in micrometers (1 µm = 0.001 mm)
int32_t min_y_um; // Minimum Y bound in micrometers
int32_t pitch_um; // Pixel raster pitch in micrometers (e.g. 100 µm = 0.1 mm)
int width; // Grid columns (X)
int height; // Grid rows (Y)
size_t num_layers; // Total number of layers (Z)
} GpuSdfMetadata;
/**
* @brief Opaque handle representing a GPU-resident Signed Distance Field stack.
* Distance values are in millimeters (float):
* d > 0 : Inside the object (positive Euclidean distance to boundary)
* d < 0 : Outside the object (negative Euclidean distance to boundary)
* d = 0 : Exact object boundary
*/
typedef struct GpuSdfStack_T* GpuSdfStackHandle;
/**
* @brief Create an empty GPU SDF stack with given metadata and layer Z heights (in µm).
*/
GPU_SDF_API GpuSdfStackHandle gpu_sdf_create(
const GpuSdfMetadata* meta,
const int32_t* layer_z_heights_um
);
/**
* @brief Create a GPU SDF stack from 3D mesh triangles directly on GPU.
*/
GPU_SDF_API GpuSdfStackHandle gpu_sdf_from_mesh(
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_mm,
float first_layer_height_mm,
float pitch_mm,
float pad_mm
);
/**
* @brief Create a GPU SDF stack from raw host binary occupancy masks (0 = background, >0 = object).
*/
GPU_SDF_API GpuSdfStackHandle gpu_sdf_from_masks(
const uint8_t* h_masks,
int width,
int height,
size_t num_layers,
const int32_t* layer_z_heights_um,
int32_t min_x_um,
int32_t min_y_um,
int32_t pitch_um
);
/**
* @brief Clone an existing GPU SDF stack (allocates new GPU memory).
*/
GPU_SDF_API GpuSdfStackHandle gpu_sdf_clone(GpuSdfStackHandle stack);
/**
* @brief Free GPU memory allocated for a GPU SDF stack.
*/
GPU_SDF_API void gpu_sdf_destroy(GpuSdfStackHandle stack);
/**
* @brief Query metadata of a GPU SDF stack.
*/
GPU_SDF_API int gpu_sdf_get_metadata(GpuSdfStackHandle stack, GpuSdfMetadata* out_meta);
/**
* @brief Retrieve layer Z heights in micrometers (copies to host buffer of size num_layers).
*/
GPU_SDF_API int gpu_sdf_get_layer_z_um(GpuSdfStackHandle stack, int32_t* out_z_um);
/**
* @brief Retrieve raw SDF distance data (float mm) copied from GPU to Host.
* Buffer size must be: width * height * num_layers * sizeof(float).
*/
GPU_SDF_API int gpu_sdf_to_host(GpuSdfStackHandle stack, float* h_out_data);
/**
* @brief Get raw GPU device pointer for zero-copy interop (CuPy / PyTorch).
*/
GPU_SDF_API float* gpu_sdf_get_device_ptr(GpuSdfStackHandle stack);
/**
* @brief Get raw GPU device pointer for layer Z heights (int32_t* on device).
*/
GPU_SDF_API int32_t* gpu_sdf_get_device_z_ptr(GpuSdfStackHandle stack);
/**
* @brief Execute a CSG operation between two GPU SDF stacks without host roundtrips.
* Handles spatial bounding box differences and layer Z matching automatically.
*
* @param a First operand stack (GPU)
* @param b Second operand stack (GPU)
* @param op CSG operation (UNION, INTERSECTION, DIFFERENCE, SMOOTH_...)
* @param match_mode Layer Z matching mode
* @param blend_radius_mm Blend radius parameter for smooth operations
* @return GpuSdfStackHandle Resulting stack on GPU (caller must free with gpu_sdf_destroy)
*/
GPU_SDF_API GpuSdfStackHandle gpu_sdf_csg_op(
GpuSdfStackHandle a,
GpuSdfStackHandle b,
GpuSdfOp op,
GpuSdfLayerMatchMode match_mode,
float blend_radius_mm
);
/**
* @brief Apply a constant distance offset (dilation/expansion if delta > 0, inset/erosion if delta < 0).
*/
GPU_SDF_API GpuSdfStackHandle gpu_sdf_offset(
GpuSdfStackHandle stack,
float delta_mm
);
/**
* @brief Apply hollowing / shelling with a given wall thickness.
*/
GPU_SDF_API GpuSdfStackHandle gpu_sdf_shell(
GpuSdfStackHandle stack,
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.
*/
GPU_SDF_API int gpu_sdf_vectorize(
GpuSdfStackHandle stack,
const GpuOffsetConfig* config,
GpuLayerResult** out_results,
size_t* out_num_layers,
GpuOffsetStats* stats
);
#ifdef __cplusplus
}
#endif
#endif // GPU_SDF_H

View file

@ -347,11 +347,225 @@ def slice_mesh_and_offset(
# Alias for convenience
slice_mesh = slice_mesh_and_offset
class SdfStack:
"""
GPU-resident Signed Distance Field (SDF) stack representing a 3D model or modifier volume.
Enables chained GPU-accelerated Boolean CSG operations (union, intersection, difference,
smooth blend, offset, shelling, procedural 3D infill) without host-to-device data transfers.
"""
def __init__(self, backend_stack: Any):
self._stack = backend_stack
@property
def metadata(self) -> Dict[str, Any]:
"""Dictionary containing spatial bounds, grid dimensions, and layer count."""
return self._stack.get_metadata()
@property
def layer_z_um(self) -> np.ndarray:
"""1D array of layer heights in micrometers (µm)."""
return self._stack.get_layer_z_um()
@property
def layer_z_mm(self) -> np.ndarray:
"""1D array of layer heights in millimeters (mm)."""
return self._stack.get_layer_z_mm()
@property
def shape(self) -> Tuple[int, int, int]:
"""Grid shape (num_layers, height, width)."""
m = self.metadata
return (m["num_layers"], m["height"], m["width"])
def numpy(self) -> np.ndarray:
"""Download raw 3D distance field array (num_layers, H, W) to CPU NumPy float32."""
return self._stack.to_numpy()
def clone(self) -> "SdfStack":
"""Duplicate this SDF stack in GPU VRAM."""
return SdfStack(self._stack.clone())
def union(self, other: "SdfStack", blend_radius_mm: float = 0.0) -> "SdfStack":
"""Compute Boolean Union (A ∪ B) on GPU. Optional blend_radius_mm for smooth fillet union."""
if not isinstance(other, SdfStack):
raise TypeError(f"Operand must be an SdfStack, got {type(other)}")
return SdfStack(self._stack.op_union(other._stack, float(blend_radius_mm)))
def intersection(self, other: "SdfStack", blend_radius_mm: float = 0.0) -> "SdfStack":
"""Compute Boolean Intersection (A ∩ B) on GPU. Optional blend_radius_mm for smooth intersection."""
if not isinstance(other, SdfStack):
raise TypeError(f"Operand must be an SdfStack, got {type(other)}")
return SdfStack(self._stack.op_intersection(other._stack, float(blend_radius_mm)))
def difference(self, other: "SdfStack", blend_radius_mm: float = 0.0) -> "SdfStack":
"""Compute Boolean Difference / Subtraction (A \\ B) on GPU. Optional blend_radius_mm for smooth difference."""
if not isinstance(other, SdfStack):
raise TypeError(f"Operand must be an SdfStack, got {type(other)}")
return SdfStack(self._stack.op_difference(other._stack, float(blend_radius_mm)))
def offset(self, delta_mm: float) -> "SdfStack":
"""Constant-distance offset/dilation (delta > 0) or inset/erosion (delta < 0) on GPU."""
return SdfStack(self._stack.offset(float(delta_mm)))
def shell(self, thickness_mm: float) -> "SdfStack":
"""Hollow / shell the volume with specified wall thickness on GPU."""
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(
self,
perimeters_count: int = 2,
layer_width: float = 0.45,
export_infill_mask: bool = True,
simplify_tolerance_mm: float = 0.02,
subpixel_lerp: bool = True,
offset_distances: Optional[Sequence[float]] = None
) -> List[LayerResult]:
"""
Vectorize GPU SDF stack directly to smooth polygon perimeter contours and infill masks.
Parameters
----------
perimeters_count : int, default=2
Number of perimeter passes.
layer_width : float, default=0.45
Extrusion line width in mm.
export_infill_mask : bool, default=True
Whether to export binary 2D infill mask.
simplify_tolerance_mm : float, default=0.02
Ramer-Douglas-Peucker (RDP) polygon simplification tolerance in mm (eliminates micro-steps).
subpixel_lerp : bool, default=True
Continuous SDF linear interpolation for sub-pixel boundary placement at arbitrary angles.
offset_distances : sequence of float, optional
Custom offset distances.
"""
offsets_float = [float(d) for d in offset_distances] if offset_distances is not None else []
raw_res = self._stack.vectorize(
int(perimeters_count),
float(layer_width),
bool(export_infill_mask),
float(simplify_tolerance_mm),
bool(subpixel_lerp),
offsets_float
)
return [LayerResult(r) for r in raw_res]
# Operator Overloads for fluent Python syntax
def __or__(self, other: "SdfStack") -> "SdfStack":
return self.union(other)
def __and__(self, other: "SdfStack") -> "SdfStack":
return self.intersection(other)
def __sub__(self, other: "SdfStack") -> "SdfStack":
return self.difference(other)
def __add__(self, delta: float) -> "SdfStack":
if isinstance(delta, (int, float)):
return self.offset(float(delta))
raise TypeError(f"Unsupported operand type for +: 'SdfStack' and '{type(delta)}'")
def __repr__(self) -> str:
m = self.metadata
return f"<SdfStack layers={m['num_layers']} shape=({m['height']}, {m['width']}) pitch={m['pitch_mm']:.2f}mm>"
def slice_mesh_to_sdf(
mesh_or_triangles: Any,
layer_height: float = 0.20,
first_layer_height: Optional[float] = None,
pitch_mm: float = 0.10,
pad_mm: float = 1.5
) -> SdfStack:
"""
Slice a 3D mesh directly to a GPU-resident Signed Distance Field (SDF) stack.
Parameters
----------
mesh_or_triangles : trimesh.Trimesh OR np.ndarray
Mesh or triangle array of shape (N, 3, 3) or (N, 9).
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.
pitch_mm : float, default=0.10
Pixel raster pitch in millimeters.
pad_mm : float, default=1.5
Bounding box padding in millimeters.
Returns
-------
SdfStack
GPU-resident SDF stack object ready for CSG operations or vectorization.
"""
if not _HAS_BACKEND or _backend is None:
raise RuntimeError("CUDASlicer backend extension (_cudaslicer_backend) is not available.")
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)
backend_sdf = _backend.slice_mesh_to_sdf(
tris, min_x, min_y, min_z, max_x, max_y, max_z,
float(layer_height), float(h_first), float(pitch_mm), float(pad_mm)
)
return SdfStack(backend_sdf)
def masks_to_sdf(
masks: np.ndarray,
layer_z_um: Optional[Sequence[int]] = None,
min_x_mm: float = 0.0,
min_y_mm: float = 0.0,
pitch_mm: float = 0.10
) -> SdfStack:
"""
Compute a GPU-resident Signed Distance Field from 2D (H, W) or 3D (N, H, W) binary masks.
"""
if not _HAS_BACKEND or _backend is None:
raise RuntimeError("CUDASlicer backend extension (_cudaslicer_backend) is not available.")
masks_u8 = np.ascontiguousarray(masks, dtype=np.uint8)
z_arr = np.ascontiguousarray(layer_z_um, dtype=np.int32) if layer_z_um is not None else np.array([], dtype=np.int32)
backend_sdf = _backend.masks_to_sdf(masks_u8, z_arr, float(min_x_mm), float(min_y_mm), float(pitch_mm))
return SdfStack(backend_sdf)
__all__ = [
"offset_masks",
"offset_polygons",
"slice_mesh_and_offset",
"slice_mesh",
"slice_mesh_to_sdf",
"masks_to_sdf",
"SdfStack",
"LayerResult",
"is_available",
"has_cuda",
@ -361,3 +575,4 @@ __all__ = [
"get_cuda_device_count",
"__version__",
]

View file

@ -362,8 +362,352 @@ py::list py_slice_mesh_and_offset(
return py_layer_results;
}
#include "gpu_sdf.h"
#include <memory>
class PyGpuSdfStack {
private:
GpuSdfStackHandle handle_;
public:
PyGpuSdfStack(GpuSdfStackHandle handle) : handle_(handle) {
if (!handle_) throw std::runtime_error("Failed to allocate GPU SDF Stack.");
}
~PyGpuSdfStack() {
if (handle_) {
gpu_sdf_destroy(handle_);
handle_ = nullptr;
}
}
PyGpuSdfStack(const PyGpuSdfStack&) = delete;
PyGpuSdfStack& operator=(const PyGpuSdfStack&) = delete;
PyGpuSdfStack(PyGpuSdfStack&& other) noexcept : handle_(other.handle_) {
other.handle_ = nullptr;
}
PyGpuSdfStack& operator=(PyGpuSdfStack&& other) noexcept {
if (this != &other) {
if (handle_) gpu_sdf_destroy(handle_);
handle_ = other.handle_;
other.handle_ = nullptr;
}
return *this;
}
GpuSdfStackHandle get_handle() const { return handle_; }
std::shared_ptr<PyGpuSdfStack> clone() const {
return std::make_shared<PyGpuSdfStack>(gpu_sdf_clone(handle_));
}
py::dict get_metadata() const {
GpuSdfMetadata meta;
if (gpu_sdf_get_metadata(handle_, &meta) != GPU_OFFSET_SUCCESS) {
throw std::runtime_error("Failed to get SDF metadata.");
}
py::dict d;
d["width"] = meta.width;
d["height"] = meta.height;
d["num_layers"] = meta.num_layers;
d["min_x_um"] = meta.min_x_um;
d["min_y_um"] = meta.min_y_um;
d["pitch_um"] = meta.pitch_um;
d["min_x_mm"] = (float)meta.min_x_um * 0.001f;
d["min_y_mm"] = (float)meta.min_y_um * 0.001f;
d["pitch_mm"] = (float)meta.pitch_um * 0.001f;
return d;
}
py::array_t<int32_t> get_layer_z_um() const {
GpuSdfMetadata meta;
gpu_sdf_get_metadata(handle_, &meta);
py::array_t<int32_t> arr((ssize_t)meta.num_layers);
auto buf = arr.request();
gpu_sdf_get_layer_z_um(handle_, static_cast<int32_t*>(buf.ptr));
return arr;
}
py::array_t<float> get_layer_z_mm() const {
GpuSdfMetadata meta;
gpu_sdf_get_metadata(handle_, &meta);
std::vector<int32_t> z_um(meta.num_layers);
gpu_sdf_get_layer_z_um(handle_, z_um.data());
py::array_t<float> arr((ssize_t)meta.num_layers);
auto r = arr.mutable_unchecked<1>();
for (size_t i = 0; i < meta.num_layers; ++i) {
r(i) = (float)z_um[i] * 0.001f;
}
return arr;
}
py::array_t<float> to_numpy() const {
GpuSdfMetadata meta;
gpu_sdf_get_metadata(handle_, &meta);
std::vector<ssize_t> shape = { (ssize_t)meta.num_layers, (ssize_t)meta.height, (ssize_t)meta.width };
py::array_t<float> out_arr(shape);
auto buf = out_arr.request();
if (gpu_sdf_to_host(handle_, static_cast<float*>(buf.ptr)) != GPU_OFFSET_SUCCESS) {
throw std::runtime_error("Failed to copy SDF to NumPy host buffer.");
}
return out_arr;
}
std::shared_ptr<PyGpuSdfStack> csg_op(
const PyGpuSdfStack& other,
const std::string& op_name,
const std::string& match_mode_name = "dominant",
float blend_radius_mm = 0.0f
) const {
GpuSdfOp op = GPU_SDF_OP_UNION;
if (op_name == "union" || op_name == "or" || op_name == "+") op = GPU_SDF_OP_UNION;
else if (op_name == "intersection" || op_name == "and" || op_name == "&") op = GPU_SDF_OP_INTERSECTION;
else if (op_name == "difference" || op_name == "subtract" || op_name == "-") op = GPU_SDF_OP_DIFFERENCE;
else if (op_name == "smooth_union") op = GPU_SDF_OP_SMOOTH_UNION;
else if (op_name == "smooth_intersection" || op_name == "smooth_intersect") op = GPU_SDF_OP_SMOOTH_INTERSECT;
else if (op_name == "smooth_difference" || op_name == "smooth_diff") op = GPU_SDF_OP_SMOOTH_DIFF;
else throw std::invalid_argument("Unknown CSG op: " + op_name);
GpuSdfLayerMatchMode match = GPU_SDF_LAYER_MATCH_A_DOMINANT;
if (match_mode_name == "dominant" || match_mode_name == "a") match = GPU_SDF_LAYER_MATCH_A_DOMINANT;
else if (match_mode_name == "union") match = GPU_SDF_LAYER_MATCH_UNION;
else if (match_mode_name == "intersection" || match_mode_name == "intersect") match = GPU_SDF_LAYER_MATCH_INTERSECTION;
else throw std::invalid_argument("Unknown layer match mode: " + match_mode_name);
GpuSdfStackHandle res = gpu_sdf_csg_op(handle_, other.handle_, op, match, blend_radius_mm);
if (!res) throw std::runtime_error("CSG operation failed.");
return std::make_shared<PyGpuSdfStack>(res);
}
std::shared_ptr<PyGpuSdfStack> op_union(const PyGpuSdfStack& other, float blend_k = 0.0f) const {
return csg_op(other, blend_k > 0.0f ? "smooth_union" : "union", "dominant", blend_k);
}
std::shared_ptr<PyGpuSdfStack> op_intersection(const PyGpuSdfStack& other, float blend_k = 0.0f) const {
return csg_op(other, blend_k > 0.0f ? "smooth_intersection" : "intersection", "dominant", blend_k);
}
std::shared_ptr<PyGpuSdfStack> op_difference(const PyGpuSdfStack& other, float blend_k = 0.0f) const {
return csg_op(other, blend_k > 0.0f ? "smooth_difference" : "difference", "dominant", blend_k);
}
std::shared_ptr<PyGpuSdfStack> offset(float delta_mm) const {
GpuSdfStackHandle res = gpu_sdf_offset(handle_, delta_mm);
if (!res) throw std::runtime_error("SDF offset failed.");
return std::make_shared<PyGpuSdfStack>(res);
}
std::shared_ptr<PyGpuSdfStack> shell(float thickness_mm) const {
GpuSdfStackHandle res = gpu_sdf_shell(handle_, thickness_mm);
if (!res) throw std::runtime_error("SDF shell failed.");
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(
int perimeters_count = 2,
float layer_width = 0.45f,
bool export_infill_mask = true,
float simplify_tolerance_mm = 0.02f,
bool subpixel_lerp = true,
const std::vector<float>& offset_distances = std::vector<float>()
) const {
GpuOffsetConfig config = {0};
config.perimeters_count = perimeters_count;
config.layer_width = layer_width;
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.backend = GPU_OFFSET_BACKEND_CUDA;
if (!offset_distances.empty()) {
config.offset_distances_mm = offset_distances.data();
config.num_offsets = offset_distances.size();
}
GpuLayerResult* results = nullptr;
size_t out_num_layers = 0;
GpuOffsetStats stats = {0};
int ret = gpu_sdf_vectorize(handle_, &config, &results, &out_num_layers, &stats);
if (ret != GPU_OFFSET_SUCCESS || !results) {
throw std::runtime_error("SDF vectorization failed.");
}
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;
py_layer_dict["z_um"] = (int32_t)std::round(results[l].z_height * 1000.0f);
// 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;
}
py::dict cuda_array_interface() const {
GpuSdfMetadata meta;
gpu_sdf_get_metadata(handle_, &meta);
float* d_ptr = gpu_sdf_get_device_ptr(handle_);
py::dict iface;
iface["shape"] = py::make_tuple(meta.num_layers, meta.height, meta.width);
iface["typestr"] = "<f4";
iface["data"] = py::make_tuple((uintptr_t)d_ptr, false);
iface["version"] = 3;
return iface;
}
};
// Python Factory Helpers
std::shared_ptr<PyGpuSdfStack> py_slice_mesh_to_sdf(
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_mm = 0.20f,
float first_layer_height_mm = 0.0f,
float pitch_mm = 0.10f,
float pad_mm = 1.5f
) {
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) throw std::runtime_error("Triangle array is empty.");
const GpuTriangle3D* tris_ptr = static_cast<const GpuTriangle3D*>(buf.ptr);
GpuSdfStackHandle handle = gpu_sdf_from_mesh(
tris_ptr, num_tris,
min_x, min_y, min_z, max_x, max_y, max_z,
layer_height_mm, first_layer_height_mm, pitch_mm, pad_mm
);
if (!handle) throw std::runtime_error("Failed to slice mesh to GPU SDF stack.");
return std::make_shared<PyGpuSdfStack>(handle);
}
std::shared_ptr<PyGpuSdfStack> py_masks_to_sdf(
py::array_t<uint8_t, py::array::c_style | py::array::forcecast> in_masks,
py::array_t<int32_t, py::array::c_style | py::array::forcecast> layer_z_um_arr,
float min_x_mm = 0.0f,
float min_y_mm = 0.0f,
float pitch_mm = 0.10f
) {
py::buffer_info buf = in_masks.request();
size_t 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 = (size_t)buf.shape[0];
height = (int)buf.shape[1];
width = (int)buf.shape[2];
} else {
throw std::runtime_error("Input mask must be 2D (H, W) or 3D (N, H, W).");
}
py::buffer_info z_buf = layer_z_um_arr.request();
const int32_t* z_ptr = nullptr;
if (z_buf.size == (ssize_t)num_layers) {
z_ptr = static_cast<const int32_t*>(z_buf.ptr);
}
int32_t min_x_um = (int32_t)std::round(min_x_mm * 1000.0f);
int32_t min_y_um = (int32_t)std::round(min_y_mm * 1000.0f);
int32_t pitch_um = (int32_t)std::round(pitch_mm * 1000.0f);
GpuSdfStackHandle handle = gpu_sdf_from_masks(
static_cast<const uint8_t*>(buf.ptr),
width, height, num_layers,
z_ptr, min_x_um, min_y_um, pitch_um
);
if (!handle) throw std::runtime_error("Failed to create GPU SDF stack from masks.");
return std::make_shared<PyGpuSdfStack>(handle);
}
PYBIND11_MODULE(_cudaslicer_backend, m) {
m.doc() = "GPU/CPU Slicing & SDF/JFA Offsetting C++/CUDA Python Extension";
m.doc() = "GPU/CPU Slicing, Signed Distance Fields (SDF), CSG Booleans & Vectorizer 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.");
@ -417,4 +761,58 @@ PYBIND11_MODULE(_cudaslicer_backend, m) {
py::arg("offset_distances") = std::vector<float>(),
py::arg("backend") = "auto",
"Mesh slicing, perimeter offsetting, and infill mask generation.");
// GPU SDF Stack Factory Functions
m.def("slice_mesh_to_sdf", &py_slice_mesh_to_sdf,
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_mm") = 0.20f,
py::arg("first_layer_height_mm") = 0.0f,
py::arg("pitch_mm") = 0.10f,
py::arg("pad_mm") = 1.5f,
"Slice 3D mesh directly to a GPU-resident Signed Distance Field stack.");
m.def("masks_to_sdf", &py_masks_to_sdf,
py::arg("in_masks"),
py::arg("layer_z_um_arr") = py::array_t<int32_t>(),
py::arg("min_x_mm") = 0.0f,
py::arg("min_y_mm") = 0.0f,
py::arg("pitch_mm") = 0.10f,
"Compute GPU Signed Distance Field stack from 2D/3D binary raster masks.");
// GPU SDF Stack Class Binding
py::class_<PyGpuSdfStack, std::shared_ptr<PyGpuSdfStack>>(m, "GpuSdfStack")
.def("get_metadata", &PyGpuSdfStack::get_metadata, "Get dictionary of spatial metadata.")
.def("get_layer_z_um", &PyGpuSdfStack::get_layer_z_um, "Get 1D array of layer heights in micrometers.")
.def("get_layer_z_mm", &PyGpuSdfStack::get_layer_z_mm, "Get 1D array of layer heights in millimeters.")
.def("to_numpy", &PyGpuSdfStack::to_numpy, "Download SDF float array (N, H, W) to CPU NumPy buffer.")
.def("clone", &PyGpuSdfStack::clone, "Clone this SDF stack on GPU.")
.def("csg_op", &PyGpuSdfStack::csg_op,
py::arg("other"),
py::arg("op_name") = "union",
py::arg("match_mode_name") = "dominant",
py::arg("blend_radius_mm") = 0.0f,
"Execute a GPU CSG operation with another SDF stack.")
.def("op_union", &PyGpuSdfStack::op_union, py::arg("other"), py::arg("blend_radius_mm") = 0.0f, "Compute A U B on GPU.")
.def("op_intersection", &PyGpuSdfStack::op_intersection, 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("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,
py::arg("perimeters_count") = 2,
py::arg("layer_width") = 0.45f,
py::arg("export_infill_mask") = true,
py::arg("simplify_tolerance_mm") = 0.02f,
py::arg("subpixel_lerp") = true,
py::arg("offset_distances") = std::vector<float>(),
"Vectorize SDF stack directly with sub-pixel LERP and RDP polygon simplification.")
.def_property_readonly("__cuda_array_interface__", &PyGpuSdfStack::cuda_array_interface,
"Zero-copy CUDA array interface for PyTorch / CuPy integration.");
}

View file

@ -317,6 +317,39 @@ __global__ void gpu_parity_scan_to_mask_kernel(
}
}
// Helper function for direct mesh slicing to device mask
extern "C" int gpu_slice_mesh_to_device_mask(
const GpuTriangle3D* triangles,
size_t num_triangles,
float min_z, float h_first, float layer_height, int num_layers,
float bounds_min_x, float bounds_min_y, float pitch,
int cols, int rows,
uint8_t* d_masks,
cudaStream_t stream
) {
int row_stride_words = (cols + 31) / 32;
size_t total_words = (size_t)num_layers * rows * row_stride_words;
unsigned int* d_parity = nullptr;
CUDA_CHECK(cudaMalloc(&d_parity, total_words * sizeof(unsigned int)));
CUDA_CHECK(cudaMemsetAsync(d_parity, 0, total_words * sizeof(unsigned int), 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, stream>>>(
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, stream>>>(
d_parity, d_masks, cols, rows, num_layers, row_stride_words
);
cudaFree(d_parity);
return GPU_OFFSET_SUCCESS;
}
// -----------------------------------------------------------------------------
// Public C API Implementation & Backend Management
// -----------------------------------------------------------------------------

1184
src/gpu_sdf.cu Normal file

File diff suppressed because it is too large Load diff

View file

@ -28,6 +28,275 @@ inline GpuPoint2D get_edge_point(int edge_id, int stride_w, float min_x, float m
}
}
// Sub-pixel linear interpolation on continuous distance field
inline GpuPoint2D get_sdf_edge_point(
int edge_id,
const float* sdf,
int width, int height,
float min_x, float min_y, float pitch_mm,
float iso_val
) {
int type = edge_id & 1;
int cell = edge_id >> 1;
int stride_w = width + 1;
int y = cell / stride_w;
int x = cell % stride_w;
if (type == 0) {
// Horizontal edge: from (x, y) to (x + 1, y)
float d0 = -1e5f;
float d1 = -1e5f;
if (y >= 0 && y < height) {
if (x >= 0 && x < width) d0 = sdf[y * width + x];
if (x + 1 >= 0 && x + 1 < width) d1 = sdf[y * width + (x + 1)];
}
float t = 0.5f;
float denom = d1 - d0;
if (std::abs(denom) > 1e-7f) {
t = (iso_val - d0) / denom;
if (t < 0.0f) t = 0.0f;
else if (t > 1.0f) t = 1.0f;
}
return GpuPoint2D{ min_x + ((float)x + t) * pitch_mm, min_y + (float)y * pitch_mm };
} else {
// Vertical edge: from (x, y) to (x, y + 1)
float d0 = -1e5f;
float d3 = -1e5f;
if (x >= 0 && x < width) {
if (y >= 0 && y < height) d0 = sdf[y * width + x];
if (y + 1 >= 0 && y + 1 < height) d3 = sdf[(y + 1) * width + x];
}
float t = 0.5f;
float denom = d3 - d0;
if (std::abs(denom) > 1e-7f) {
t = (iso_val - d0) / denom;
if (t < 0.0f) t = 0.0f;
else if (t > 1.0f) t = 1.0f;
}
return GpuPoint2D{ min_x + (float)x * pitch_mm, min_y + ((float)y + t) * pitch_mm };
}
}
// Ramer-Douglas-Peucker (RDP) Polyline Simplification
static inline void rdp_simplify_recursive(
const std::vector<GpuPoint2D>& pts,
size_t i, size_t j,
float eps_sq,
std::vector<bool>& keep
) {
if (j <= i + 1) return;
float p1x = pts[i].x, p1y = pts[i].y;
float p2x = pts[j].x, p2y = pts[j].y;
float dx = p2x - p1x;
float dy = p2y - p1y;
float len_sq = dx * dx + dy * dy;
float max_dist_sq = 0.0f;
size_t max_idx = i;
for (size_t k = i + 1; k < j; ++k) {
float px = pts[k].x, py = pts[k].y;
float dist_sq = 0.0f;
if (len_sq < 1e-10f) {
float dpx = px - p1x;
float dpy = py - p1y;
dist_sq = dpx * dpx + dpy * dpy;
} else {
float cross = dy * (px - p1x) - dx * (py - p1y);
dist_sq = (cross * cross) / len_sq;
}
if (dist_sq > max_dist_sq) {
max_dist_sq = dist_sq;
max_idx = k;
}
}
if (max_dist_sq > eps_sq) {
keep[max_idx] = true;
rdp_simplify_recursive(pts, i, max_idx, eps_sq, keep);
rdp_simplify_recursive(pts, max_idx, j, eps_sq, keep);
}
}
static inline std::vector<GpuPoint2D> simplify_contour_rdp(
const std::vector<GpuPoint2D>& loop_pts,
float tolerance_mm
) {
size_t n = loop_pts.size();
if (n < 4 || tolerance_mm <= 1e-6f) return loop_pts;
float eps_sq = tolerance_mm * tolerance_mm;
// For closed loop: find point furthest from loop_pts[0]
float max_d_sq = 0.0f;
size_t split_idx = n / 2;
for (size_t i = 1; i < n; ++i) {
float dx = loop_pts[i].x - loop_pts[0].x;
float dy = loop_pts[i].y - loop_pts[0].y;
float d_sq = dx * dx + dy * dy;
if (d_sq > max_d_sq) {
max_d_sq = d_sq;
split_idx = i;
}
}
std::vector<bool> keep(n, false);
keep[0] = true;
keep[split_idx] = true;
rdp_simplify_recursive(loop_pts, 0, split_idx, eps_sq, keep);
rdp_simplify_recursive(loop_pts, split_idx, n - 1, eps_sq, keep);
std::vector<GpuPoint2D> simplified;
simplified.reserve(n);
for (size_t i = 0; i < n; ++i) {
if (keep[i]) simplified.push_back(loop_pts[i]);
}
return simplified;
}
static inline void vectorize_sdf_layer(
const float* sdf,
int width, int height,
float min_x, float min_y, float pitch_mm,
float iso_val,
float simplify_tolerance_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 float* row0 = sdf + y * width;
const float* row1 = sdf + (y + 1) * width;
for (int x = 0; x < width - 1; ++x) {
float c0 = row0[x];
float c1 = row0[x + 1];
float c2 = row1[x + 1];
float c3 = row1[x];
int b0 = (c0 >= iso_val) ? 1 : 0;
int b1 = (c1 >= iso_val) ? 1 : 0;
int b2 = (c2 >= iso_val) ? 1 : 0;
int b3 = (c3 >= iso_val) ? 1 : 0;
int case_idx = b0 | (b1 << 1) | (b2 << 2) | (b3 << 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: {
float center = (c0 + c1 + c2 + c3) * 0.25f;
if (center >= iso_val) {
add_directed_edge(e_left, e_top);
add_directed_edge(e_bottom, e_right);
} else {
add_directed_edge(e_left, e_bottom);
add_directed_edge(e_right, e_top);
}
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: {
float center = (c0 + c1 + c2 + c3) * 0.25f;
if (center >= iso_val) {
add_directed_edge(e_bottom, e_left);
add_directed_edge(e_top, e_right);
} else {
add_directed_edge(e_top, e_left);
add_directed_edge(e_bottom, 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_sdf_edge_point(cur, sdf, width, height, min_x, min_y, pitch_mm, iso_val);
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-7f) {
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) {
std::vector<GpuPoint2D> simplified = simplify_contour_rdp(loop_pts, simplify_tolerance_mm);
if (simplified.size() >= 3) {
GpuContour c;
c.num_points = simplified.size();
c.points = (GpuPoint2D*)malloc(c.num_points * sizeof(GpuPoint2D));
memcpy(c.points, simplified.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_mask_layer(
const uint8_t* mask,
int width, int height,