Affects: Rhino 8.34 (8.34.26223.11002), macOS 26.5.2 (Apple Silicon) — reproduced identically on Rhino 8 for Windows
Severity: unmanaged memory leak, gigabytes per session in plugins that call mesh booleans repeatedly
Summary
Every call to Rhino.Geometry.Mesh.CreateBooleanIntersection (both the legacy overload and the MeshBooleanOptions overload — they appear to route into the same native entry point,
RHC_RhinoMeshBooleanIntDiffUni) leaks unmanaged memory inside the new MX mesh boolean engine.
The .NET managed heap stays flat; the leaked memory is native and is not reclaimed by
GC.Collect() + GC.WaitForPendingFinalizers(), nor when all managed result meshes are disposed.
In our Grasshopper plugin (which ran one small mesh boolean per candidate connection between
structural elements), this leaked ~750 MB per recompute, reaching 9+ GB of process memory in a
few minutes of normal use. Managed memory (dotMemory snapshots) stayed at ~150 MB throughout.
Evidence: macOS leaks with malloc stack logging
Launching Rhino with MallocStackLogging=lite and running leaks after ~8 recomputes:
Process 38766: 6877371 nodes malloced for 13001373 KB
Process 38766: 2207151 leaks for 11303250656 total leaked bytes.
11.3 GB across 2.2 million leaked blocks. The leaked object types are the engine’s internals:
ON_Mesh, ON_SimpleArray<bool>, ON_SimpleArray<unsigned int>, plus raw onmalloc blocks
(reported both as ROOT LEAK and ROOT CYCLE groups). Every top leak group shares this allocation stack (managed JIT frames elided):
...(managed frames)...
com.mcneel.rhinoceros.8 RHC_RhinoMeshBooleanIntDiffUni + 208
com.mcneel.rhinoceros.RhCore RhinoMeshBooleanIntersection(ON_SimpleArray<ON_Mesh const*> const&, ON_SimpleArray<ON_Mesh const*> c...
com.mcneel.opennurbs MX_MeshMeshBooleanIntersection(ON_SimpleArray<ON_Mesh const*> const&, ON_SimpleArray<ON_Mesh const*>...
com.mcneel.opennurbs _MX_MeshMeshBoolean(ON_SimpleArray<ON_Mesh const*> const&, double, double, bool*, ON_SimpleArray<ON_...
com.mcneel.opennurbs MX::PublicIntersectionOps::ComputeMeshIntersections(MX::TessellationArray const&, MX::TessellationAr...
com.mcneel.opennurbs onmalloc + 44
libsystem_malloc.dylib _malloc_zone_malloc_instrumented_or_legacy + 152
(Some groups end at _MX_MeshMeshBoolean without the ComputeMeshIntersections frame.)
Full leaks output available on request (157 MB; excerpt attached).
Minimal repro (Grasshopper C# script or any RhinoCommon context)
Run this and watch process memory (Task Manager / Activity Monitor). Managed memory stays flat;
process memory grows by hundreds of MB and never returns, including after forced GC:
var boxA = Mesh.CreateFromBox(new BoundingBox(new Point3d(0, 0, 0), new Point3d(1, 1, 1)), 2, 2, 2);
var boxB = Mesh.CreateFromBox(new BoundingBox(new Point3d(0.5, 0.5, 0.5), new Point3d(1.5, 1.5, 1.5)), 2, 2, 2);
for(int i = 0; i < 20000; i++)
{
var result = Mesh.CreateBooleanIntersection(new[] { boxA }, new[] { boxB });
if(result != null)
foreach(var m in result)
m.Dispose(); // disposing results makes no difference
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
// managed heap: flat. process memory: grew by GBs and stays there.
Notes from our testing:
- Occurs with both
CreateBooleanIntersection(IEnumerable<Mesh>, IEnumerable<Mesh>)and theMeshBooleanOptionsoverload — switching between them changed nothing, consistent with both routing intoRHC_RhinoMeshBooleanIntDiffUni. - Our production calls included open (planar patch) meshes against closed boxes as well as
closed-vs-closed boxes; leak stacks were recorded for both. - Given the shared native entry name (
...IntDiffUni),CreateBooleanDifference/Unionare likely affected as well (not separately profiled). - Reproduced on macOS (Apple Silicon, 8.34) and Windows (Rhino 8). Cross-platform, so the leak is in the shared engine code, not platform glue.
Impact / workaround
We removed mesh booleans from our hot paths entirely (replaced with analytic clipping), but our
remaining legitimate uses (terrain/excavation booleans on large meshes) still leak per call, and
any plugin or scripted workflow calling mesh booleans in a loop will exhaust memory. It seems there is no SDK-level way to reach the pre-MX engine (the +old fallback exists only as UI commands). What do?
Happy to provide the full leaks report.
Cheers
Ben