Skip to content

Dzint/resize connectivity vectors - #1056

Merged
daniel-zint merged 6 commits into
mainfrom
dzint/resize_connectivity_vectors
Aug 24, 2026
Merged

Dzint/resize connectivity vectors#1056
daniel-zint merged 6 commits into
mainfrom
dzint/resize_connectivity_vectors

Conversation

@daniel-zint

Copy link
Copy Markdown
Contributor

No description provided.

daniel-zint and others added 6 commits August 21, 2026 10:28
TriMesh and TetMesh each carried two copies of the same bump allocator --
request_tri_slots/request_tet_slots and their respective request_vert_slots.
After the hash alignment in 340d429 those four functions differed only in
which atomic counter and which storage vector they named: same CAS loop,
same refuse-rather-than-resize, same reset of the slots handed out.

SlotPool<T> holds the storage and its counter together. The invariant that
matters is live() <= capacity(), and it can only be maintained where both
are in reach -- as separate members there was nothing to stop a counter
being paired with the wrong vector. It is also the single place a future
growth strategy has to be implemented, rather than four.

The two numbers are now named apart. tri_capacity()/tet_capacity() return
the LIVE count rather than the storage size, which is what made the spare
region hard to reason about; SlotPool calls them live() and capacity().

The entry points behave identically now. request_*_slots returns size_t and
reports refusal as INVALID_SLOT instead of -1, and each get_next_empty_slot_*
is just request_*_slots(1). TetMesh's wrappers previously returned int, so
callers there tested `< 0` while TriMesh's tested `== size_t(-1)`; unifying
them also drops an (int) narrowing that silently capped tet indices at 2^31.

The capacity check is phrased as `n > cap - first` rather than
`first + n > cap`, which cannot wrap now that the arithmetic is unsigned.

Testing that rewrite exposed a gap. An off-by-one at the capacity boundary
was NOT caught by the existing tests: every assertion was framed against
cell_capacity(), the live count, so letting the live count run one slot past
the storage stayed internally consistent. live() <= capacity() simply was not
observable through the meshes. SlotPool being standalone makes it directly
testable, and the added test case fails in three places under that mutation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A pass that exhausted the preallocated storage abandoned the affected
operations silently. Nothing detected it, and the capacity was only
re-derived at the end of the outer iteration -- from the same
m_preallocation_factor that had just proved insufficient, so a mesh that
needed more room kept losing the same work every iteration.

SlotPool now records that a request was refused. A flag rather than a
count: the response is to consolidate, which re-derives the capacity from
the real element count, so the size of the shortfall is not an input.

local_operations checks it at each op-group boundary and consolidates.
consolidate_mesh already does both halves of the job:

  - it reclaims the slots removed elements still hold. The counter only
    advances during a pass, so live() is an allocation high-water mark and
    churn alone can exhaust the headroom without the mesh growing at all.
  - it resizes to reserved_capacity(real count), which when there is no
    churn to reclaim grows the storage by m_preallocation_factor.

The group is then retried in place, with the retry depth counted and
logged. Consolidate renumbers, so the failed operations' tuples are gone;
re-running the group re-collects them against the enlarged storage.

Also adds tri/tet/vert_storage_capacity(). *_capacity() reports the LIVE
count, so the storage size was not observable from outside the mesh --
which is what made this behavior undiagnosable, and what made an earlier
reading of it wrong.

simwild_spec.json gains /preallocation_factor. set_preallocation_factor_from_json
already read it, but the spec rejected it, so the knob was dead for simwild.
(It is still dead for the other components that read it without declaring it:
topological_offset, isotropic_remeshing, qslim, manifold_extraction,
shortest_edge_collapse.)

Measured on 441722.stl at preallocation_factor 1.05, where exhaustion is
guaranteed:

    split pass exhausted: 113630 of 171845 tets reclaimed as churn,
                          storage 173887 -> 61126
    split pass exhausted:      0 of  60997 tets reclaimed as churn,
                          storage  61126 -> 64047
    split pass exhausted:      0 of  63923 tets reclaimed as churn,
                          storage  64047 -> 67120

The first fire is pure churn -- two thirds of the allocation reclaimed, and
the storage shrinks because the real mesh is far smaller than the high-water
mark. Later fires reclaim nothing and grow by the factor instead.

15 fires, max retry depth 4, converged in 21.3s. The same configuration ran
past 10 minutes without finishing before the retry existed. At the default
factor 6.0, nothing fires, and behavior is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@daniel-zint
daniel-zint merged commit 8c063bd into main Aug 24, 2026
10 checks passed
@daniel-zint
daniel-zint deleted the dzint/resize_connectivity_vectors branch August 24, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant