Development¶
How to build isoext from source, run the tests, and find your way around the code.
Setup¶
You need a Linux machine with an NVIDIA GPU, a CUDA toolkit of version 12.4 or newer, and pixi for environment management. Everything else — Python, PyTorch, nanobind, the build tools — is installed by pixi from the lock file.
git clone https://github.com/GuangyanCai/isoext.git
cd isoext
pixi run compile
compile builds the CUDA extension and installs it editably into the
pixi environment. Rebuilds after source changes are incremental and
triggered automatically on import.
The bindings use nanobind 3 in split mode: the extension targets the
stable ABI with a Python 3.10 floor and holds none of nanobind’s own
library code, which lives in the nanobind-backend package that pip
installs alongside isoext. One wheel per platform therefore covers
every supported Python version, and building from source needs
nanobind 3.0.1 or newer.
One pitfall: some distributions ship an old nvcc (Ubuntu’s
nvidia-cuda-toolkit package installs CUDA 12.0 as /usr/bin/nvcc),
and CMake will pick it up before a newer toolkit installed elsewhere.
The build stops with a clear error if the detected nvcc is older
than 12.4; set the CUDACXX environment variable to the right
compiler if that happens.
Everyday Tasks¶
Command |
What it does |
|---|---|
|
Build and install the extension |
|
Run the test suite |
|
Run the benchmark |
|
Format the sources |
|
Serve the docs with live reload |
|
Rebuild the docs from scratch |
The default environment carries everything development needs,
including PyTorch with CUDA 12.8 wheels; the doc environment adds
Sphinx and Jupyter. The CUDA flavor is chosen inside the environment
definitions in pyproject.toml, so a future CUDA bump changes those
definitions and none of the commands.
format applies the repository’s .clang-format to the CUDA/C++
sources – except the generated lookup-table headers, which follow
their generators instead – and ruff format to the Python files.
Run it before committing.
Tests¶
pixi run test # everything
pixi run pytest tests/test_vega.py -n 2 # one file
The suite runs under pytest-xdist (-n 2) so that a crashed CUDA
context kills a worker instead of the whole run, and the failure is
reported instead of aborting silently. Tests that compare against
scikit-image skip automatically when it is not installed.
Method tests follow a pattern worth keeping: a sphere accuracy check,
a watertightness sweep over random smoothed fields, and, where a
reference implementation exists, a topology comparison against it
(see tests/test_lewiner.py and tests/test_vega.py).
Code Layout¶
include/, src/ CUDA/C++ core (mirrored layout)
grid/ dense and sparse grids, GridView
mc/ marching cubes variants and their tables
extraction.cuh the shared per-cell extraction pipeline
dc.cu, mt.cu dual contouring + surface nets, marching tetrahedra
isoext_ext.cu nanobind bindings (one function per method)
src/isoext/ Python: SDF toolbox, viewer, utilities
tests/ pytest suite
luts/ lookup-table generators and converters
benchmarks/ the timing harness
doc/ this documentation
Architecture¶
Extraction is a pipeline of small pieces that methods share:
GridViewgives kernels a uniform way to read any grid type. It computes cell corner positions and values on the fly instead of materializing them, which is why memory scales with the surface and not the volume.compute_cell_casesclassifies every cell’s sign pattern, andcompact_active_cellskeeps the indices of cells the surface actually crosses.The per-method kernel writes its triangles into a fixed-size slot per cell (a “triangle soup”).
soup_to_meshcompacts the soup and welds shared vertices. Edge crossings are always interpolated in a canonical direction (the corner with the smaller index first) so neighboring cells produce bitwise-identical vertices, which is what makes welding by position safe.
Dual methods (dual contouring, surface nets) instead consume the edge
crossings from get_intersection and build the dual mesh; the two
differ only in where each cell’s vertex is placed.
Adding a Marching Cubes Variant¶
Variants are classes behind a string registry, so a new one does not touch the dispatch code:
Add the lookup table to
include/mc/, either generated byluts/gen_mc_lut.py(fixed tables expanded from base cases by symmetry) or converted from a reference implementation likeluts/convert_mc33_luts.pyandluts/convert_vega_luts.pydo. Prefer converting: MC33-style tables couple triangulations to test descriptors, and re-deriving that coupling is where historical implementations accumulated bugs.Subclass
MCBase(seeinclude/mc/nagae.cuhfor the minimal shape), implementrun, and register it with one line:static MCRegistrar<MyVariant> registrar("myvariant");Mention the new method string in the
marching_cubesdocstring insrc/isoext_ext.cu, and add tests following the pattern above.
Documentation¶
The doc pages are Jupyter notebooks executed in place plus a few Markdown files, built with Sphinx:
pixi run -e doc doc-serve # live preview on :8000
The interactive viewers are serialized viser
scenes: viewer.embed writes a content-addressed .viser file to
doc/_static/scenes/ and returns an iframe pointing at viser’s
static client, so the built pages stay interactive on a static host.
The landing page scene is regenerated by doc/_hero.py; its caption
quotes rounded estimates on purpose. When re-executing notebooks,
delete scene files that no page references anymore — and keep
hero.viser, which is referenced from index.md rather than a
notebook.
Benchmarks¶
pixi run python benchmarks/benchmark.py \
--res 128 512 --json results.json
Each number is the median over repeated iterations after warmup; the first call is reported separately because it includes the driver’s PTX compilation. The tables in Performance round to two significant digits, which absorbs run-to-run jitter.