Fix callback/timer GC: prevent premature destruction of Python callbacks

closes #251

Two related bugs where Python garbage collection destroyed callbacks
that were still needed by live C++ objects:

1. **Drawable callbacks (all 8 types)**: tp_dealloc unconditionally called
   click_unregister() etc., destroying callbacks even when the C++ object
   was still alive in a parent's children vector. Fixed by guarding with
   shared_ptr::use_count() <= 1 — only unregister when the Python wrapper
   is the last owner.

2. **Timer GC prevention**: Active timers now hold a Py_INCREF'd reference
   to their Python wrapper (Timer::py_wrapper), preventing GC while the
   timer is registered in the engine. Released on stop(), one-shot fire,
   or destruction. mcrfpy.Timer("name", cb, 100) now works without storing
   the return value.

Also includes audio synth demo UI fixes: button click handling (don't set
on_click on Caption children), single-column slider layout, improved
Animalese contrast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-02-19 20:53:50 -05:00
commit 9718153709
15 changed files with 740 additions and 231 deletions

View file

@ -101,6 +101,8 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
it->second->stop();
}
Resources::game->timers[self->name] = self->data;
// Prevent Python GC while timer is active (#251)
self->data->retainPyWrapper((PyObject*)self);
}
return 0;
@ -147,6 +149,8 @@ PyObject* PyTimer::start(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
}
self->data->start(current_time);
// Prevent Python GC while timer is active (#251)
self->data->retainPyWrapper((PyObject*)self);
Py_RETURN_NONE;
}
@ -218,6 +222,8 @@ PyObject* PyTimer::restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
}
self->data->restart(current_time);
// Prevent Python GC while timer is active (#251)
self->data->retainPyWrapper((PyObject*)self);
Py_RETURN_NONE;
}
@ -316,6 +322,8 @@ int PyTimer::set_active(PyTimerObject* self, PyObject* value, void* closure) {
Resources::game->timers[self->name] = self->data;
}
self->data->start(current_time);
// Prevent Python GC while timer is active (#251)
self->data->retainPyWrapper((PyObject*)self);
} else if (self->data->isPaused()) {
// Resume from pause
self->data->resume(current_time);