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

pixi run compile

Build and install the extension

pixi run test

Run the test suite

pixi run bench

Run the benchmark

pixi run format

Format the sources

pixi run -e doc doc-serve

Serve the docs with live reload

pixi run -e doc doc-rebuild

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:

  1. GridView gives 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.

  2. compute_cell_cases classifies every cell’s sign pattern, and compact_active_cells keeps the indices of cells the surface actually crosses.

  3. The per-method kernel writes its triangles into a fixed-size slot per cell (a “triangle soup”).

  4. soup_to_mesh compacts 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:

  1. Add the lookup table to include/mc/, either generated by luts/gen_mc_lut.py (fixed tables expanded from base cases by symmetry) or converted from a reference implementation like luts/convert_mc33_luts.py and luts/convert_vega_luts.py do. Prefer converting: MC33-style tables couple triangulations to test descriptors, and re-deriving that coupling is where historical implementations accumulated bugs.

  2. Subclass MCBase (see include/mc/nagae.cuh for the minimal shape), implement run, and register it with one line: static MCRegistrar<MyVariant> registrar("myvariant");

  3. Mention the new method string in the marching_cubes docstring in src/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.