limits and robustness
Where it stops working, and how it tells you
A geometry library that claims to handle anything is lying about something. This page is the list of the places where RidgeForge gives up, degrades, or produces an answer that is correct but not the one you expected.
Hard limits
| Quantity | Limit | Beyond it |
|---|---|---|
| Vertices per mesh | 2³¹ − 1 | validate() returns OutOfRange; the index type is 32-bit and the top bit is reserved as a marker |
| Triangles per mesh | 2³¹ − 1 | same, and you would need 25 GB just for the arrays |
| Coordinate magnitude | 10¹⁸ | quadric entries square the coordinates and overflow to infinity; the collapse order becomes arbitrary |
| Recommended magnitude | 10⁴ | above this, single precision loses the resolution that the error ceiling assumes |
Boundary loop for fill_holes | ~5 000 edges | no refusal, but ear clipping is quadratic and a 20 000-edge loop takes minutes |
| Components | unbounded | nothing special; a million one-triangle components works and is slow for other reasons |
| Attribute sets | 1 UV, 1 normal | additional sets are dropped on read without a warning |
Move the mesh so its bounding box is centred on the origin and scale it into a unit cube, decimate, then undo both. A terrain tile stored in projected metres with a 6-digit easting spends most of its float mantissa on an offset nobody cares about, and the error ceiling you pass then means something different on each side of the model. Two lines of code buy a great deal of predictability.
Degenerate input
Nothing here throws, nothing here silently corrupts. Each case has a defined behaviour:
| Input | What happens |
|---|---|
| NaN or infinite coordinate | validate() reports it. Every other operation treats it as data: a NaN propagates into the quadric and that vertex stops being collapsible, which usually looks like a frozen patch. |
| Zero-area triangle | Its plane is undefined, so it contributes nothing to any quadric. It is never collapsed away by itself; Repair::remove_degenerate is the tool for it. |
| Triangle with a repeated index | Same as zero area, and it also breaks the link condition for its edges, which blocks nearby collapses. Remove it first. |
| Duplicate triangle, same winding | Doubles the quadric weight of its region, which biases decimation towards keeping it. Harmless otherwise. |
| Duplicate triangle, opposite winding | Makes the mesh non-orientable at those edges. orient_consistently fails with NonManifold; detection of intersections reports the pair. |
| Edge with three or more faces | Decimation refuses collapses there and continues elsewhere. Stats::non_manifold_edges counts them. |
| Vertex joining two cones | Counted in non_manifold_vertices. Collapsing across it is blocked by the link condition, so it survives decimation intact. |
| Unreferenced vertex | Kept. No operation deletes vertices that no triangle uses; Repair::weld_vertices will compact them away only if another vertex is within tolerance. |
| Empty mesh | Operations return EmptyMesh rather than succeeding vacuously. |
Precision
The default scalar is float. That is enough for the geometry and not quite enough for the quadrics, which is why the quadric accumulation runs in double regardless of the scalar setting, and only the resulting optimal position is rounded back. Without that, a mesh with a million triangles accumulates visible drift in the flat regions.
Building with RF_DOUBLE_PRECISION=ON changes positions, not predicates: the intersection tests were already exact and do not improve. What improves is the stability of the Cholesky solve on nearly singular quadrics, which matters for meshes with very long thin triangles. The cost is 1.9× the memory for positions and roughly 15 % on decimation time.
Decimate the same mesh twice, once at the original scale and once scaled by 1000 with the error ceiling scaled by 10⁶, then compare triangle counts. If they differ by more than a fraction of a percent, your model sits in a numerically awkward range and normalising will pay for itself.
Memory
Everything is a flat vector, so the arithmetic is easy to do in advance. Single precision, 64-bit platform:
| Structure | Bytes | Per | Present during |
|---|---|---|---|
positions | 12 | vertex | always |
triangles | 12 | triangle | always |
normals, uv | 12, 8 | vertex | only if populated |
| Vertex quadrics | 40 | vertex | decimation |
| Triangle adjacency | 12 | triangle | decimation, remeshing |
| Collapse heap entry | 28 | candidate edge | decimation |
| Hierarchy node | 32 | node, ≈ triangles ÷ 4 | intersection, projection |
| Hierarchy index | 4 | triangle | intersection, projection |
A two-million-triangle mesh with a million vertices therefore needs about 36 MB resident, and decimating it peaks near 190 MB, dominated by the three million heap entries. Searching the same mesh for self-intersections peaks near 72 MB, because the hierarchy is cheaper than the heap. The measured figures are in the benchmark tables and they match this arithmetic to within a few percent.
What is not promised
- No distance bound.
max_errorbounds a quadric value, not a deviation in model units. Nothing in the library computes a Hausdorff distance. - No watertight guarantee after resolve.
SelfIntersection::resolveremoves triangles when it cannot separate them, and removing a triangle opens a hole it then tries to close. On hard cases it stops with a report instead of a promise. - No UV seam preservation. Decimation interpolates UV at the merged vertex. A vertex that sits on a texture seam has two legitimate values and gets one of them. Lock seam vertices if this matters.
- No volume preservation. Neither decimation nor remeshing conserves enclosed volume, and remeshing with projection off will shrink a closed surface by roughly 0.1 % per iteration.
- No cross-version determinism. Same version, same answer; new minor version, possibly different collapse order.
- No thread safety on a single mesh. Two threads may run on two meshes; two threads on one mesh is undefined, including one reader and one writer.
Known issues
Open issues carry an identifier that is stable across releases, so you can refer to one in a message and get a useful reply.
| Id | Issue | State | Since |
|---|---|---|---|
RF-41 | resolve makes no progress on two shells nested one inside the other and exhausts its passes | open | 0.9.2 |
RF-57 | fill_holes may produce a self-intersecting patch when a loop is far from planar and the centroid fan is used | open | 0.6.0 |
RF-63 | the OBJ reader rejects negative, relative face indices instead of resolving them | open | 0.5.0 |
RF-68 | welding with a tolerance larger than the smallest feature merges a UV seam into one vertex; by design, listed because it surprises people | as designed | 0.6.0 |
RF-72 | the remesher dropped components of two triangles or fewer | fixed in 0.9.4 | 0.9.0 |
RF-74 | PLY files that store coordinates as double are reported as UnsupportedFormat rather than being narrowed | open | 0.5.0 |
Reporting something new
Send it to dev@ridgeforge.dev. What makes a report actionable, in order of usefulness:
- The smallest mesh that still shows it. Four triangles beats four million, and
rfmesh checkoften finds the offending region for you. - The exact option values, ideally as the
rfmeshcommand line that reproduces it. - Version, compiler and whether the build has
RF_DOUBLE_PRECISIONor sanitizers on. - What you expected, stated as a property rather than as an appearance.
Reports that arrive with a four-triangle reproducer usually get an answer in a day. Reports that say decimation looks wrong on a model that cannot be shared usually do not get a useful answer at all, through no fault of either side.