From 4a82ea55c2b4954e1963f3b887373878625db407 Mon Sep 17 00:00:00 2001 From: SuperRedingBros Date: Tue, 4 Nov 2025 18:47:36 -0500 Subject: [PATCH 1/8] add Event Watchers and Filters --- buildconfig/stubs/pygame/event.pyi | 6 +- docs/reST/ref/event.rst | 66 +++++++++ src_c/doc/event_doc.h | 4 + src_c/event.c | 211 ++++++++++++++++++++++++++++- test/event_test.py | 103 ++++++++++++++ 5 files changed, 388 insertions(+), 2 deletions(-) diff --git a/buildconfig/stubs/pygame/event.pyi b/buildconfig/stubs/pygame/event.pyi index 4c0674731b..1c3fb99211 100644 --- a/buildconfig/stubs/pygame/event.pyi +++ b/buildconfig/stubs/pygame/event.pyi @@ -1,4 +1,4 @@ -from typing import Any, ClassVar, Optional, Union, final +from typing import Any, Callable, ClassVar, Optional, Union, final from pygame.typing import SequenceLike from typing_extensions import deprecated # added in 3.13 @@ -53,3 +53,7 @@ def set_grab(grab: bool, /) -> None: ... def get_grab() -> bool: ... def post(event: Event, /) -> bool: ... def custom_type() -> int: ... +def add_event_watcher[T: Callable[[Event], Any]](watcher: T, /) -> T: ... +def remove_event_watcher[T: Callable[[Event], Any]](watcher: T, /) -> None: ... +def add_event_filter[T: Callable[[Event], Any]](filter: T, /) -> T: ... +def remove_event_filter[T: Callable[[Event], Any]](filter: T, /) -> None: ... diff --git a/docs/reST/ref/event.rst b/docs/reST/ref/event.rst index 9364dff647..008e227a1a 100644 --- a/docs/reST/ref/event.rst +++ b/docs/reST/ref/event.rst @@ -468,6 +468,72 @@ On Android, the following events can be generated .. ## pygame.event.custom_type ## +.. function:: add_event_watcher + + | :sl:`add an event watcher` + | :sg:`add_event_watcher[T: Callable[[Event], Any]](T) -> T` + + Adds an event watcher + The provided function should take a ``Event`` and may return any value. + + For convenience this function returns its parameter. + + WARNING: The passed function may be called in a seperate thread. + The watcher is called when events are added to the queue. + The queue processing may occur immediately or when ``pygame.event.poll`` / ``pygame.event.get`` is called. + This function is not called if the event is blocked or filtered out. + + If an error is generated in this function it is printed to stderr and otherwise ignored. + + .. versionaddedold:: todo + + .. ## pygame.event.add_event_watcher ## + +.. function:: remove_event_watcher + + | :sl:`remove an event watcher` + | :sg:`remove_event_watcher[T: (Event) -> Any](T) -> None` + + Removes a already existing event watcher. + + The object passed to this function should be the same as was passed to ``add_event_watcher``. + + .. versionaddedold:: todo + + .. ## pygame.event.remove_event_watcher ## + +.. function:: add_event_filter + + | :sl:`add an event filter` + | :sg:`add_event_filter[T: (Event) -> Any](T) -> T` + + Adds an event filter + The provided function should take a ``Event`` and should return a truthy value to skip the event. + + For convenience this function returns its parameter. + + WARNING: The passed function may be called in a seperate thread. + The filter is called before events are added to the queue. + This function is not called if the event is blocked. + + If an error is generated in this function it is printed to stderr and otherwise ignored. + + .. versionaddedold:: todo + + .. ## pygame.event.add_event_filter ## + +.. function:: remove_event_filter + + | :sl:`remove an event filter` + | :sg:`remove_event_filter[T: (Event) -> Any](T) -> None` + + Removes an already existing event filter. + The object passed to this function should be the same as was passed to ``add_event_filter``. + + .. versionaddedold:: todo + + .. ## pygame.event.remove_event_filter ## + .. class:: Event | :sl:`pygame object for representing events` diff --git a/src_c/doc/event_doc.h b/src_c/doc/event_doc.h index 16b6f1b0a1..800a93d767 100644 --- a/src_c/doc/event_doc.h +++ b/src_c/doc/event_doc.h @@ -14,6 +14,10 @@ #define DOC_EVENT_GETGRAB "get_grab() -> bool\ntest if the program is sharing input devices" #define DOC_EVENT_POST "post(event, /) -> bool\nplace a new event on the queue" #define DOC_EVENT_CUSTOMTYPE "custom_type() -> int\nmake custom user event type" +#define DOC_EVENT_ADDEVENTWATCHER "add_event_watcher[T: Callable[[Event], Any]](T) -> T\nadd an event watcher" +#define DOC_EVENT_REMOVEEVENTWATCHER "remove_event_watcher[T: (Event) -> Any](T) -> None\nremove an event watcher" +#define DOC_EVENT_ADDEVENTFILTER "add_event_filter[T: (Event) -> Any](T) -> T\nadd an event filter" +#define DOC_EVENT_REMOVEEVENTFILTER "remove_event_filter[T: (Event) -> Any](T) -> None\nremove an event filter" #define DOC_EVENT_EVENT "Event(type, dict) -> Event\nEvent(type, **attributes) -> Event\npygame object for representing events" #define DOC_EVENT_EVENT_TYPE "type -> int\nevent type identifier." #define DOC_EVENT_EVENT_DICT "__dict__ -> dict\nevent attribute dictionary" diff --git a/src_c/event.c b/src_c/event.c index 35ac437d4e..c7dae02c62 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -100,6 +100,9 @@ static char released_keys[SDL_NUM_SCANCODES] = {0}; static char pressed_mouse_buttons[5] = {0}; static char released_mouse_buttons[5] = {0}; +/*The filters for events added by the user*/ +static PyObject *userFilters; + #ifdef __EMSCRIPTEN__ /* these macros are no-op here */ #define PG_LOCK_EVFILTER_MUTEX @@ -517,6 +520,9 @@ _pg_remove_pending_VIDEOEXPOSE(void *userdata, SDL_Event *event) } return 1; } +// Forward declare pg_eventNew_no_free_proxy to be used in pg_event_filter +PyObject * +pg_eventNew_no_free_proxy(SDL_Event *event); /* SDL 2 to SDL 1.2 event mapping and SDL 1.2 key repeat emulation, * this can alter events in-place. @@ -709,7 +715,39 @@ pg_event_filter(void *_, SDL_Event *event) return RAISE(pgExc_SDLError, SDL_GetError()), 0; */ } - return PG_EventEnabled(_pg_pgevent_proxify(event->type)); + // If the event has been disabled, skip filtering it + if (!PG_EventEnabled(_pg_pgevent_proxify(event->type))) { + return false; + } + + PG_LOCK_EVFILTER_MUTEX + + PyGILState_STATE gstate = PyGILState_Ensure(); + + Py_ssize_t totalUserFilters = PyList_Size(userFilters); + for (Py_ssize_t i = 0; i < totalUserFilters; i++) { + PyObject *filter = PyList_GetItem(userFilters, i); + PyObject *returnValue = + PyObject_CallOneArg(filter, pg_eventNew_no_free_proxy(event)); + if (!returnValue) { + PyGILState_Release(gstate); + PG_UNLOCK_EVFILTER_MUTEX + + PyErr_Print(); + PyErr_Clear(); + return true; + } + int skip = PyObject_IsTrue(returnValue); + if (skip) { + PyGILState_Release(gstate); + PG_UNLOCK_EVFILTER_MUTEX + return false; + } + } + PyGILState_Release(gstate); + PG_UNLOCK_EVFILTER_MUTEX + + return true; } /* The two keyrepeat functions below modify state accessed by the event filter, @@ -773,6 +811,7 @@ pgEvent_AutoInit(PyObject *self, PyObject *_null) } } #endif + userFilters = PyList_New(0); SDL_SetEventFilter(pg_event_filter, NULL); } _pg_event_is_init = 1; @@ -2505,6 +2544,168 @@ pg_event_custom_type(PyObject *self, PyObject *_null) } } +/* +Constructs a Event object but if it is using a dict proxy it doesn't free the +proxy Used when other pygame functions will also be using the event to avoid it +being freed +*/ +static PyObject * +pg_eventNew_no_free_proxy(SDL_Event *event) +{ + if (event->type >= PGPOST_EVENTBEGIN) { + // Since the dict_proxy has a counter for how many are on the queue, we + // need to increase that counter + pgEventDictProxy *dict_proxy = (pgEventDictProxy *)event->user.data1; + // We only need to do anything if the dict_proxy exists since if it + // doesn't that implies an empty dict + if (dict_proxy) { + SDL_AtomicLock(&dict_proxy->lock); + // So when it gets decremented in pg_EventNew it goes back to where + // it was + dict_proxy->num_on_queue++; + // So that if it gets dropped to zero it wont get freed and risk a + // double free later on + bool proxy_frees_on_end = dict_proxy->do_free_at_end; + dict_proxy->do_free_at_end = false; + SDL_AtomicUnlock(&dict_proxy->lock); + // Contruct the event object + PyObject *eventObj = pgEvent_New(event); + if (proxy_frees_on_end) { + // Restore the state of do_free_at_end + dict_proxy->do_free_at_end = true; + } + return eventObj; + } + } + // If the event is not a posted event than there is no dict_proxy to risk + // issues so the event object constructer can be called as-is + return pgEvent_New(event); +} + +/* +A wrapper around a callable python object to be used by SDL + */ +static int +pg_watcher_wrapper(void *userdata, SDL_Event *event) +{ + PyObject *callable = (PyObject *)userdata; +/* WINDOWEVENT translation needed only on SDL2 */ +#if !SDL_VERSION_ATLEAST(3, 0, 0) + /* We need to translate WINDOWEVENTS. But if we do that from the + * from event filter, internal SDL stuff that rely on WINDOWEVENT + * might break. So after every event pump, we translate events from + * here */ + // We make a local copy of the event on the stack and use that so that the + // original event is not modified + SDL_Event localEvent = *event; + _pg_translate_windowevent(NULL, &localEvent); + + PyObject *eventObj = pg_eventNew_no_free_proxy(&localEvent); +#else + PyObject *eventObj = pg_eventNew_no_free_proxy(event); +#endif + if (PyErr_Occurred()) { + PyErr_Print(); + PyErr_Clear(); + return 0; + } + if (!eventObj) { + return 0; + } + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject *returnValue = PyObject_CallOneArg(callable, eventObj); + if (PyErr_Occurred()) { + PyGILState_Release(gstate); + PyErr_Print(); + PyErr_Clear(); + return 0; + } + PyGILState_Release(gstate); + return 0; +} + +/* +Add a function as an event watcher + +*/ +static PyObject * +pg_event_add_watcher(PyObject *self, PyObject *arg) +{ + VIDEO_INIT_CHECK(); + + if (PyCallable_Check(arg)) { + SDL_AddEventWatch(pg_watcher_wrapper, arg); + } + else { + PyErr_SetString(PyExc_ValueError, + "event watchers must be callable objects"); + return NULL; + } + // Increase reference count of param and return it so that this could be + // used as a decorator + Py_INCREF(arg); + return arg; +} + +static PyObject * +pg_event_remove_watcher(PyObject *self, PyObject *arg) +{ + VIDEO_INIT_CHECK(); + +// This function does nothing if arg is not currently set as a watch +#if SDL_VERSION_ATLEAST(3, 0, 0) + // SDL 3 renamed DelEventWatch to RemoveEventWatch + SDL_RemoveEventWatch(pg_watcher_wrapper, arg); +#else + SDL_DelEventWatch(pg_watcher_wrapper, arg); +#endif + return Py_None; +} + +/* +These two function access state used in the filter, so they use the mutex lock +*/ +static PyObject * +pg_event_add_filter(PyObject *self, PyObject *arg) +{ + VIDEO_INIT_CHECK(); + + if (PyCallable_Check(arg)) { + PyGILState_STATE gstate = PyGILState_Ensure(); + PG_LOCK_EVFILTER_MUTEX + PyList_Append(userFilters, arg); + PG_UNLOCK_EVFILTER_MUTEX + PyGILState_Release(gstate); + } + else { + PyErr_SetString(PyExc_ValueError, + "event filters must be callable objects"); + return NULL; + } + Py_INCREF(arg); + return arg; +} + +static PyObject * +pg_event_remove_filter(PyObject *self, PyObject *arg) +{ + VIDEO_INIT_CHECK(); + + PyGILState_STATE gstate = PyGILState_Ensure(); + PG_LOCK_EVFILTER_MUTEX + Py_ssize_t index = PySequence_Index(userFilters, arg); + if (index == -1) { + PG_UNLOCK_EVFILTER_MUTEX + PyErr_SetString(PyExc_ValueError, "event filter not found"); + PyGILState_Release(gstate); + return NULL; + } + PySequence_DelItem(userFilters, index); + PG_UNLOCK_EVFILTER_MUTEX + PyGILState_Release(gstate); + return Py_None; +} + static PyMethodDef _event_methods[] = { {"_internal_mod_init", (PyCFunction)pgEvent_AutoInit, METH_NOARGS, "auto initialize for event module"}, @@ -2534,6 +2735,14 @@ static PyMethodDef _event_methods[] = { DOC_EVENT_SETBLOCKED}, {"get_blocked", (PyCFunction)pg_event_get_blocked, METH_O, DOC_EVENT_GETBLOCKED}, + {"add_event_watcher", (PyCFunction)pg_event_add_watcher, METH_O, + DOC_EVENT_ADDEVENTWATCHER}, + {"remove_event_watcher", (PyCFunction)pg_event_remove_watcher, METH_O, + DOC_EVENT_REMOVEEVENTWATCHER}, + {"add_event_filter", (PyCFunction)pg_event_add_filter, METH_O, + DOC_EVENT_ADDEVENTFILTER}, + {"remove_event_filter", (PyCFunction)pg_event_remove_filter, METH_O, + DOC_EVENT_REMOVEEVENTFILTER}, {"custom_type", (PyCFunction)pg_event_custom_type, METH_NOARGS, DOC_EVENT_CUSTOMTYPE}, diff --git a/test/event_test.py b/test/event_test.py index f4ce259232..42a1fa5343 100644 --- a/test/event_test.py +++ b/test/event_test.py @@ -935,6 +935,109 @@ def test_poll(self): self.assertEqual(pygame.event.poll().type, e3.type) self.assertEqual(pygame.event.poll().type, pygame.NOEVENT) + def test_add_event_watcher(self): + """Check that the event watcher is called""" + counter = 0 + + def eventWatcher(event): + nonlocal counter + counter += 1 + + pygame.event.add_event_watcher(eventWatcher) + + self.assertEqual(counter, 0) + + pygame.event.clear() + pygame.event.post(pygame.event.Event(pygame.VIDEOEXPOSE)) + + pygame.event.poll() # Make sure that SDL notices + + self.assertEqual(counter, 1) + """Test multiple event watchers""" + + def otherEventWatcher(event): + nonlocal counter + counter = 10 + + pygame.event.add_event_watcher(otherEventWatcher) + + pygame.event.clear() + pygame.event.post(pygame.event.Event(pygame.VIDEOEXPOSE)) + + pygame.event.poll() # Make sure that SDL notices + + self.assertEqual(counter, 10) + + def test_remove_event_watcher(self): + pygame.event.clear() + """Check that the event watcher is removed""" + counter = 0 + + def eventWatcher(event): + nonlocal counter + counter += 1 + + pygame.event.add_event_watcher(eventWatcher) + pygame.event.remove_event_watcher(eventWatcher) + + self.assertEqual(counter, 0) + + pygame.event.clear() + pygame.event.post(pygame.event.Event(pygame.VIDEOEXPOSE)) + + pygame.event.poll() # Make sure that SDL notices + + self.assertEqual(counter, 0) + + def test_add_event_filter(self): + """Check that the event filter is used""" + + def eventFilter(event): + if event.type == pygame.USEREVENT: + return True + return False + + pygame.event.add_event_filter(eventFilter) + + pygame.event.clear() + pygame.event.post(pygame.event.Event(pygame.VIDEOEXPOSE)) + + ev = pygame.event.poll() + self.assertEqual(ev.type, pygame.VIDEOEXPOSE) + + pygame.event.post(pygame.event.Event(pygame.USEREVENT)) + + ev = pygame.event.poll() + self.assertEqual(ev.type, pygame.NOEVENT) + + def test_remove_event_filter(self): + """Check that the event filter is used""" + + def eventFilter(event): + if event.type == pygame.USEREVENT: + return True + return False + + with self.assertRaises(ValueError): + pygame.event.remove_event_filter(eventFilter) + + pygame.event.add_event_filter(eventFilter) + pygame.event.remove_event_filter(eventFilter) + + with self.assertRaises(ValueError): + pygame.event.remove_event_filter(eventFilter) + + pygame.event.clear() + pygame.event.post(pygame.event.Event(pygame.VIDEOEXPOSE)) + + ev = pygame.event.poll() + self.assertEqual(ev.type, pygame.VIDEOEXPOSE) + + pygame.event.post(pygame.event.Event(pygame.USEREVENT)) + + ev = pygame.event.poll() + self.assertEqual(ev.type, pygame.USEREVENT) + class EventModuleTestsWithTiming(unittest.TestCase): __tags__ = ["timing"] From 7f570af9e9c8ddd272a78d8e2673e52ea19c5dda Mon Sep 17 00:00:00 2001 From: SuperRedingBros <89418819+SuperRedingBros@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:33:08 -0500 Subject: [PATCH 2/8] fix a typo --- docs/reST/ref/event.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reST/ref/event.rst b/docs/reST/ref/event.rst index 008e227a1a..70a4406d77 100644 --- a/docs/reST/ref/event.rst +++ b/docs/reST/ref/event.rst @@ -478,7 +478,7 @@ On Android, the following events can be generated For convenience this function returns its parameter. - WARNING: The passed function may be called in a seperate thread. + WARNING: The passed function may be called in a separate thread. The watcher is called when events are added to the queue. The queue processing may occur immediately or when ``pygame.event.poll`` / ``pygame.event.get`` is called. This function is not called if the event is blocked or filtered out. @@ -512,7 +512,7 @@ On Android, the following events can be generated For convenience this function returns its parameter. - WARNING: The passed function may be called in a seperate thread. + WARNING: The passed function may be called in a separate thread. The filter is called before events are added to the queue. This function is not called if the event is blocked. From 9de102a5929a4c385938f61a3700690081082074 Mon Sep 17 00:00:00 2001 From: SuperRedingBros Date: Tue, 4 Nov 2025 19:42:53 -0500 Subject: [PATCH 3/8] Fix a reference counting issue --- src_c/event.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src_c/event.c b/src_c/event.c index c7dae02c62..8c8c29811e 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -2659,7 +2659,7 @@ pg_event_remove_watcher(PyObject *self, PyObject *arg) #else SDL_DelEventWatch(pg_watcher_wrapper, arg); #endif - return Py_None; + Py_RETURN_NONE; } /* @@ -2703,7 +2703,7 @@ pg_event_remove_filter(PyObject *self, PyObject *arg) PySequence_DelItem(userFilters, index); PG_UNLOCK_EVFILTER_MUTEX PyGILState_Release(gstate); - return Py_None; + Py_RETURN_NONE; } static PyMethodDef _event_methods[] = { From c3282b131580ffcfe566c338b409090fda95dda9 Mon Sep 17 00:00:00 2001 From: SuperRedingBros Date: Tue, 4 Nov 2025 19:59:10 -0500 Subject: [PATCH 4/8] Fix some memory leaks --- src_c/event.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src_c/event.c b/src_c/event.c index 8c8c29811e..6780eec2ed 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -738,6 +738,7 @@ pg_event_filter(void *_, SDL_Event *event) return true; } int skip = PyObject_IsTrue(returnValue); + Py_DECREF(returnValue); if (skip) { PyGILState_Release(gstate); PG_UNLOCK_EVFILTER_MUTEX @@ -812,6 +813,9 @@ pgEvent_AutoInit(PyObject *self, PyObject *_null) } #endif userFilters = PyList_New(0); + if (!userFilters) { + return RAISE(pgExc_SDLError, "Failed to initialize event filters"); + } SDL_SetEventFilter(pg_event_filter, NULL); } _pg_event_is_init = 1; @@ -2565,7 +2569,7 @@ pg_eventNew_no_free_proxy(SDL_Event *event) dict_proxy->num_on_queue++; // So that if it gets dropped to zero it wont get freed and risk a // double free later on - bool proxy_frees_on_end = dict_proxy->do_free_at_end; + int proxy_frees_on_end = dict_proxy->do_free_at_end; dict_proxy->do_free_at_end = false; SDL_AtomicUnlock(&dict_proxy->lock); // Contruct the event object @@ -2604,22 +2608,28 @@ pg_watcher_wrapper(void *userdata, SDL_Event *event) #else PyObject *eventObj = pg_eventNew_no_free_proxy(event); #endif + PyGILState_STATE gstate = PyGILState_Ensure(); if (PyErr_Occurred()) { + Py_XDECREF(eventObj); + PyGILState_Release(gstate); PyErr_Print(); PyErr_Clear(); return 0; } if (!eventObj) { + PyGILState_Release(gstate); return 0; } - PyGILState_STATE gstate = PyGILState_Ensure(); PyObject *returnValue = PyObject_CallOneArg(callable, eventObj); + Py_DECREF(eventObj); if (PyErr_Occurred()) { + Py_XDECREF(returnValue); PyGILState_Release(gstate); PyErr_Print(); PyErr_Clear(); return 0; } + Py_DECREF(returnValue); PyGILState_Release(gstate); return 0; } @@ -2671,11 +2681,9 @@ pg_event_add_filter(PyObject *self, PyObject *arg) VIDEO_INIT_CHECK(); if (PyCallable_Check(arg)) { - PyGILState_STATE gstate = PyGILState_Ensure(); PG_LOCK_EVFILTER_MUTEX PyList_Append(userFilters, arg); PG_UNLOCK_EVFILTER_MUTEX - PyGILState_Release(gstate); } else { PyErr_SetString(PyExc_ValueError, @@ -2691,7 +2699,6 @@ pg_event_remove_filter(PyObject *self, PyObject *arg) { VIDEO_INIT_CHECK(); - PyGILState_STATE gstate = PyGILState_Ensure(); PG_LOCK_EVFILTER_MUTEX Py_ssize_t index = PySequence_Index(userFilters, arg); if (index == -1) { @@ -2702,7 +2709,6 @@ pg_event_remove_filter(PyObject *self, PyObject *arg) } PySequence_DelItem(userFilters, index); PG_UNLOCK_EVFILTER_MUTEX - PyGILState_Release(gstate); Py_RETURN_NONE; } From af99c8bf2624eec83b99b39b2c90563d09055f7e Mon Sep 17 00:00:00 2001 From: SuperRedingBros Date: Tue, 4 Nov 2025 19:59:10 -0500 Subject: [PATCH 5/8] Fix some memory leaks --- src_c/event.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src_c/event.c b/src_c/event.c index 8c8c29811e..9bc388a854 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -738,6 +738,7 @@ pg_event_filter(void *_, SDL_Event *event) return true; } int skip = PyObject_IsTrue(returnValue); + Py_DECREF(returnValue); if (skip) { PyGILState_Release(gstate); PG_UNLOCK_EVFILTER_MUTEX @@ -812,6 +813,9 @@ pgEvent_AutoInit(PyObject *self, PyObject *_null) } #endif userFilters = PyList_New(0); + if (!userFilters) { + return RAISE(pgExc_SDLError, "Failed to initialize event filters"); + } SDL_SetEventFilter(pg_event_filter, NULL); } _pg_event_is_init = 1; @@ -2565,7 +2569,7 @@ pg_eventNew_no_free_proxy(SDL_Event *event) dict_proxy->num_on_queue++; // So that if it gets dropped to zero it wont get freed and risk a // double free later on - bool proxy_frees_on_end = dict_proxy->do_free_at_end; + int proxy_frees_on_end = dict_proxy->do_free_at_end; dict_proxy->do_free_at_end = false; SDL_AtomicUnlock(&dict_proxy->lock); // Contruct the event object @@ -2604,22 +2608,28 @@ pg_watcher_wrapper(void *userdata, SDL_Event *event) #else PyObject *eventObj = pg_eventNew_no_free_proxy(event); #endif + PyGILState_STATE gstate = PyGILState_Ensure(); if (PyErr_Occurred()) { + Py_XDECREF(eventObj); + PyGILState_Release(gstate); PyErr_Print(); PyErr_Clear(); return 0; } if (!eventObj) { + PyGILState_Release(gstate); return 0; } - PyGILState_STATE gstate = PyGILState_Ensure(); PyObject *returnValue = PyObject_CallOneArg(callable, eventObj); + Py_DECREF(eventObj); if (PyErr_Occurred()) { + Py_XDECREF(returnValue); PyGILState_Release(gstate); PyErr_Print(); PyErr_Clear(); return 0; } + Py_DECREF(returnValue); PyGILState_Release(gstate); return 0; } @@ -2671,11 +2681,9 @@ pg_event_add_filter(PyObject *self, PyObject *arg) VIDEO_INIT_CHECK(); if (PyCallable_Check(arg)) { - PyGILState_STATE gstate = PyGILState_Ensure(); PG_LOCK_EVFILTER_MUTEX PyList_Append(userFilters, arg); PG_UNLOCK_EVFILTER_MUTEX - PyGILState_Release(gstate); } else { PyErr_SetString(PyExc_ValueError, @@ -2691,18 +2699,15 @@ pg_event_remove_filter(PyObject *self, PyObject *arg) { VIDEO_INIT_CHECK(); - PyGILState_STATE gstate = PyGILState_Ensure(); PG_LOCK_EVFILTER_MUTEX Py_ssize_t index = PySequence_Index(userFilters, arg); if (index == -1) { PG_UNLOCK_EVFILTER_MUTEX PyErr_SetString(PyExc_ValueError, "event filter not found"); - PyGILState_Release(gstate); return NULL; } PySequence_DelItem(userFilters, index); PG_UNLOCK_EVFILTER_MUTEX - PyGILState_Release(gstate); Py_RETURN_NONE; } From 0c5b42fea869b169d144307e234d09ce719a234d Mon Sep 17 00:00:00 2001 From: SuperRedingBros Date: Tue, 4 Nov 2025 20:15:15 -0500 Subject: [PATCH 6/8] fix some error path issues --- src_c/event.c | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src_c/event.c b/src_c/event.c index 9bc388a854..4fbae1fa40 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -521,7 +521,7 @@ _pg_remove_pending_VIDEOEXPOSE(void *userdata, SDL_Event *event) return 1; } // Forward declare pg_eventNew_no_free_proxy to be used in pg_event_filter -PyObject * +static PyObject * pg_eventNew_no_free_proxy(SDL_Event *event); /* SDL 2 to SDL 1.2 event mapping and SDL 1.2 key repeat emulation, @@ -727,9 +727,15 @@ pg_event_filter(void *_, SDL_Event *event) Py_ssize_t totalUserFilters = PyList_Size(userFilters); for (Py_ssize_t i = 0; i < totalUserFilters; i++) { PyObject *filter = PyList_GetItem(userFilters, i); - PyObject *returnValue = - PyObject_CallOneArg(filter, pg_eventNew_no_free_proxy(event)); - if (!returnValue) { + PyObject *eventObj = pg_eventNew_no_free_proxy(event); + if (!eventObj) { + PyErr_Print(); + PyErr_Clear(); + break; + } + PyObject *returnValue = PyObject_CallOneArg(filter, eventObj); + Py_DECREF(eventObj) if (!returnValue) + { PyGILState_Release(gstate); PG_UNLOCK_EVFILTER_MUTEX @@ -2574,10 +2580,8 @@ pg_eventNew_no_free_proxy(SDL_Event *event) SDL_AtomicUnlock(&dict_proxy->lock); // Contruct the event object PyObject *eventObj = pgEvent_New(event); - if (proxy_frees_on_end) { - // Restore the state of do_free_at_end - dict_proxy->do_free_at_end = true; - } + // Restore the state of do_free_at_end + dict_proxy->do_free_at_end = proxy_frees_on_end; return eventObj; } } @@ -2593,6 +2597,7 @@ static int pg_watcher_wrapper(void *userdata, SDL_Event *event) { PyObject *callable = (PyObject *)userdata; + PyGILState_STATE gstate = PyGILState_Ensure(); /* WINDOWEVENT translation needed only on SDL2 */ #if !SDL_VERSION_ATLEAST(3, 0, 0) /* We need to translate WINDOWEVENTS. But if we do that from the @@ -2608,7 +2613,6 @@ pg_watcher_wrapper(void *userdata, SDL_Event *event) #else PyObject *eventObj = pg_eventNew_no_free_proxy(event); #endif - PyGILState_STATE gstate = PyGILState_Ensure(); if (PyErr_Occurred()) { Py_XDECREF(eventObj); PyGILState_Release(gstate); @@ -2644,6 +2648,7 @@ pg_event_add_watcher(PyObject *self, PyObject *arg) VIDEO_INIT_CHECK(); if (PyCallable_Check(arg)) { + Py_INCREF(arg); SDL_AddEventWatch(pg_watcher_wrapper, arg); } else { @@ -2666,10 +2671,12 @@ pg_event_remove_watcher(PyObject *self, PyObject *arg) #if SDL_VERSION_ATLEAST(3, 0, 0) // SDL 3 renamed DelEventWatch to RemoveEventWatch SDL_RemoveEventWatch(pg_watcher_wrapper, arg); + Py_DECREF(arg) #else SDL_DelEventWatch(pg_watcher_wrapper, arg); + Py_DECREF(arg) #endif - Py_RETURN_NONE; + Py_RETURN_NONE; } /* @@ -2682,7 +2689,10 @@ pg_event_add_filter(PyObject *self, PyObject *arg) if (PyCallable_Check(arg)) { PG_LOCK_EVFILTER_MUTEX - PyList_Append(userFilters, arg); + if (PyList_Append(userFilters, arg) < 0) { + PG_UNLOCK_EVFILTER_MUTEX + return NULL; // PyErr already set + } PG_UNLOCK_EVFILTER_MUTEX } else { @@ -2703,10 +2713,16 @@ pg_event_remove_filter(PyObject *self, PyObject *arg) Py_ssize_t index = PySequence_Index(userFilters, arg); if (index == -1) { PG_UNLOCK_EVFILTER_MUTEX - PyErr_SetString(PyExc_ValueError, "event filter not found"); - return NULL; + if (PyErr_Occurred()) { + // An error occurred during the search + return NULL; + } + return RAISE(PyExc_ValueError, "event filter not found"); + } + if (PySequence_DelItem(userFilters, index) < 0) { + PG_UNLOCK_EVFILTER_MUTEX + return NULL; // PyErr already set } - PySequence_DelItem(userFilters, index); PG_UNLOCK_EVFILTER_MUTEX Py_RETURN_NONE; } From bd10dee9804c796a5c379f83bb4bb3d31bbf9398 Mon Sep 17 00:00:00 2001 From: SuperRedingBros Date: Tue, 4 Nov 2025 20:23:08 -0500 Subject: [PATCH 7/8] resolve conflicts --- buildconfig/stubs/pygame/event.pyi | 2 +- src_c/event.c | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/buildconfig/stubs/pygame/event.pyi b/buildconfig/stubs/pygame/event.pyi index 1c3fb99211..4abb4a0695 100644 --- a/buildconfig/stubs/pygame/event.pyi +++ b/buildconfig/stubs/pygame/event.pyi @@ -1,4 +1,4 @@ -from typing import Any, Callable, ClassVar, Optional, Union, final +from typing import Any, Callable, ClassVar, Optional, TypeAlias, Union, final from pygame.typing import SequenceLike from typing_extensions import deprecated # added in 3.13 diff --git a/src_c/event.c b/src_c/event.c index 4fbae1fa40..24c80b0dd5 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -716,6 +716,14 @@ pg_event_filter(void *_, SDL_Event *event) */ } // If the event has been disabled, skip filtering it + /* TODO: + * Any event that gets blocked here will not be visible to the event + * watchers. So things like WINDOWEVENT should never be blocked here. + * This is taken care of in SDL2 codepaths already but needs to also + * be verified in SDL3 porting. + * If the user requests a block on WINDOWEVENTs we are going to handle + * it specially and call it a "pseudo-block", where the filtering will + * happen in a _pg_filter_blocked_events call. */ if (!PG_EventEnabled(_pg_pgevent_proxify(event->type))) { return false; } From 3e98bcc0bab0ee94f9aabac421f92fbc1fb6ebf0 Mon Sep 17 00:00:00 2001 From: SuperRedingBros Date: Tue, 4 Nov 2025 20:28:02 -0500 Subject: [PATCH 8/8] fix more issues --- src_c/event.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src_c/event.c b/src_c/event.c index 24c80b0dd5..34655c8e75 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -742,8 +742,8 @@ pg_event_filter(void *_, SDL_Event *event) break; } PyObject *returnValue = PyObject_CallOneArg(filter, eventObj); - Py_DECREF(eventObj) if (!returnValue) - { + Py_DECREF(eventObj); + if (!returnValue) { PyGILState_Release(gstate); PG_UNLOCK_EVFILTER_MUTEX @@ -2679,12 +2679,12 @@ pg_event_remove_watcher(PyObject *self, PyObject *arg) #if SDL_VERSION_ATLEAST(3, 0, 0) // SDL 3 renamed DelEventWatch to RemoveEventWatch SDL_RemoveEventWatch(pg_watcher_wrapper, arg); - Py_DECREF(arg) + Py_DECREF(arg); #else SDL_DelEventWatch(pg_watcher_wrapper, arg); - Py_DECREF(arg) + Py_DECREF(arg); #endif - Py_RETURN_NONE; + Py_RETURN_NONE; } /*