From d3735512fc54dc4b31748d53f856a24c29ca1908 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 11:18:10 +0000 Subject: [PATCH] Add axRemoveMessages to drop queued events by target and type axRemoveFromQueue drops everything queued for a target and cancels its timers, which is right for a destroyed window but too broad for one stale event. axRemoveMessages(target, message, wparam) removes only the events of one type for one target; AX_ANY_WPARAM ((uint32_t)-1) matches any wParam, and a specific value removes, for example, the queued ticks of one cancelled timer. Other events keep their order and the target's timers keep running. The ring-buffer backends (X11, Wayland, Windows, WebGL, QNX) compact the queue. macOS clears matches in place because Cocoa events refer to queue slots by index, and peek already skips cleared slots. iOS shares one matcher with axRemoveFromQueue. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gft4mytn9t9qr1ANVAo2gW --- ios/ios_event.m | 2 +- ios/ios_message_queue.h | 3 ++- ios/ios_message_queue.m | 9 +++++-- macos/macos_event.m | 11 +++++++++ platform.h | 19 +++++++++++++++ qnx/qnx_system.c | 12 ++++++++++ tests/test_ios_message_queue.c | 24 ++++++++++++++++--- tests/test_macos_events.m | 23 ++++++++++++++++++ tests/test_messages.c | 43 +++++++++++++++++++++++++++++++++- wayland/wayland_event.c | 15 ++++++++++++ webgl/webgl_event.c | 15 ++++++++++++ windows/windows_event.c | 15 ++++++++++++ x11/x11_event.c | 15 ++++++++++++ 13 files changed, 198 insertions(+), 8 deletions(-) diff --git a/ios/ios_event.m b/ios/ios_event.m index 363bdf9..511fb89 100644 --- a/ios/ios_event.m +++ b/ios/ios_event.m @@ -24,7 +24,7 @@ int axWaitMessage(longTime_t msec) { } void axRemoveFromQueue(void *target) { - ios_remove_messages(target); + ios_remove_messages(target, 0, AX_ANY_WPARAM); for (NSNumber *key in ios_timers.allKeys) { if ([objc_getAssociatedObject(ios_timers[key], &ios_timer_owner) pointerValue] == target) axCancelTimer(key.unsignedIntValue); } diff --git a/ios/ios_message_queue.h b/ios/ios_message_queue.h index 94d8e7f..b21207c 100644 --- a/ios/ios_message_queue.h +++ b/ios/ios_message_queue.h @@ -10,7 +10,8 @@ void ios_post_display_paint(uint32_t size); // Freeze the readable queue at the start of a UIKit display callback. void ios_begin_message_frame(void); int ios_messages_ready(void); -void ios_remove_messages(void *target); +// Remove queued messages for target; message 0 matches any type. +void ios_remove_messages(void *target, uint32_t message, uint32_t wparam); void ios_shutdown_messages(void); #endif diff --git a/ios/ios_message_queue.m b/ios/ios_message_queue.m index 1bb9e83..25e5ef9 100644 --- a/ios/ios_message_queue.m +++ b/ios/ios_message_queue.m @@ -99,13 +99,14 @@ int axPeekMessage(struct AXmessage *msg) { return ready; } -void ios_remove_messages(void *target) { +void ios_remove_messages(void *target, uint32_t message, uint32_t wparam) { pthread_mutex_lock(&ios_queue_mutex); ios_message_t **link = &ios_head; ios_tail = NULL; while (*link) { ios_message_t *p = *link; - if (p->value.target == target) { + if (p->value.target == target && (!message || p->value.message == message) && + (wparam == AX_ANY_WPARAM || p->value.wParam == wparam)) { *link = p->next; if (p->value.message == kEventDragDrop) free(p->value.lParam); free(p); @@ -115,6 +116,10 @@ void ios_remove_messages(void *target) { pthread_mutex_unlock(&ios_queue_mutex); } +void axRemoveMessages(void *target, uint32_t message, uint32_t wparam) { + if (message) ios_remove_messages(target, message, wparam); +} + void ios_shutdown_messages(void) { pthread_mutex_lock(&ios_queue_mutex); while (ios_head) { diff --git a/macos/macos_event.m b/macos/macos_event.m index 33e615a..3852a2b 100644 --- a/macos/macos_event.m +++ b/macos/macos_event.m @@ -40,6 +40,17 @@ uint32_t KEY_GetKeyName(uint32_t keycode) { axCancelTimer(s_timers[i].id); } +void +axRemoveMessages(void* target, uint32_t message, uint32_t wparam) +{ + // Cocoa events refer to queue slots by index, so clear matches in place. + for (uint16_t r = queue.read; r != queue.write; r++) { + struct AXmessage const *e = &queue.data[r]; + if (e->target == target && e->message == message && (wparam == AX_ANY_WPARAM || e->wParam == wparam)) + memset(&queue.data[r], 0, sizeof(queue.data[r])); + } +} + void axPostMessageW(void* obj, uint32_t Msg, wParam_t wParam, lParam_t lParam) { diff --git a/platform.h b/platform.h index 1f20f93..7cc8af2 100644 --- a/platform.h +++ b/platform.h @@ -406,6 +406,25 @@ axGetMessage(struct AXmessage* msg); AX_API void axRemoveFromQueue(void* target); +/** @brief Wildcard for #axRemoveMessages: match an event whatever its wParam. */ +#define AX_ANY_WPARAM ((uint32_t)-1) + +/** + * @brief Remove queued events of one type for one target. + * + * Removes every queued event whose target is @p target and whose type is + * @p message, keeping the order of the others. Pass #AX_ANY_WPARAM as + * @p wparam to match any wParam, or a value to remove only the events that + * carry it, such as the ticks of one cancelled timer. Unlike + * #axRemoveFromQueue, this leaves the target's timers running. + * + * @param target Window or object handle to match against. + * @param message Event type to remove (e.g. #kEventTimer). + * @param wparam wParam to match, or #AX_ANY_WPARAM for any. + */ +AX_API void +axRemoveMessages(void* target, uint32_t message, uint32_t wparam); + /** * @brief Notify the event system that a file was dropped onto the window. * diff --git a/qnx/qnx_system.c b/qnx/qnx_system.c index edd013b..71974a5 100644 --- a/qnx/qnx_system.c +++ b/qnx/qnx_system.c @@ -369,6 +369,18 @@ axRemoveFromQueue(void *target) memset(&queue.data[r], 0, sizeof(queue.data[r])); } +void +axRemoveMessages(void *target, uint32_t message, uint32_t wparam) +{ + uint16_t r = queue.read, w = queue.write, nw = queue.read; + while (r != w) { + struct AXmessage const *e = &queue.data[r++]; + if (e->target != target || e->message != message || (wparam != AX_ANY_WPARAM && e->wParam != wparam)) + queue.data[nw++] = *e; + } + queue.write = nw; +} + int axWaitMessage(longTime_t msec) { diff --git a/tests/test_ios_message_queue.c b/tests/test_ios_message_queue.c index 398a4d7..2ee917e 100644 --- a/tests/test_ios_message_queue.c +++ b/tests/test_ios_message_queue.c @@ -73,10 +73,10 @@ static void test_removal_and_reset(void) { axPostMessageW(&first, kEventDragDrop, 0, strdup("removed file")); ios_begin_message_frame(); axPostMessageW(&second, kEventKeyUp, 3, NULL); - ios_remove_messages(&first); + ios_remove_messages(&first, 0, AX_ANY_WPARAM); assert(axPeekMessage(&msg) && msg.target == &second && msg.wParam == 2); assert(!axPeekMessage(&msg)); - ios_remove_messages(&second); + ios_remove_messages(&second, 0, AX_ANY_WPARAM); axPostMessageW(&first, kEventKeyDown, 4, NULL); assert(!axPeekMessage(&msg)); ios_begin_message_frame(); @@ -89,6 +89,23 @@ static void test_removal_and_reset(void) { assert(!axPeekMessage(&msg)); } +static void test_remove_by_type(void) { + ios_shutdown_messages(); + int first, second; + struct AXmessage msg; + axPostMessageW(&first, kEventTimer, 1, NULL); + axPostMessageW(&first, kEventTimer, 2, NULL); + axPostMessageW(&second, kEventTimer, 1, NULL); + axPostMessageW(&first, kEventKeyDown, 3, NULL); + axRemoveMessages(&first, kEventTimer, 1); + assert(axPeekMessage(&msg) && msg.target == &first && msg.message == kEventTimer && msg.wParam == 2); + assert(axPeekMessage(&msg) && msg.target == &second && msg.wParam == 1); + axPostMessageW(&first, kEventTimer, 4, NULL); + axRemoveMessages(&first, kEventTimer, AX_ANY_WPARAM); + assert(axPeekMessage(&msg) && msg.message == kEventKeyDown && msg.wParam == 3); + assert(!axPeekMessage(&msg)); +} + static void test_sequence_wrap(void) { ios_shutdown_messages(); ios_test_message_sequence(UINT32_MAX - 1); @@ -116,8 +133,9 @@ int main(void) { test_inertia_interrupt(); test_complete_snapshot(); test_removal_and_reset(); + test_remove_by_type(); test_sequence_wrap(); ios_shutdown_messages(); - puts("PASS: iOS queue frame boundaries, touch interruption, FIFO, removal, reset and sequence wrap"); + puts("PASS: iOS queue frame boundaries, touch interruption, FIFO, removal by target and type, reset and sequence wrap"); return 0; } diff --git a/tests/test_macos_events.m b/tests/test_macos_events.m index 2d3d13d..ee03f3c 100644 --- a/tests/test_macos_events.m +++ b/tests/test_macos_events.m @@ -39,6 +39,28 @@ static void test_event_lifetime(void) } } +// Removed messages leave cleared slots that Cocoa still points at. +static void test_remove_messages(void) +{ + struct AXmessage msg; + void *a = (void *)&test_remove_messages, *b = (void *)&drain_events; + drain_events(); + axPostMessageW(a, kEventTimer, 1, NULL); + axPostMessageW(a, kEventTimer, 2, NULL); + axPostMessageW(b, kEventTimer, 1, NULL); + axPostMessageW(a, kEventKeyDown, 3, NULL); + axPostMessageW(a, kEventTimer, 1, NULL); + axRemoveMessages(a, kEventTimer, 1); + assert(axPeekMessage(&msg) == 1 && msg.target == a && msg.message == kEventTimer && msg.wParam == 2); + assert(axPeekMessage(&msg) == 1 && msg.target == b && msg.message == kEventTimer && msg.wParam == 1); + assert(axPeekMessage(&msg) == 1 && msg.target == a && msg.message == kEventKeyDown); + axPostMessageW(a, kEventTimer, 4, NULL); + axPostMessageW(a, kEventKeyUp, 5, NULL); + axRemoveMessages(a, kEventTimer, AX_ANY_WPARAM); + assert(axPeekMessage(&msg) == 1 && msg.message == kEventKeyUp && msg.wParam == 5); + drain_events(); +} + static void test_fullscreen(void) { assert(axCreateWindow("Fullscreen event test", 640, 480, @@ -67,6 +89,7 @@ int main(int argc, char **argv) axInit(); drain_events(); test_event_lifetime(); + test_remove_messages(); if (argc > 1 && !strcmp(argv[1], "--fullscreen")) test_fullscreen(); puts("macOS event lifetime tests passed."); } diff --git a/tests/test_messages.c b/tests/test_messages.c index 81cf44f..998c83d 100644 --- a/tests/test_messages.c +++ b/tests/test_messages.c @@ -1,7 +1,8 @@ /* * test_messages.c – behavioral tests for the platform event-queue API. * - * Tests axPostMessageW, axPeekMessage, and axRemoveFromQueue without + * Tests axPostMessageW, axPeekMessage, axRemoveFromQueue and + * axRemoveMessages without * requiring a live display server or windowing context. * * On macOS the internal queue is coupled to NSApp, so message-delivery tests @@ -136,6 +137,45 @@ static void test_remove_nonexistent_target(void) assert(axPeekMessage(&msg) == 0); } +/* ------------------------------------------------------------------- + * test_remove_messages + * axRemoveMessages must remove only one type for one target, matching + * wParam exactly unless AX_ANY_WPARAM is given. + * ------------------------------------------------------------------- */ +static void test_remove_messages(void) +{ + struct AXmessage msg; + void *handle_a = (void *)0xA0A0; + void *handle_b = (void *)0xB0B0; + + drain_queue(); + + axPostMessageW(handle_a, kEventTimer, 1, NULL); + axPostMessageW(handle_a, kEventTimer, 2, NULL); + axPostMessageW(handle_b, kEventTimer, 1, NULL); + axPostMessageW(handle_a, kEventKeyDown, 1, NULL); + axPostMessageW(handle_a, kEventTimer, 1, NULL); + + /* One timer of handle_a: other timers, targets and types stay queued */ + axRemoveMessages(handle_a, kEventTimer, 1); + assert(axPeekMessage(&msg) == 1); + assert(msg.target == handle_a && msg.message == kEventTimer && msg.wParam == 2); + assert(axPeekMessage(&msg) == 1); + assert(msg.target == handle_b && msg.message == kEventTimer && msg.wParam == 1); + assert(axPeekMessage(&msg) == 1); + assert(msg.target == handle_a && msg.message == kEventKeyDown); + assert(axPeekMessage(&msg) == 0); + + /* AX_ANY_WPARAM removes the type whatever its wParam */ + axPostMessageW(handle_a, kEventTimer, 3, NULL); + axPostMessageW(handle_a, kEventKeyDown, 4, NULL); + axPostMessageW(handle_a, kEventTimer, 5, NULL); + axRemoveMessages(handle_a, kEventTimer, AX_ANY_WPARAM); + assert(axPeekMessage(&msg) == 1); + assert(msg.message == kEventKeyDown && msg.wParam == 4); + assert(axPeekMessage(&msg) == 0); +} + /* ------------------------------------------------------------------- * test_lparam_roundtrip * The lParam pointer must survive the queue round-trip unchanged. @@ -230,6 +270,7 @@ int main(void) test_fifo_ordering(); test_remove_from_queue(); test_remove_nonexistent_target(); + test_remove_messages(); test_lparam_roundtrip(); test_double_click_sequence(); test_get_message(); diff --git a/wayland/wayland_event.c b/wayland/wayland_event.c index 94eed13..a545e59 100644 --- a/wayland/wayland_event.c +++ b/wayland/wayland_event.c @@ -797,6 +797,21 @@ axRemoveFromQueue(void* hobj) axCancelTimer(s_timers[i].id); } +void +axRemoveMessages(void* hobj, uint32_t event, uint32_t wparam) +{ + WORD read_idx = events.read; + WORD write_idx = events.write; + WORD new_write = events.read; + + while (read_idx != write_idx) { + EVENT const *e = &events.queue[read_idx++]; + if (e->target != hobj || e->message != event || (wparam != AX_ANY_WPARAM && e->wParam != wparam)) + events.queue[new_write++] = *e; + } + events.write = new_write; +} + void axNotifyFileDropEvent(char const* filename, float x, float y) { diff --git a/webgl/webgl_event.c b/webgl/webgl_event.c index d24b1c3..5c09c2d 100644 --- a/webgl/webgl_event.c +++ b/webgl/webgl_event.c @@ -376,6 +376,21 @@ axRemoveFromQueue(void *hobj) axCancelTimer(s_timers[i].id); } +void +axRemoveMessages(void *hobj, uint32_t event, uint32_t wparam) +{ + WORD read_idx = events.read; + WORD write_idx = events.write; + WORD new_write = events.read; + + while (read_idx != write_idx) { + EVENT const *e = &events.queue[read_idx++]; + if (e->target != hobj || e->message != event || (wparam != AX_ANY_WPARAM && e->wParam != wparam)) + events.queue[new_write++] = *e; + } + events.write = new_write; +} + void axNotifyFileDropEvent(char const *filename, float x, float y) { diff --git a/windows/windows_event.c b/windows/windows_event.c index f6ef540..83ddba7 100644 --- a/windows/windows_event.c +++ b/windows/windows_event.c @@ -495,6 +495,21 @@ axRemoveFromQueue(void *hobj) axCancelTimer(s_timers[i].id); } +void +axRemoveMessages(void *hobj, uint32_t event, uint32_t wparam) +{ + WORD read_idx = events.read; + WORD write_idx = events.write; + WORD new_write = events.read; + + while (read_idx != write_idx) { + EVENT const *e = &events.queue[read_idx++]; + if (e->target != hobj || e->message != event || (wparam != AX_ANY_WPARAM && e->wParam != wparam)) + events.queue[new_write++] = *e; + } + events.write = new_write; +} + void axNotifyFileDropEvent(char const *filename, float x, float y) { diff --git a/x11/x11_event.c b/x11/x11_event.c index 41d9ef5..1ca691b 100644 --- a/x11/x11_event.c +++ b/x11/x11_event.c @@ -765,6 +765,21 @@ axRemoveFromQueue(void* hobj) #endif } +void +axRemoveMessages(void* hobj, uint32_t event, uint32_t wparam) +{ + WORD read_idx = events.read; + WORD write_idx = events.write; + WORD new_write = events.read; + + while (read_idx != write_idx) { + EVENT const *e = &events.queue[read_idx++]; + if (e->target != hobj || e->message != event || (wparam != AX_ANY_WPARAM && e->wParam != wparam)) + events.queue[new_write++] = *e; + } + events.write = new_write; +} + void axNotifyFileDropEvent(char const* filename, float x, float y) {