fix(grid): ~UIGridView unregisters from the GridData view registry

GridData::views (the N-view registry that replaced owning_view in #359) was only
ever pruned by UIGridView::set_grid and by tp_dealloc -- and tp_dealloc's
unregister is gated on data.use_count() <= 1 (the #251 pattern). When the C++
view outlives its Python wrapper, which is the normal case because the scene
holds the last shared_ptr, that guard skips the unregister and the view's entry
survives its own destruction as an expired weak_ptr.

Entries therefore accumulated for the lifetime of a GridData, and markDirty() /
markCompositeDirty() lock() every one of them on every entity move. Not a
correctness bug (expired weak_ptrs lock to null and are skipped) but an unbounded
leak on a hot path.

~UIGridView is the only place guaranteed to run exactly once per view, so the
unregister belongs there. unregisterView already prunes expired entries, so no
other change is needed.

NO REGRESSION TEST: GridData::views has no Python observable, so the registry's
size cannot be asserted from a test script. Adding one (e.g. a read-only
GridData.view_count) is deliberately deferred to #361, which makes GridData a
public type and reworks this area; the fix is landed now because the leak is on
a hot path and the one-liner is unambiguous. Full suite green (324/324).

closes #362
This commit is contained in:
John McCardle 2026-07-12 20:27:28 -04:00
commit 461dcf7f29
2 changed files with 11 additions and 1 deletions

File diff suppressed because one or more lines are too long

View file

@ -32,6 +32,16 @@ UIGridView::UIGridView()
}
UIGridView::~UIGridView() {
// #362: drop ourselves from the GridData's view registry. This is the only
// place guaranteed to run exactly once per view: tp_dealloc's unregister is
// gated on data.use_count() <= 1 (the #251 pattern), so a view that outlives
// its Python wrapper -- the normal case, when the scene holds the last ref --
// would otherwise leave an expired weak_ptr in `views` forever. markDirty()
// walks that vector on every entity move.
if (grid_data) {
grid_data->unregisterView(this);
}
// #348: release the persistent internal-Grid wrapper. Guard with
// Py_IsInitialized() because this destructor may run during interpreter
// shutdown (mirrors Animation::~Animation).