[Bugfix] UIEntity self reference cycle causes permanent memory leak #266
Labels
No labels
Alpha Release Requirement
Bugfix
Demo Target
Documentation
Major Feature
Minor Feature
priority:tier1-active
priority:tier2-foundation
priority:tier3-future
priority:tier4-deferred
Refactoring & Cleanup
system:animation
system:documentation
system:grid
system:input
system:performance
system:procgen
system:python-binding
system:rendering
system:ui-hierarchy
Tiny Feature
workflow:blocked
workflow:needs-benchmark
workflow:needs-documentation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
john/McRogueFace#266
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Every
UIEntitycreated from Python leaks its Python wrapper object permanently.UIEntity::init()creates a reference cycle between the Python wrapper and the C++ object that is never broken, preventing garbage collection.Root Cause
UIEntity.cpp:248-249:This creates an uncollectable cycle:
PyUIEntityObjectholdsstd::shared_ptr<UIEntity> data→ prevents C++ object destructionUIEntity::selfholdsPyObject*withPy_INCREF→ prevents Python object destructionNeither can be freed because each prevents the other from reaching refcount 0. The comment says "legacy - to be removed" — it should be removed.
Impact
Every entity created leaks ~200+ bytes permanently. In a game that creates/destroys many entities (e.g., spawning enemies, projectiles, zone transitions), this is a continuous memory leak.
Reproduction
Fix
The
PythonObjectCache(weak reference cache) already provides the same functionality thatselfwas meant to provide (preserving derived class identity). The fix is:self->data->self = (PyObject*)self; Py_INCREF(self);fromUIEntity::init()selffield fromUIEntityclass entirelyentity->selfto usePythonObjectCache::lookup()insteadUIEntityCollectionIter::next()(line 45) which checkstarget->self != nullptrSeverity
High — every entity permanently leaks. Not a crash, but causes unbounded memory growth in long-running games.
Reopened: Subclass identity regression (commit
836a058)The Phase 3 fix (
a12e035) that removedUIEntity::selfto fix the permanent memory leak also broke Python subclass identity preservation. When the Python wrapper for a subclass likeGameEntity(mcrfpy.Entity)was the only reference and went out of scope, the wrapper was GC'd. Later access viagrid.entitiesreturned a new baseEntitywrapper, losing all custom methods (send, tooltip, bump, etc.).This caused regressions in Liber Noster (7DRL 2026) where entities lost their derived behavior after GC cycles.
Fix: Added
UIEntity::pyobject— a strong reference to the Python wrapper that keeps the subclass alive while the entity is in a grid. Cleared at all grid exit points viareleasePyIdentity():die()— entity removed from gridset_grid(None)— entity detached from griddel,remove(),pop(),setitem, slice deletion/replacementThe key difference from the old
selffield: the strong reference is released when the entity leaves the grid, breaking the cycle and allowing proper GC. The old code never released it.Regression test:
tests/regression/issue_266_subclass_identity_test.py(29 tests exercising Liber Noster patterns — subclass hierarchy, isinstance checks, combat mixins, GC survival, die/pop/remove).step()callback — Python-side turn handler #299Status update
The
selfreference cycle has been fixed forUIEntity— replaced with thepyobjectpattern (strong ref only while in grid, released at all exit points).The old
self->data->selfpattern remains only insrc/3d/Entity3D.cpp:725. Once that's fixed, this issue can be closed.Part of the Grid & Entity Overhaul Roadmap (
docs/GRID_ENTITY_OVERHAUL_ROADMAP.md), Phase 1 (Foundation).step()callback — Python-side turn handler #299