ManifoldNative.kt
src/jvmMain/kotlin/afield/geometry/solid/manifold/ManifoldNative.kt · 928 lines
@file:afield.runtime.completion.CompletionCategory("Internal")
package afield.geometry.solid.manifold
import java.lang.foreign.Arena
import java.lang.foreign.FunctionDescriptor
import java.lang.foreign.Linker
import java.lang.foreign.MemoryLayout
import java.lang.foreign.MemorySegment
import java.lang.foreign.SegmentAllocator
import java.lang.foreign.SymbolLookup
import java.lang.foreign.ValueLayout.ADDRESS
import java.lang.foreign.ValueLayout.JAVA_DOUBLE
import java.lang.foreign.ValueLayout.JAVA_FLOAT
import java.lang.foreign.ValueLayout.JAVA_INT
import java.lang.foreign.ValueLayout.JAVA_LONG
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.nio.file.Path
import kotlin.io.path.exists
The FFM binding to libmanifoldc that Solid and Section are built on, plus the small public enums they take.
Box code needs only ManifoldLibrary, to ask whether the native library is installed, and the option enums BooleanOp, JoinType and FillRule; the native entry points themselves are internal.
internal object ManifoldNative
The Panama/FFM plumbing under Solid and Section: where libmanifoldc is, the function
descriptors for the C entry points Field uses, and the two upcall stubs that let a Kotlin lambda
be a Manifold callback.
Nothing here is meant to be called from a box — read Solid instead. It is internal for
everything except ManifoldLibrary, which a box may reasonably ask "is this installed?".
Why FFM, and why no wrapper
Manifold ships a C binding library (libmanifoldc, built from manifoldc.h) beside its C++
one, and Homebrew's bottle contains it. That changes the shape of this binding completely compared
to afield.math.optimization.ceres.FfmCeresBackend, the other native solver in the tree:
| Ceres | Manifold | |
|---|---|---|
| upstream API | C++ templates only | C++ and a C ABI |
| so Field needs | a hand-written C++ wrapper (native/ken-ceres) |
nothing |
| build step | ./gradlew buildCeresRedistributable, minutes |
brew install manifold, seconds |
| ABI drift | guarded by a version string we made up | upstream's own SO version |
So there is no native/ directory for this and no CMake: the whole binding is the Kotlin in this
package, calling a library someone else built. That is the case for FFM over JNI in one line — a
JNI binding would still need a compiled shim per platform, which is the thing we get to skip.
The three shapes, and what each one cost
1. Plain downcalls. size_t is JAVA_LONG, int is JAVA_INT, every pointer is ADDRESS.
Handles are looked up eagerly in handle so a library that is present but too old fails on the
first touch of this object, naming the symbol, rather than mid-pipeline.
2. Structs returned by value. manifold_box_min returns a ManifoldVec3 — three doubles,
which AArch64 returns in v0..v2 and x86-64 splits between xmm0 and xmm1. FFM does that
unpacking from VEC3 with no help; what it needs in exchange is a SegmentAllocator as an extra
leading argument, which is why invokeStruct exists and why those calls look different from
the rest.
3. Upcalls — the interesting one. manifold_level_set_seq takes a double (*)(double,
double, double, void*) and marches it into a mesh; manifold_warp takes a ManifoldVec3 (*)(double,
double, double, void*). upcallSdf and upcallWarp turn a Kotlin lambda into a function pointer
for the duration of one call.
Manifold is built with its parallel (TBB) backend, so manifold_warp calls the lambda from its
own C++ thread pool, which works because FFM attaches a foreign thread on entry. The level set
uses the sequential entry point only: the parallel manifold_level_set is about twice as fast but
does not return the same mesh twice (see Solid.levelSet). Two things the bridges handle rather
than hope about:
- An exception must not escape. There are C++ frames between the lambda and the caller; unwinding through them is undefined, and FFM's default for an escaping exception is to abort the VM. So the bridge catches Throwable, remembers the first one, returns a finite sentinel, and Solid.levelSet rethrows after the native call returns.
- A warp lambda must be thread-safe. It is documented as such at every entry point that takes one.
internal object ManifoldNative {
private val linker: Linker = Linker.nativeLinker()
Resolved on first touch of this object, which is also when a missing library is reported.
Arena.global rather than a scoped arena: the library stays loaded for the life of the VM,
because a Solid can outlive any scope we could name.
private val lookup: SymbolLookup by lazy {
val path = ManifoldLibrary.resolvedPath
?: throw UnsatisfiedLinkError(
"Manifold is not installed. ${ManifoldLibrary.HOW_TO_INSTALL}\n" +
"Looked at: " + ManifoldLibrary.searchedPaths.joinToString(", "),
)
SymbolLookup.libraryLookup(path, Arena.global())
}
Layouts
val VEC2: MemoryLayout
ManifoldVec2 { double x, y; }.
val VEC2: MemoryLayout = MemoryLayout.structLayout(
JAVA_DOUBLE.withName("x"),
JAVA_DOUBLE.withName("y"),
)
val VEC3: MemoryLayout
ManifoldVec3 { double x, y, z; }.
val VEC3: MemoryLayout = MemoryLayout.structLayout(
JAVA_DOUBLE.withName("x"),
JAVA_DOUBLE.withName("y"),
JAVA_DOUBLE.withName("z"),
)
val RAY_HIT: MemoryLayout
ManifoldRayHit { uint64_t face_id; double distance; ManifoldVec3 position, normal; }.
64 bytes, mixed integer and float, so unlike VEC3 this one comes back through memory rather than registers on both of Field's architectures. FFM picks that up from the layout; the only reason to know it is that the field offsets below are read straight out of the returned segment.
val RAY_HIT: MemoryLayout = MemoryLayout.structLayout(
JAVA_LONG.withName("face_id"),
JAVA_DOUBLE.withName("distance"),
VEC3.withName("position"),
VEC3.withName("normal"),
)
val PAIR: MemoryLayout
ManifoldManifoldPair { ManifoldManifold *first, *second; }, returned by value by the splits.
val PAIR: MemoryLayout = MemoryLayout.structLayout(
ADDRESS.withName("first"),
ADDRESS.withName("second"),
)
Handles
internal val boundDescriptors
Every descriptor a handle was bound with, by symbol, for ManifoldHeaderCheck.
internal val boundDescriptors = java.util.concurrent.ConcurrentHashMap<String, FunctionDescriptor>()
private fun handle(name: String, descriptor: FunctionDescriptor): MethodHandle {
boundDescriptors[name] = descriptor
val symbol = lookup.find(name).orElseThrow {
UnsatisfiedLinkError(
"libmanifoldc has no symbol $name — this is Manifold " +
"${ManifoldLibrary.reportedVersion ?: "of an unknown version"}, and Field needs 3.x. " +
ManifoldLibrary.HOW_TO_INSTALL,
)
}
return linker.downcallHandle(symbol, descriptor)
}
T* f() — the manifold_alloc_* family only, which takes no arguments and returns malloc'd
memory of the right size.
Deliberately not shared with construct: manifold_alloc_manifold() and
manifold_tetrahedron(void* mem) look alike from a distance and differ by one parameter, and
calling the second through the first's descriptor is a WrongMethodTypeException at the first
invocation rather than anything the linker notices.
private fun alloc(name: String) = handle(name, FunctionDescriptor.of(ADDRESS))
T* f(void* mem) — a constructor whose only argument is where to build.
private fun construct(name: String) = handle(name, FunctionDescriptor.of(ADDRESS, ADDRESS))
void f(T*) — the delete family.
private fun consume(name: String) = handle(name, FunctionDescriptor.ofVoid(ADDRESS))
double f(T*) — the scalar measurements.
private fun measure(name: String) = handle(name, FunctionDescriptor.of(JAVA_DOUBLE, ADDRESS))
size_t f(T*) — the counts.
private fun count(name: String) = handle(name, FunctionDescriptor.of(JAVA_LONG, ADDRESS))
int f(T*) — the predicates and the status enum.
private fun flag(name: String) = handle(name, FunctionDescriptor.of(JAVA_INT, ADDRESS))
T* f(void* mem, T* a) — the unary op shape that dominates this API.
private fun unary(name: String) = handle(name, FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS))
T* f(void* mem, T* a, T* b) — the booleans.
private fun binary(name: String) =
handle(name, FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, ADDRESS))
T* f(void* mem, T* a, double...) — a transform taking n doubles.
private fun unaryWithDoubles(name: String, n: Int) = handle(
name,
FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, *Array(n) { JAVA_DOUBLE }),
)
allocation and destruction
Every constructor in this API takes the memory to build into as its first argument, so there
are two ways to own a Manifold object from the JVM: allocate from an FFM Arena and call
manifold_destruct_*, or take the library's own malloc via manifold_alloc_* and call
manifold_delete_* (destruct and free). This binding takes the second, for a reason worth
stating: a confined Arena is thread-confined and a Solid built in a box is routinely read from
the runloop thread, while a shared Arena costs a thread handshake to close. malloc has neither
problem, and ManifoldHandle puts a Cleaner behind it so a box that abandons a Solid does not
leak one.
val allocManifold: MethodHandle by lazy { alloc("manifold_alloc_manifold") }
val allocManifoldVec: MethodHandle by lazy { alloc("manifold_alloc_manifold_vec") }
val allocSection: MethodHandle by lazy { alloc("manifold_alloc_cross_section") }
val allocSectionVec: MethodHandle by lazy { alloc("manifold_alloc_cross_section_vec") }
val allocMeshGL: MethodHandle by lazy { alloc("manifold_alloc_meshgl") }
val allocMeshGL64: MethodHandle by lazy { alloc("manifold_alloc_meshgl64") }
val allocBox: MethodHandle by lazy { alloc("manifold_alloc_box") }
val allocRect: MethodHandle by lazy { alloc("manifold_alloc_rect") }
val allocPolygons: MethodHandle by lazy { alloc("manifold_alloc_polygons") }
val allocSimplePolygon: MethodHandle by lazy { alloc("manifold_alloc_simple_polygon") }
val allocRayHitVec: MethodHandle by lazy { alloc("manifold_alloc_ray_hit_vec") }
val deleteManifold: MethodHandle by lazy { consume("manifold_delete_manifold") }
val deleteManifoldVec: MethodHandle by lazy { consume("manifold_delete_manifold_vec") }
val deleteSection: MethodHandle by lazy { consume("manifold_delete_cross_section") }
val deleteSectionVec: MethodHandle by lazy { consume("manifold_delete_cross_section_vec") }
val deleteMeshGL: MethodHandle by lazy { consume("manifold_delete_meshgl") }
val deleteMeshGL64: MethodHandle by lazy { consume("manifold_delete_meshgl64") }
val deleteBox: MethodHandle by lazy { consume("manifold_delete_box") }
val deleteRect: MethodHandle by lazy { consume("manifold_delete_rect") }
val deletePolygons: MethodHandle by lazy { consume("manifold_delete_polygons") }
val deleteSimplePolygon: MethodHandle by lazy { consume("manifold_delete_simple_polygon") }
val deleteRayHitVec: MethodHandle by lazy { consume("manifold_delete_ray_hit_vec") }
3D constructors
val empty: MethodHandle by lazy { construct("manifold_empty") }
val tetrahedron: MethodHandle by lazy { construct("manifold_tetrahedron") }
val copy: MethodHandle by lazy { unary("manifold_copy") }
val cube: MethodHandle by lazy
val cube: MethodHandle by lazy {
handle("manifold_cube", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_INT))
}
val sphere: MethodHandle by lazy
val sphere: MethodHandle by lazy {
handle("manifold_sphere", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_INT))
}
val cylinder: MethodHandle by lazy
val cylinder: MethodHandle by lazy {
handle(
"manifold_cylinder",
FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_INT, JAVA_INT),
)
}
val extrude: MethodHandle by lazy
val extrude: MethodHandle by lazy {
handle(
"manifold_extrude",
FunctionDescriptor.of(
ADDRESS, ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_INT, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE,
),
)
}
val revolve: MethodHandle by lazy
val revolve: MethodHandle by lazy {
handle("manifold_revolve", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_INT, JAVA_DOUBLE))
}
val levelSetSeq: MethodHandle by lazy
val levelSetSeq: MethodHandle by lazy {
handle(
"manifold_level_set_seq",
FunctionDescriptor.of(
ADDRESS, ADDRESS, ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, ADDRESS,
),
)
}
3D booleans and combination
val union: MethodHandle by lazy { binary("manifold_union") }
val difference: MethodHandle by lazy { binary("manifold_difference") }
val intersection: MethodHandle by lazy { binary("manifold_intersection") }
val minkowskiSum: MethodHandle by lazy { binary("manifold_minkowski_sum") }
val minkowskiDifference: MethodHandle by lazy { binary("manifold_minkowski_difference") }
val batchBoolean: MethodHandle by lazy
val batchBoolean: MethodHandle by lazy {
handle("manifold_batch_boolean", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_INT))
}
val compose: MethodHandle by lazy { unary("manifold_compose") }
val decompose: MethodHandle by lazy { unary("manifold_decompose") }
val splitByPlane: MethodHandle by lazy
val splitByPlane: MethodHandle by lazy {
handle(
"manifold_split_by_plane",
FunctionDescriptor.of(
PAIR, ADDRESS, ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE,
),
)
}
val split: MethodHandle by lazy
val split: MethodHandle by lazy {
handle("manifold_split", FunctionDescriptor.of(PAIR, ADDRESS, ADDRESS, ADDRESS, ADDRESS))
}
val trimByPlane: MethodHandle by lazy { unaryWithDoubles("manifold_trim_by_plane", 4) }
3D transforms
val translate: MethodHandle by lazy { unaryWithDoubles("manifold_translate", 3) }
val rotate: MethodHandle by lazy { unaryWithDoubles("manifold_rotate", 3) }
val scale: MethodHandle by lazy { unaryWithDoubles("manifold_scale", 3) }
val mirror: MethodHandle by lazy { unaryWithDoubles("manifold_mirror", 3) }
val transform: MethodHandle by lazy { unaryWithDoubles("manifold_transform", 12) }
val setTolerance: MethodHandle by lazy { unaryWithDoubles("manifold_set_tolerance", 1) }
val simplify: MethodHandle by lazy { unaryWithDoubles("manifold_simplify", 1) }
val refineToLength: MethodHandle by lazy { unaryWithDoubles("manifold_refine_to_length", 1) }
val refineToTolerance: MethodHandle by lazy
val refineToTolerance: MethodHandle by lazy { unaryWithDoubles("manifold_refine_to_tolerance", 1) }
val smoothOut: MethodHandle by lazy { unaryWithDoubles("manifold_smooth_out", 2) }
val refine: MethodHandle by lazy
val refine: MethodHandle by lazy {
handle("manifold_refine", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_INT))
}
val calculateNormals: MethodHandle by lazy
val calculateNormals: MethodHandle by lazy {
handle(
"manifold_calculate_normals",
FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_INT, JAVA_DOUBLE),
)
}
val warp: MethodHandle by lazy
val warp: MethodHandle by lazy {
handle("manifold_warp", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, ADDRESS, ADDRESS))
}
val hull: MethodHandle by lazy { unary("manifold_hull") }
val hullPoints: MethodHandle by lazy
val hullPoints: MethodHandle by lazy {
handle("manifold_hull_pts", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_LONG))
}
3D to 2D
val slice: MethodHandle by lazy { unaryWithDoubles("manifold_slice", 1) }
val project: MethodHandle by lazy { unary("manifold_project") }
3D info
val volume: MethodHandle by lazy { measure("manifold_volume") }
val surfaceArea: MethodHandle by lazy { measure("manifold_surface_area") }
val tolerance: MethodHandle by lazy { measure("manifold_get_tolerance") }
val epsilon: MethodHandle by lazy { measure("manifold_epsilon") }
val numVert: MethodHandle by lazy { count("manifold_num_vert") }
val numEdge: MethodHandle by lazy { count("manifold_num_edge") }
val numTri: MethodHandle by lazy { count("manifold_num_tri") }
val numProp: MethodHandle by lazy { count("manifold_num_prop") }
val genus: MethodHandle by lazy { flag("manifold_genus") }
val isEmpty: MethodHandle by lazy { flag("manifold_is_empty") }
val status: MethodHandle by lazy { flag("manifold_status") }
val originalId: MethodHandle by lazy { flag("manifold_original_id") }
val boundingBox: MethodHandle by lazy { unary("manifold_bounding_box") }
val minGap: MethodHandle by lazy
val minGap: MethodHandle by lazy {
handle("manifold_min_gap", FunctionDescriptor.of(JAVA_DOUBLE, ADDRESS, ADDRESS, JAVA_DOUBLE))
}
val circularSegments: MethodHandle by lazy
val circularSegments: MethodHandle by lazy {
handle("manifold_get_circular_segments", FunctionDescriptor.of(JAVA_INT, JAVA_DOUBLE))
}
val setCircularSegments: MethodHandle by lazy
val setCircularSegments: MethodHandle by lazy {
handle("manifold_set_circular_segments", FunctionDescriptor.ofVoid(JAVA_INT))
}
val setMinCircularAngle: MethodHandle by lazy
val setMinCircularAngle: MethodHandle by lazy {
handle("manifold_set_min_circular_angle", FunctionDescriptor.ofVoid(JAVA_DOUBLE))
}
val setMinCircularEdgeLength: MethodHandle by lazy
val setMinCircularEdgeLength: MethodHandle by lazy {
handle("manifold_set_min_circular_edge_length", FunctionDescriptor.ofVoid(JAVA_DOUBLE))
}
val resetCircularDefaults: MethodHandle by lazy
val resetCircularDefaults: MethodHandle by lazy {
handle("manifold_reset_to_circular_defaults", FunctionDescriptor.ofVoid())
}
ray casting
val rayCast: MethodHandle by lazy
val rayCast: MethodHandle by lazy {
handle(
"manifold_ray_cast",
FunctionDescriptor.of(
ADDRESS, ADDRESS, ADDRESS,
JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE,
),
)
}
val rayHitVecLength: MethodHandle by lazy { count("manifold_ray_hit_vec_length") }
val rayHitVecGet: MethodHandle by lazy
val rayHitVecGet: MethodHandle by lazy {
handle("manifold_ray_hit_vec_get", FunctionDescriptor.of(RAY_HIT, ADDRESS, JAVA_LONG))
}
manifold vectors (for the batch ops)
val manifoldVec: MethodHandle by lazy
val manifoldVec: MethodHandle by lazy {
handle("manifold_manifold_vec", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_LONG))
}
val manifoldVecSet: MethodHandle by lazy
val manifoldVecSet: MethodHandle by lazy {
handle("manifold_manifold_vec_set", FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, ADDRESS))
}
val manifoldVecLength: MethodHandle by lazy { count("manifold_manifold_vec_length") }
val manifoldVecGet: MethodHandle by lazy
val manifoldVecGet: MethodHandle by lazy {
handle("manifold_manifold_vec_get", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_LONG))
}
val batchHull: MethodHandle by lazy { unary("manifold_batch_hull") }
MeshGL: the mesh bridge both ways
val getMeshGL: MethodHandle by lazy { unary("manifold_get_meshgl") }
val getMeshGLWithNormals: MethodHandle by lazy
val getMeshGLWithNormals: MethodHandle by lazy {
handle("manifold_get_meshgl_w_normals", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_INT))
}
val ofMeshGL: MethodHandle by lazy { unary("manifold_of_meshgl") }
val meshGL: MethodHandle by lazy
val meshGL: MethodHandle by lazy {
handle(
"manifold_meshgl",
FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS, JAVA_LONG),
)
}
val meshGLNumVert: MethodHandle by lazy { count("manifold_meshgl_num_vert") }
val meshGLNumTri: MethodHandle by lazy { count("manifold_meshgl_num_tri") }
val meshGLNumProp: MethodHandle by lazy { count("manifold_meshgl_num_prop") }
val meshGLVertProperties: MethodHandle by lazy { unary("manifold_meshgl_vert_properties") }
val meshGLTriVerts: MethodHandle by lazy { unary("manifold_meshgl_tri_verts") }
val meshGLMergeLength: MethodHandle by lazy { count("manifold_meshgl_merge_length") }
val meshGLMergeFromVert: MethodHandle by lazy { unary("manifold_meshgl_merge_from_vert") }
val meshGLMergeToVert: MethodHandle by lazy { unary("manifold_meshgl_merge_to_vert") }
val getMeshGL64: MethodHandle by lazy { unary("manifold_get_meshgl64") }
MeshGL64 is the fabrication bridge. Unlike the ordinary rendering export it retains Manifold's internal double precision and, crucially, the run/face identity needed to reconstruct the polygonal planar faces behind the output triangulation.
val ofMeshGL64: MethodHandle by lazy { unary("manifold_of_meshgl64") }
val meshGL64: MethodHandle by lazy
val meshGL64: MethodHandle by lazy {
handle(
"manifold_meshgl64",
FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS, JAVA_LONG),
)
}
val meshGL64NumVert: MethodHandle by lazy { count("manifold_meshgl64_num_vert") }
val meshGL64NumTri: MethodHandle by lazy { count("manifold_meshgl64_num_tri") }
val meshGL64NumProp: MethodHandle by lazy { count("manifold_meshgl64_num_prop") }
val meshGL64MergeLength: MethodHandle by lazy { count("manifold_meshgl64_merge_length") }
val meshGL64RunIndexLength: MethodHandle by lazy { count("manifold_meshgl64_run_index_length") }
val meshGL64RunOriginalIdLength: MethodHandle by lazy
val meshGL64RunOriginalIdLength: MethodHandle by lazy { count("manifold_meshgl64_run_original_id_length") }
val meshGL64FaceIdLength: MethodHandle by lazy { count("manifold_meshgl64_face_id_length") }
val meshGL64VertProperties: MethodHandle by lazy { unary("manifold_meshgl64_vert_properties") }
val meshGL64TriVerts: MethodHandle by lazy { unary("manifold_meshgl64_tri_verts") }
val meshGL64MergeFromVert: MethodHandle by lazy { unary("manifold_meshgl64_merge_from_vert") }
val meshGL64MergeToVert: MethodHandle by lazy { unary("manifold_meshgl64_merge_to_vert") }
val meshGL64RunIndex: MethodHandle by lazy { unary("manifold_meshgl64_run_index") }
val meshGL64RunOriginalId: MethodHandle by lazy { unary("manifold_meshgl64_run_original_id") }
val meshGL64FaceId: MethodHandle by lazy { unary("manifold_meshgl64_face_id") }
val meshGL64Tolerance: MethodHandle by lazy
val meshGL64Tolerance: MethodHandle by lazy {
handle("manifold_meshgl64_tolerance", FunctionDescriptor.of(JAVA_DOUBLE, ADDRESS))
}
boxes and rects
val boxOf: MethodHandle by lazy
val boxOf: MethodHandle by lazy {
handle(
"manifold_box",
FunctionDescriptor.of(
ADDRESS, ADDRESS,
JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE,
),
)
}
val boxMin: MethodHandle by lazy
val boxMin: MethodHandle by lazy { handle("manifold_box_min", FunctionDescriptor.of(VEC3, ADDRESS)) }
val boxMax: MethodHandle by lazy
val boxMax: MethodHandle by lazy { handle("manifold_box_max", FunctionDescriptor.of(VEC3, ADDRESS)) }
val rectMin: MethodHandle by lazy
val rectMin: MethodHandle by lazy { handle("manifold_rect_min", FunctionDescriptor.of(VEC2, ADDRESS)) }
val rectMax: MethodHandle by lazy
val rectMax: MethodHandle by lazy { handle("manifold_rect_max", FunctionDescriptor.of(VEC2, ADDRESS)) }
2D: cross sections
val sectionEmpty: MethodHandle by lazy { construct("manifold_cross_section_empty") }
val sectionCopy: MethodHandle by lazy { unary("manifold_cross_section_copy") }
val sectionSquare: MethodHandle by lazy
val sectionSquare: MethodHandle by lazy {
handle(
"manifold_cross_section_square",
FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_INT),
)
}
val sectionCircle: MethodHandle by lazy
val sectionCircle: MethodHandle by lazy {
handle("manifold_cross_section_circle", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_INT))
}
val sectionOfPolygons: MethodHandle by lazy
val sectionOfPolygons: MethodHandle by lazy {
handle(
"manifold_cross_section_of_polygons",
FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_INT),
)
}
val sectionUnion: MethodHandle by lazy { binary("manifold_cross_section_union") }
val sectionDifference: MethodHandle by lazy { binary("manifold_cross_section_difference") }
val sectionIntersection: MethodHandle by lazy { binary("manifold_cross_section_intersection") }
val sectionTranslate: MethodHandle by lazy
val sectionTranslate: MethodHandle by lazy { unaryWithDoubles("manifold_cross_section_translate", 2) }
val sectionRotate: MethodHandle by lazy { unaryWithDoubles("manifold_cross_section_rotate", 1) }
val sectionScale: MethodHandle by lazy { unaryWithDoubles("manifold_cross_section_scale", 2) }
val sectionMirror: MethodHandle by lazy { unaryWithDoubles("manifold_cross_section_mirror", 2) }
val sectionSimplify: MethodHandle by lazy
val sectionSimplify: MethodHandle by lazy { unaryWithDoubles("manifold_cross_section_simplify", 1) }
val sectionOffset: MethodHandle by lazy
val sectionOffset: MethodHandle by lazy {
handle(
"manifold_cross_section_offset",
FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_DOUBLE, JAVA_INT, JAVA_DOUBLE, JAVA_INT),
)
}
val sectionHull: MethodHandle by lazy { unary("manifold_cross_section_hull") }
val sectionDecompose: MethodHandle by lazy { unary("manifold_cross_section_decompose") }
val sectionArea: MethodHandle by lazy { measure("manifold_cross_section_area") }
val sectionNumVert: MethodHandle by lazy { count("manifold_cross_section_num_vert") }
val sectionNumContour: MethodHandle by lazy { count("manifold_cross_section_num_contour") }
val sectionIsEmpty: MethodHandle by lazy { flag("manifold_cross_section_is_empty") }
val sectionBounds: MethodHandle by lazy { unary("manifold_cross_section_bounds") }
val sectionToPolygons: MethodHandle by lazy { unary("manifold_cross_section_to_polygons") }
val sectionVecLength: MethodHandle by lazy { count("manifold_cross_section_vec_length") }
val sectionVecGet: MethodHandle by lazy
val sectionVecGet: MethodHandle by lazy {
handle("manifold_cross_section_vec_get", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_LONG))
}
val sectionVecOf: MethodHandle by lazy
val sectionVecOf: MethodHandle by lazy {
handle("manifold_cross_section_vec", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_LONG))
}
val sectionVecSet: MethodHandle by lazy
val sectionVecSet: MethodHandle by lazy {
handle("manifold_cross_section_vec_set", FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, ADDRESS))
}
val sectionBatchBoolean: MethodHandle by lazy
val sectionBatchBoolean: MethodHandle by lazy {
handle("manifold_cross_section_batch_boolean", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_INT))
}
polygons
val simplePolygon: MethodHandle by lazy
val simplePolygon: MethodHandle by lazy {
handle("manifold_simple_polygon", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_LONG))
}
val polygonsOf: MethodHandle by lazy
val polygonsOf: MethodHandle by lazy {
handle("manifold_polygons", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS, JAVA_LONG))
}
val polygonsLength: MethodHandle by lazy { count("manifold_polygons_length") }
val polygonsSimpleLength: MethodHandle by lazy
val polygonsSimpleLength: MethodHandle by lazy {
handle("manifold_polygons_simple_length", FunctionDescriptor.of(JAVA_LONG, ADDRESS, JAVA_LONG))
}
val polygonsGetPoint: MethodHandle by lazy
val polygonsGetPoint: MethodHandle by lazy {
handle("manifold_polygons_get_point", FunctionDescriptor.of(VEC2, ADDRESS, JAVA_LONG, JAVA_LONG))
}
val simplePolygonSize: MethodHandle by lazy
val simplePolygonSize: MethodHandle by lazy {
handle("manifold_simple_polygon_size", FunctionDescriptor.of(JAVA_LONG))
}
val polygonsSize: MethodHandle by lazy
val polygonsSize: MethodHandle by lazy {
handle("manifold_polygons_size", FunctionDescriptor.of(JAVA_LONG))
}
Invocation helpers
fun invokeStruct(handle: MethodHandle, allocator: SegmentAllocator, vararg args: Any?): MemorySegment
Invoke a handle whose C return type is a struct, which FFM models as an extra leading
SegmentAllocator holding the returned bytes. Kotlin cannot make a signature-polymorphic
invokeExact call cleanly, so like afield.math.optimization.ceres.FfmCeresBackend this whole file goes
through MethodHandle.invokeWithArguments: the boxing is a rounding error next to the
geometry, and the argument lists stay readable.
fun invokeStruct(handle: MethodHandle, allocator: SegmentAllocator, vararg args: Any?): MemorySegment =
call { handle.invokeWithArguments(allocator, *args) as MemorySegment }
fun ptr(handle: MethodHandle, vararg args: Any?): MemorySegment
fun ptr(handle: MethodHandle, vararg args: Any?): MemorySegment =
call { handle.invokeWithArguments(*args) as MemorySegment }
fun double(handle: MethodHandle, vararg args: Any?): Double
fun double(handle: MethodHandle, vararg args: Any?): Double =
call { handle.invokeWithArguments(*args) as Double }
fun long(handle: MethodHandle, vararg args: Any?): Long
fun long(handle: MethodHandle, vararg args: Any?): Long =
call { handle.invokeWithArguments(*args) as Long }
fun int(handle: MethodHandle, vararg args: Any?): Int
fun int(handle: MethodHandle, vararg args: Any?): Int =
call { handle.invokeWithArguments(*args) as Int }
fun unit(handle: MethodHandle, vararg args: Any?)
fun unit(handle: MethodHandle, vararg args: Any?) {
call { handle.invokeWithArguments(*args) }
}
Native lifetime
A Solid or Section frees its native object from a Cleaner once the JVM no longer reaches it. An
operation reads its operands' pointers and then calls native code, and after the read nothing keeps
a temporary operand alive: previewPolygon(p) and ring let the Cleaner free the left-hand section
while Manifold was intersecting it, and the house corpus host died in CrossSection::GetPaths
(a mutex in freed memory). Two things close that:
* reading ptr pins its owner to this thread until the next native call here returns, so an
operand cannot become unreachable between its pointer being read and the call using it;
* every native call holds a shared lock that a free takes exclusively, so nothing is freed while
any call is running. The lock is fair, so a steady stream of calls cannot starve the frees.
A Kotlin lambda that Manifold calls back (an SDF or a warp) runs inside a native call that already holds the shared lock, often on one of Manifold's own threads. Native calls it makes skip the lock, since nothing can be freed until the outer call returns, and frees it asks for wait for the next free outside a callback. Taking the lock there instead could deadlock against a queued free.
private val calls = java.util.concurrent.locks.ReentrantReadWriteLock(true)
private val pinned = ThreadLocal.withInitial { ArrayList<Any>() }
private val upcallDepth = ThreadLocal.withInitial { IntArray(1) }
private val deferred = java.util.concurrent.ConcurrentLinkedQueue<Pair<MethodHandle, MemorySegment>>()
fun pin(owner: Any)
Keep owner reachable until the next native call on this thread returns.
fun pin(owner: Any) {
pinned.get().add(owner)
}
private inline fun <T> inUpcall(body: () -> T): T {
val depth = upcallDepth.get()
depth[0]++
try {
return body()
} finally {
depth[0]--
}
}
private inline fun <T> call(body: () -> T): T {
if (upcallDepth.get()[0] > 0 || calls.readHoldCount > 0) {
try {
return body()
} finally {
pinned.get().clear()
}
}
val lock = calls.readLock()
lock.lock()
try {
return body()
} finally {
lock.unlock()
pinned.get().clear()
}
}
fun release(delete: MethodHandle, handle: MemorySegment)
Free a native object, never while a native call is running.
fun release(delete: MethodHandle, handle: MemorySegment) {
deferred.add(delete to handle)
if (upcallDepth.get()[0] > 0 || calls.readHoldCount > 0) return
val lock = calls.writeLock()
lock.lock()
try {
while (true) {
val (d, h) = deferred.poll() ?: break
d.invokeWithArguments(h)
}
} finally {
lock.unlock()
}
}
fun newManifold(): MemorySegment = ptr(allocManifold)
A fresh ManifoldManifold* from the library's own allocator, ready to be constructed into.
fun newSection(): MemorySegment = ptr(allocSection)
A fresh ManifoldCrossSection*.
fun vec3(segment: MemorySegment): Triple<Double, Double, Double>
fun vec3(segment: MemorySegment): Triple<Double, Double, Double> = Triple(
segment.get(JAVA_DOUBLE, 0),
segment.get(JAVA_DOUBLE, 8),
segment.get(JAVA_DOUBLE, 16),
)
fun vec2(segment: MemorySegment): Pair<Double, Double>
fun vec2(segment: MemorySegment): Pair<Double, Double> =
segment.get(JAVA_DOUBLE, 0) to segment.get(JAVA_DOUBLE, 8)
Upcalls
class SdfBridge(private val field: (Double, Double, Double) -> Double) {
The Kotlin side of a ManifoldSdf, and the reason it is a class rather than a lambda: FFM
needs a MethodHandle with the callback's exact signature, which means a real method with the
trailing void* ctx in it.
firstFailure is the exception channel. Manifold calls sample from its own thread pool, so a throw here would unwind through C++ frames — undefined behaviour, and FFM's response to an escaping exception is to take the VM down. Instead the first throwable is kept and a finite sentinel returned; the caller rethrows once the native call is over. The sentinel is deliberately not NaN: a NaN sample propagates into the marching-cubes interpolation and comes back as a corrupt mesh, whereas a large positive value simply reads as "far outside" and leaves the shape empty, which is the honest answer for a field that failed.
var firstFailure: Throwable?
@Volatile
var firstFailure: Throwable? = null
private set
fun sample(x: Double, y: Double, z: Double, @Suppress("UNUSED_PARAMETER") ctx: MemorySegment): Double
fun sample(x: Double, y: Double, z: Double, @Suppress("UNUSED_PARAMETER") ctx: MemorySegment): Double =
try {
inUpcall { field(x, y, z) }
} catch (t: Throwable) {
if (firstFailure == null) firstFailure = t
FAILED_SAMPLE
}
}
class WarpBridge(private val map: (Double, Double, Double) -> Triple<Double, Double, Double>) {
The Kotlin side of manifold_warp's callback. Returns a struct, so sample hands back a
24-byte MemorySegment whose contents FFM copies out on return.
The segment is a per-thread scratch buffer rather than a fresh allocation, because this is
called once per vertex from Manifold's thread pool and an Arena per vertex would dominate
the cost of the warp itself. Reuse is safe precisely because the copy-out happens before this
thread can call sample again.
var firstFailure: Throwable?
@Volatile
var firstFailure: Throwable? = null
private set
private val scratch = ThreadLocal.withInitial { Arena.ofAuto().allocate(VEC3) }
fun sample(x: Double, y: Double, z: Double, @Suppress("UNUSED_PARAMETER") ctx: MemorySegment): MemorySegment
body — 46 lines, from line 702
fun sample(x: Double, y: Double, z: Double, @Suppress("UNUSED_PARAMETER") ctx: MemorySegment): MemorySegment {
val out = scratch.get()
val moved = try {
inUpcall { map(x, y, z) }
} catch (t: Throwable) {
if (firstFailure == null) firstFailure = t
Triple(x, y, z) // leave the vertex where it was rather than fold the mesh onto itself
}
out.set(JAVA_DOUBLE, 0, moved.first)
out.set(JAVA_DOUBLE, 8, moved.second)
out.set(JAVA_DOUBLE, 16, moved.third)
return out
}
}
private val SDF_DESCRIPTOR = FunctionDescriptor.of(
JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, ADDRESS,
)
private val WARP_DESCRIPTOR = FunctionDescriptor.of(
VEC3, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, ADDRESS,
)
private val SDF_TARGET: MethodHandle = MethodHandles.lookup().findVirtual(
SdfBridge::class.java,
"sample",
MethodType.methodType(
Double::class.javaPrimitiveType,
Double::class.javaPrimitiveType,
Double::class.javaPrimitiveType,
Double::class.javaPrimitiveType,
MemorySegment::class.java,
),
)
private val WARP_TARGET: MethodHandle = MethodHandles.lookup().findVirtual(
WarpBridge::class.java,
"sample",
MethodType.methodType(
MemorySegment::class.java,
Double::class.javaPrimitiveType,
Double::class.javaPrimitiveType,
Double::class.javaPrimitiveType,
MemorySegment::class.java,
),
)
fun upcallSdf(bridge: SdfBridge, arena: Arena): MemorySegment
A ManifoldSdf function pointer valid for the life of arena.
fun upcallSdf(bridge: SdfBridge, arena: Arena): MemorySegment =
linker.upcallStub(SDF_TARGET.bindTo(bridge), SDF_DESCRIPTOR, arena)
fun upcallWarp(bridge: WarpBridge, arena: Arena): MemorySegment
A manifold_warp function pointer valid for the life of arena.
fun upcallWarp(bridge: WarpBridge, arena: Arena): MemorySegment =
linker.upcallStub(WARP_TARGET.bindTo(bridge), WARP_DESCRIPTOR, arena)
const val FAILED_SAMPLE: Double = 1e30
What an SDF that threw reports instead. See SdfBridge.
object ManifoldLibrary
Where libmanifoldc is, and what to do when it is not there.
Manifold is an optional dependency, exactly like the Ceres wrapper: a clone of Field builds,
tests and runs without it, and only the boxes that reach for Solid need it. So nothing in the
build downloads or vendors it, and every entry point that needs it says so in its error message.
Resolution order, first hit wins:
-Dafield.manifold.library=<path>(a JVM system property)AFIELD_MANIFOLD_LIBRARY=<path>(an environment variable, which is also what the docs build reads, since it cannot see the property)- the platform's usual places — Homebrew on both macOS architectures,
/usr/local, the two common Linux multiarch directories - the plain library name, which lets the dynamic loader search
DYLD_/LD_LIBRARY_PATH
object ManifoldLibrary {
private val isWindows: Boolean
get() = System.getProperty("os.name").lowercase().contains("win")
private val isMac: Boolean
get() = System.getProperty("os.name").lowercase().let { it.contains("mac") || it.contains("darwin") }
val fileName: String
libmanifoldc.dylib / .so / manifoldc.dll, chosen for the platform actually running.
val fileName: String
get() = when {
isWindows -> "manifoldc.dll"
isMac -> "libmanifoldc.dylib"
else -> "libmanifoldc.so"
}
val searchedPaths: List<Path>
Every path resolvedPath tries, in order — quoted in the "not installed" error.
val searchedPaths: List<Path>
get() {
val explicit = listOfNotNull(
System.getProperty("afield.manifold.library")?.takeIf { it.isNotBlank() },
System.getenv("AFIELD_MANIFOLD_LIBRARY")?.takeIf { it.isNotBlank() },
).map(Path::of)
val conventional = when {
isWindows -> listOf("C:\\Program Files\\manifold\\bin")
isMac -> listOf("/opt/homebrew/lib", "/usr/local/lib")
else -> listOf("/usr/local/lib", "/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu")
}.map { Path.of(it, fileName) }
return explicit + conventional
}
val resolvedPath: Path?
The first path that exists, or null.
Not cached: brew install manifold during a live session should be picked up by the next box
run rather than needing a restart, and the cost is a handful of stat calls on a path that is
only consulted once per VM by ManifoldNative.lookup anyway.
val resolvedPath: Path?
get() = searchedPaths.firstOrNull { it.exists() }
val isAvailable: Boolean
true when Solid and Section will work. Cheap enough to branch on in a box.
val isAvailable: Boolean
get() = resolvedPath != null
val reportedVersion: String?
The upstream version, read off the .dylib/.so filename that the SO-versioned symlink
points at — libmanifoldc.3.5.2.dylib reports 3.5.2.
There is no manifold_version() in the C API to ask instead, and reading it this way is
honest about being a guess: null means the file was not laid out the way a released one is,
not that the library is broken.
val reportedVersion: String?
get() {
val path = resolvedPath ?: return null
val real = runCatching { path.toRealPath() }.getOrNull() ?: return null
return Regex("""(\d+\.\d+\.\d+)""").find(real.fileName.toString())?.groupValues?.get(1)
}
val HOW_TO_INSTALL: String
What to tell someone who has not got it, in the package manager they are likely to have.
val HOW_TO_INSTALL: String
get() = when {
isMac -> "Install it with `brew install manifold`, or point -Dafield.manifold.library at a libmanifoldc you built."
isWindows -> "Build Manifold with -DMANIFOLD_CBIND=ON and point -Dafield.manifold.library at manifoldc.dll."
else -> "Install it with your package manager (`apt install libmanifold-dev`, `pacman -S manifold`) " +
"or build Manifold with -DMANIFOLD_CBIND=ON, then point -Dafield.manifold.library at libmanifoldc.so."
}
fun describe(): String
One line for a box or a smoke test to print: what was found, or why not.
fun describe(): String = resolvedPath?.let {
"Manifold ${reportedVersion ?: "(version unknown)"} at $it"
} ?: "Manifold not installed. $HOW_TO_INSTALL"
}
enum class ManifoldStatus(val code: Int)
ManifoldError, the status a Solid carries when an operation could not produce a valid solid.
Manifold does not throw and does not return null: a failed operation gives you a solid whose
Solid.status is not NO_ERROR, and whose volume is zero. That is a deliberate upstream choice —
it keeps a long boolean chain from needing a check per step — and this enum is where Field turns it
back into something a box can read. Solid.checked is the one-line version.
enum class ManifoldStatus(val code: Int) {
NO_ERROR(0),
NON_FINITE_VERTEX(1),
NOT_MANIFOLD(2),
VERTEX_INDEX_OUT_OF_BOUNDS(3),
PROPERTIES_WRONG_LENGTH(4),
MISSING_POSITION_PROPERTIES(5),
MERGE_VECTORS_DIFFERENT_LENGTHS(6),
MERGE_INDEX_OUT_OF_BOUNDS(7),
TRANSFORM_WRONG_LENGTH(8),
RUN_INDEX_WRONG_LENGTH(9),
FACE_ID_WRONG_LENGTH(10),
INVALID_CONSTRUCTION(11),
RESULT_TOO_LARGE(12),
INVALID_TANGENTS(13),
CANCELLED(14),
;
val isError: Boolean get() = this != NO_ERROR
true for everything except NO_ERROR.
companion object
companion object {
fun of(code: Int): ManifoldStatus
The enum for a raw ManifoldError, or INVALID_CONSTRUCTION for a code from a newer Manifold.
fun of(code: Int): ManifoldStatus = entries.firstOrNull { it.code == code } ?: INVALID_CONSTRUCTION
}
}
enum class BooleanOp(internal val code: Int) { UNION(0), DIFFERENCE(1), INTERSECTION(2) }
ManifoldOpType, the operation Solid.batchBoolean and Section.batchBoolean apply.
enum class JoinType(internal val code: Int) { SQUARE(0), ROUND(1), MITER(2), BEVEL(3) }
ManifoldJoinType — how Section.offset treats a convex corner.
ROUND is the default because it is the one that makes an offset read as a fillet; MITER
keeps corners sharp up to miterLimit and then bevels them, SQUARE and BEVEL cut them off
immediately.
enum class FillRule(internal val code: Int)
ManifoldFillRule — how a set of contours decides what is inside, when they overlap or nest.
POSITIVE is Field's default rather than Clipper's EVEN_ODD, because it is the rule that makes
winding mean something: a hole is a contour wound the other way, so a ring is two contours and
two overlapping same-wound outlines are one shape rather than a shape with an accidental hole.
enum class FillRule(internal val code: Int) { EVEN_ODD(0), NON_ZERO(1), POSITIVE(2), NEGATIVE(3) }
data class RayHit
One intersection from Solid.raycast.
data class RayHit(
/** Which triangle of the solid was hit, as Manifold's face id. */
val faceId: Long,
/** Distance from the ray origin, in the units the solid is in. */
val distance: Double,
val x: Double,
val y: Double,
val z: Double,
val normalX: Double,
val normalY: Double,
val normalZ: Double,
)
internal fun MemorySegment.floatAt(index: Long): Float = getAtIndex(JAVA_FLOAT, index)
Internal float/int accessors kept beside the layouts they read.