From 634e4ffd1a93d62c3875e02f5ba69085fa0b4bcf Mon Sep 17 00:00:00 2001 From: Viatcheslav Gachkaylo Date: Fri, 28 Jan 2011 12:56:57 +0000 Subject: [PATCH] osx: Improve the event loop Implementation of event loop which works with GLib events, native OS X events and Clutter events. The event loop source code comes from the equivalent code in the Quartz GDK backend from GTK+ 2.22.1, which is LGPL v2.1+ and thus compatible with Clutter's licensing terms. The code has been tested with libsoup, which did not work before together with Clutter. Signed-off-by: Emmanuele Bassi http://bugzilla.clutter-project.org/show_bug.cgi?id=2490 --- clutter/Makefile.am | 6 +- clutter/clutter-debug.h | 3 +- clutter/osx/clutter-event-loop-osx.c | 1051 ++++++++++++++++++++++++++ clutter/osx/clutter-event-loop-osx.h | 32 + clutter/osx/clutter-event-osx.c | 223 ++---- clutter/osx/clutter-stage-osx.h | 3 + 6 files changed, 1176 insertions(+), 142 deletions(-) create mode 100644 clutter/osx/clutter-event-loop-osx.c create mode 100644 clutter/osx/clutter-event-loop-osx.h diff --git a/clutter/Makefile.am b/clutter/Makefile.am index a3a61e414..4fc246628 100644 --- a/clutter/Makefile.am +++ b/clutter/Makefile.am @@ -500,10 +500,14 @@ osx_source_h = $(srcdir)/osx/clutter-osx.h osx_source_h_priv = \ $(srcdir)/osx/clutter-backend-osx.h \ + $(srcdir)/osx/clutter-event-loop-osx.h \ $(srcdir)/osx/clutter-stage-osx.h \ $(NULL) -osx_source_c_priv = $(srcdir)/osx/clutter-event-osx.c +osx_source_c_priv = \ + $(srcdir)/osx/clutter-event-loop-osx.c \ + $(srcdir)/osx/clutter-event-osx.c \ + $(NULL) if SUPPORT_OSX # we need to tell the compiler that part of our code base is diff --git a/clutter/clutter-debug.h b/clutter/clutter-debug.h index 63d88a20b..d85e8c1bc 100644 --- a/clutter/clutter-debug.h +++ b/clutter/clutter-debug.h @@ -24,7 +24,8 @@ typedef enum { CLUTTER_DEBUG_MULTISTAGE = 1 << 13, CLUTTER_DEBUG_ANIMATION = 1 << 14, CLUTTER_DEBUG_LAYOUT = 1 << 15, - CLUTTER_DEBUG_PICK = 1 << 16 + CLUTTER_DEBUG_PICK = 1 << 16, + CLUTTER_DEBUG_EVENTLOOP = 1 << 17 } ClutterDebugFlag; typedef enum { diff --git a/clutter/osx/clutter-event-loop-osx.c b/clutter/osx/clutter-event-loop-osx.c new file mode 100644 index 000000000..80f7332f1 --- /dev/null +++ b/clutter/osx/clutter-event-loop-osx.c @@ -0,0 +1,1051 @@ +/* source code taken from gtk+-2.22.1/gdk/quartz/gdkeventloop-quartz.c */ +/* Clutter - An OpenGL based 'interactive canvas' library. + * OSX backend - event loop + * + * Copyright (C) 2005-2007 Imendio AB + * Copyright (C) 2011 Crystalnix + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * + */ + +#include "config.h" + +#include "clutter-osx.h" +#include "clutter-stage-osx.h" + +#import +#include +#include +#include +#include +#include +#include + +/* + * This file implementations integration between the GLib main loop and + * the native system of the Core Foundation run loop and Cocoa event + * handling. There are basically two different cases that we need to + * handle: either the GLib main loop is in control (the application + * has called gtk_main(), or is otherwise iterating the main loop), or + * CFRunLoop is in control (we are in a modal operation such as window + * resizing or drag-and-drop.) + * + * When the GLib main loop is in control we integrate in native event + * handling in two ways: first we add a GSource that handles checking + * whether there are native events available, translating native events + * to clutter events, and dispatching GDK events. Second we replace the + * "poll function" of the GLib main loop with our own version that knows + * how to wait for both the file descriptors and timeouts that GLib is + * interested in and also for incoming native events. + * + * When CFRunLoop is in control, we integrate in GLib main loop handling + * by adding a "run loop observer" that gives us notification at various + * points in the run loop cycle. We map these points onto the corresponding + * stages of the GLib main loop (prepare, check, dispatch), and make the + * appropriate calls into GLib. + * + * Both cases share a single problem: the OS X API's don't allow us to + * wait simultaneously for file descriptors and for events. So when we + * need to do a blocking wait that includes file descriptor activity, we + * push the actual work of calling select() to a helper thread (the + * "select thread") and wait for native events in the main thread. + * + * The main known limitation of this code is that if a callback is triggered + * via the OS X run loop while we are "polling" (in either case described + * above), iteration of the GLib main loop is not possible from within + * that callback. If the programmer tries to do so explicitly, then they + * will get a warning from GLib "main loop already active in another thread". + */ + +/******* State for run loop iteration *******/ + +/* Count of number of times we've gotten an "Entry" notification for + * our run loop observer. + */ +static int current_loop_level = 0; + +/* Run loop level at which we acquired ownership of the GLib main + * loop. See note in run_loop_entry(). -1 means that we don't have + * ownership + */ +static int acquired_loop_level = -1; + +/* Between run_loop_before_waiting() and run_loop_after_waiting(); + * whether we we need to call select_thread_collect_poll() + */ +static gboolean run_loop_polling_async = FALSE; + +/* Between run_loop_before_waiting() and run_loop_after_waiting(); + * max_prioritiy to pass to g_main_loop_check() + */ +static gint run_loop_max_priority; + +/* Timer that we've added to wake up the run loop when a GLib timeout + */ +static CFRunLoopTimerRef run_loop_timer = NULL; + +/* These are the file descriptors that are we are polling out of + * the run loop. (We keep the array around and reuse it to avoid + * constant allocations.) + */ +#define RUN_LOOP_POLLFDS_INITIAL_SIZE 16 +static GPollFD *run_loop_pollfds; +static guint run_loop_pollfds_size; /* Allocated size of the array */ +static guint run_loop_n_pollfds; /* Number of file descriptors in the array */ + +/******* Other global variables *******/ + +/* Since we count on replacing the GLib main loop poll function as our + * method of integrating Cocoa event handling into the GLib main loop + * we need to make sure that the poll function is always called even + * when there are no file descriptors that need to be polled. To do + * this, we add a dummy GPollFD to our event source with a file + * descriptor of '-1'. Then any time that GLib is polling the event + * source, it will call our poll function. + */ +static GPollFD event_poll_fd; + +/* Current NSEvents that we've gotten from Cocoa but haven't yet converted + * to GdkEvents. We wait until our dispatch() function to do the conversion + * since the conversion can conceivably cause signals to be emmitted + * or other things that shouldn't happen inside a poll function. + */ +static GQueue *current_events; + +/* The default poll function for GLib; we replace this with our own + * Cocoa-aware version and then call the old version to do actual + * file descriptor polling. There's no actual need to chain to the + * old one; we could reimplement the same functionality from scratch, + * but since the default implementation does the right thing, why + * bother. + */ +static GPollFunc old_poll_func = NULL; + +/* Reference to the run loop of the main thread. (There is a unique + * CFRunLoop per thread.) + */ +static CFRunLoopRef main_thread_run_loop; + +/* Normally the Cocoa main loop maintains an NSAutoReleasePool and frees + * it on every iteration. Since we are replacing the main loop we have + * to provide this functionality ourself. We free and replace the + * auto-release pool in our sources prepare() function. + */ +static NSAutoreleasePool *autorelease_pool; + +/* Flag when we've called nextEventMatchingMask ourself; this triggers + * a run loop iteration, so we need to detect that and avoid triggering + * our "run the GLib main looop while the run loop is active machinery. + */ +static gboolean getting_events; + +/************************************************************ + ********* Select Thread ********* + ************************************************************/ + +/* The states in our state machine, see comments in select_thread_func() + * for descriptiions of each state + */ +typedef enum { + BEFORE_START, + WAITING, + POLLING_QUEUED, + POLLING_RESTART, + POLLING_DESCRIPTORS, +} SelectThreadState; + +static const char *const state_names[] = { + "BEFORE_START", + "WAITING", + "POLLING_QUEUED", + "POLLING_RESTART", + "POLLING_DESCRIPTORS" +}; + +static SelectThreadState select_thread_state = BEFORE_START; + +static pthread_t select_thread; +static pthread_mutex_t select_thread_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t select_thread_cond = PTHREAD_COND_INITIALIZER; + +#define SELECT_THREAD_LOCK() pthread_mutex_lock (&select_thread_mutex) +#define SELECT_THREAD_UNLOCK() pthread_mutex_unlock (&select_thread_mutex) +#define SELECT_THREAD_SIGNAL() pthread_cond_signal (&select_thread_cond) +#define SELECT_THREAD_WAIT() pthread_cond_wait (&select_thread_cond, &select_thread_mutex) + +/* These are the file descriptors that the select thread is currently + * polling. + */ +static GPollFD *current_pollfds; +static guint current_n_pollfds; + +/* These are the file descriptors that the select thread should pick + * up and start polling when it has a chance. + */ +static GPollFD *next_pollfds; +static guint next_n_pollfds; + +/* Pipe used to wake up the select thread */ +static gint select_thread_wakeup_pipe[2]; + +/* Run loop source used to wake up the main thread */ +static CFRunLoopSourceRef select_main_thread_source; + +/* Events */ +typedef enum { + CLUTTER_OSX_EVENT_SUBTYPE_EVENTLOOP +} ClutterOSXEventSubType; + +static void +select_thread_set_state (SelectThreadState new_state) +{ + gboolean old_state; + + if (select_thread_state == new_state) + return; + + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Select thread state: %s => %s\n", state_names[select_thread_state], state_names[new_state]); + + old_state = select_thread_state; + select_thread_state = new_state; + if (old_state == WAITING && new_state != WAITING) + SELECT_THREAD_SIGNAL (); +} + +static void +signal_main_thread (void) +{ + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Waking up main thread\n"); + + /* If we are in nextEventMatchingMask, then we need to make sure an + * event gets queued, otherwise it's enough to simply wake up the + * main thread run loop + */ + if (!run_loop_polling_async) + CFRunLoopSourceSignal (select_main_thread_source); + + if (CFRunLoopIsWaiting (main_thread_run_loop)) + CFRunLoopWakeUp (main_thread_run_loop); +} + +static void * +select_thread_func (void *arg) +{ + char c; + + SELECT_THREAD_LOCK (); + + while (TRUE) + { + switch (select_thread_state) + { + case BEFORE_START: + /* The select thread has not been started yet + */ + g_assert_not_reached (); + + case WAITING: + /* Waiting for a set of file descriptors to be submitted by the main thread + * + * => POLLING_QUEUED: main thread thread submits a set of file descriptors + */ + SELECT_THREAD_WAIT (); + break; + + case POLLING_QUEUED: + /* Waiting for a set of file descriptors to be submitted by the main thread + * + * => POLLING_DESCRIPTORS: select thread picks up the file descriptors to begin polling + */ + if (current_pollfds) + g_free (current_pollfds); + + current_pollfds = next_pollfds; + current_n_pollfds = next_n_pollfds; + + next_pollfds = NULL; + next_n_pollfds = 0; + + select_thread_set_state (POLLING_DESCRIPTORS); + break; + + case POLLING_RESTART: + /* Select thread is currently polling a set of file descriptors, main thread has + * began a new iteration with the same set of file descriptors. We don't want to + * wake the select thread up and wait for it to restart immediately, but to avoid + * a race (described below in select_thread_start_polling()) we need to recheck after + * polling completes. + * + * => POLLING_DESCRIPTORS: select completes, main thread rechecks by polling again + * => POLLING_QUEUED: main thread submits a new set of file descriptors to be polled + */ + select_thread_set_state (POLLING_DESCRIPTORS); + break; + + case POLLING_DESCRIPTORS: + /* In the process of polling the file descriptors + * + * => WAITING: polling completes when a file descriptor becomes active + * => POLLING_QUEUED: main thread submits a new set of file descriptors to be polled + * => POLLING_RESTART: main thread begins a new iteration with the same set file descriptors + */ + SELECT_THREAD_UNLOCK (); + old_poll_func (current_pollfds, current_n_pollfds, -1); + SELECT_THREAD_LOCK (); + + read (select_thread_wakeup_pipe[0], &c, 1); + + if (select_thread_state == POLLING_DESCRIPTORS) + { + signal_main_thread (); + select_thread_set_state (WAITING); + } + break; + } + } +} + +static void +got_fd_activity (void *info) +{ + NSEvent *event; + + /* Post a message so we'll break out of the message loop */ + event = [NSEvent otherEventWithType: NSApplicationDefined + location: NSZeroPoint + modifierFlags: 0 + timestamp: 0 + windowNumber: 0 + context: nil + subtype: CLUTTER_OSX_EVENT_SUBTYPE_EVENTLOOP + data1: 0 + data2: 0]; + + [NSApp postEvent:event atStart:YES]; +} + +static void +select_thread_start (void) +{ + g_return_if_fail (select_thread_state == BEFORE_START); + + pipe (select_thread_wakeup_pipe); + fcntl (select_thread_wakeup_pipe[0], F_SETFL, O_NONBLOCK); + + CFRunLoopSourceContext source_context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, got_fd_activity }; + select_main_thread_source = CFRunLoopSourceCreate (NULL, 0, &source_context); + + CFRunLoopAddSource (main_thread_run_loop, select_main_thread_source, kCFRunLoopCommonModes); + + select_thread_state = WAITING; + + while (TRUE) + { + if (pthread_create (&select_thread, NULL, select_thread_func, NULL) == 0) + break; + + g_warning ("Failed to create select thread, sleeping and trying again"); + sleep (1); + } +} + +#ifdef G_ENABLE_DEBUG +static void +dump_poll_result (GPollFD *ufds, + guint nfds) +{ + gint i; + + for (i = 0; i < nfds; i++) + { + if (ufds[i].fd >= 0 && ufds[i].revents) + { + g_print (" %d:", ufds[i].fd); + if (ufds[i].revents & G_IO_IN) + g_print (" in"); + if (ufds[i].revents & G_IO_OUT) + g_print (" out"); + if (ufds[i].revents & G_IO_PRI) + g_print (" pri"); + g_print ("\n"); + } + } +} +#endif + +gboolean +pollfds_equal (GPollFD *old_pollfds, + guint old_n_pollfds, + GPollFD *new_pollfds, + guint new_n_pollfds) +{ + gint i; + + if (old_n_pollfds != new_n_pollfds) + return FALSE; + + for (i = 0; i < old_n_pollfds; i++) + { + if (old_pollfds[i].fd != new_pollfds[i].fd || + old_pollfds[i].events != new_pollfds[i].events) + return FALSE; + } + + return TRUE; +} + +/* Begins a polling operation with the specified GPollFD array; the + * timeout is used only to tell if the polling operation is blocking + * or non-blocking. + * + * Return value: + * -1: No file descriptors ready, began asynchronous poll + * 0: No file descriptors ready, asynchronous poll not needed + * > 0: Number of file descriptors ready + */ +static gint +select_thread_start_poll (GPollFD *ufds, + guint nfds, gint timeout) +{ + gint n_ready; + gboolean have_new_pollfds = FALSE; + gint poll_fd_index = -1; + gint i; + + for (i = 0; i < nfds; i++) + if (ufds[i].fd == -1) + { + poll_fd_index = i; + break; + } + + if (nfds == 0 || + (nfds == 1 && poll_fd_index >= 0)) + { + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Nothing to poll\n"); + return 0; + } + + /* If we went immediately to an async poll, then we might decide to + * dispatch idle functions when higher priority file descriptor sources + * are ready to be dispatched. So we always need to first check + * check synchronously with a timeout of zero, and only when no + * sources are immediately ready, go to the asynchronous poll. + * + * Of course, if the timeout passed in is 0, then the synchronous + * check is sufficient and we never need to do the asynchronous poll. + */ + n_ready = old_poll_func (ufds, nfds, 0); + if (n_ready > 0 || timeout == 0) + { +#ifdef G_ENABLE_DEBUG + if (CLUTTER_HAS_DEBUG(EVENTLOOP) && n_ready > 0) + { + g_print ("EventLoop: Found ready file descriptors before waiting\n"); + dump_poll_result (ufds, nfds); + } +#endif + + return n_ready; + } + + SELECT_THREAD_LOCK (); + + if (select_thread_state == BEFORE_START) + { + select_thread_start (); + } + + if (select_thread_state == POLLING_QUEUED) + { + /* If the select thread hasn't picked up the set of file descriptors yet + * then we can simply replace an old stale set with a new set. + */ + if (!pollfds_equal (ufds, nfds, next_pollfds, next_n_pollfds - 1)) + { + g_free (next_pollfds); + next_pollfds = NULL; + next_n_pollfds = 0; + + have_new_pollfds = TRUE; + } + } + else if (select_thread_state == POLLING_RESTART || select_thread_state == POLLING_DESCRIPTORS) + { + /* If we are already in the process of polling the right set of file descriptors, + * there's no need for us to immediately force the select thread to stop polling + * and then restart again. And avoiding doing so increases the efficiency considerably + * in the common case where we have a set of basically inactive file descriptors that + * stay unchanged present as we process many events. + * + * However, we have to be careful that we don't hit the following race condition + * Select Thread Main Thread + * ----------------- --------------- + * Polling Completes + * Reads data or otherwise changes file descriptor state + * Checks if polling is current + * Does nothing (*) + * Releases lock + * Acquires lock + * Marks polling as complete + * Wakes main thread + * Receives old stale file descriptor state + * + * To avoid this, when the new set of poll descriptors is the same as the current + * one, we transition to the POLLING_RESTART stage at the point marked (*). When + * the select thread wakes up from the poll because a file descriptor is active, if + * the state is POLLING_RESTART it immediately begins polling same the file descriptor + * set again. This normally will just return the same set of active file descriptors + * as the first time, but in sequence described above will properly update the + * file descriptor state. + * + * Special case: this RESTART logic is not needed if the only FD is the internal GLib + * "wakeup pipe" that is presented when threads are initialized. + * + * P.S.: The harm in the above sequence is mostly that sources can be signalled + * as ready when they are no longer ready. This may prompt a blocking read + * from a file descriptor that hangs. + */ + if (!pollfds_equal (ufds, nfds, current_pollfds, current_n_pollfds - 1)) + have_new_pollfds = TRUE; + else + { + if (!((nfds == 1 && poll_fd_index < 0 && g_thread_supported ()) || + (nfds == 2 && poll_fd_index >= 0 && g_thread_supported ()))) + select_thread_set_state (POLLING_RESTART); + } + } + else + have_new_pollfds = TRUE; + + if (have_new_pollfds) + { + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Submitting a new set of file descriptor to the select thread\n"); + + g_assert (next_pollfds == NULL); + + next_n_pollfds = nfds + 1; + next_pollfds = g_new (GPollFD, nfds + 1); + memcpy (next_pollfds, ufds, nfds * sizeof (GPollFD)); + + next_pollfds[nfds].fd = select_thread_wakeup_pipe[0]; + next_pollfds[nfds].events = G_IO_IN; + + if (select_thread_state != POLLING_QUEUED && select_thread_state != WAITING) + { + if (select_thread_wakeup_pipe[1]) + { + char c = 'A'; + write (select_thread_wakeup_pipe[1], &c, 1); + } + } + + select_thread_set_state (POLLING_QUEUED); + } + + SELECT_THREAD_UNLOCK (); + + return -1; +} + +/* End an asynchronous polling operation started with + * select_thread_collect_poll(). This must be called if and only if + * select_thread_start_poll() return -1. The GPollFD array passed + * in must be identical to the one passed to select_thread_start_poll(). + * + * The results of the poll are written into the GPollFD array passed in. + * + * Return Value: number of file descriptors ready + */ +static int +select_thread_collect_poll (GPollFD *ufds, guint nfds) +{ + gint i; + gint n_ready = 0; + + SELECT_THREAD_LOCK (); + + if (select_thread_state == WAITING) /* The poll completed */ + { + for (i = 0; i < nfds; i++) + { + if (ufds[i].fd == -1) + continue; + + g_assert (ufds[i].fd == current_pollfds[i].fd); + g_assert (ufds[i].events == current_pollfds[i].events); + + if (current_pollfds[i].revents) + { + ufds[i].revents = current_pollfds[i].revents; + n_ready++; + } + } + +#ifdef G_ENABLE_DEBUG + if (_gdk_debug_flags & GDK_DEBUG_EVENTLOOP) + { + g_print ("EventLoop: Found ready file descriptors after waiting\n"); + dump_poll_result (ufds, nfds); + } +#endif + } + + SELECT_THREAD_UNLOCK (); + + return n_ready; +} + +/************************************************************ + ********* Main Loop Source ********* + ************************************************************/ + +gboolean +_clutter_osx_event_loop_check_pending (void) +{ + return current_events && current_events->head; +} + +NSEvent* +_clutter_osx_event_loop_get_pending (void) +{ + NSEvent *event = NULL; + + if (current_events) + event = g_queue_pop_tail (current_events); + + return event; +} + +void +_clutter_osx_event_loop_release_event (NSEvent *event) +{ + [event release]; +} + +static gboolean +clutter_event_prepare (GSource *source, + gint *timeout) +{ + gboolean retval; + + clutter_threads_enter (); + + *timeout = -1; + + retval = (clutter_events_pending () || _clutter_osx_event_loop_check_pending ()); + + clutter_threads_leave (); + + return retval; +} + +static gboolean +clutter_event_check (GSource *source) +{ + gboolean retval; + + clutter_threads_enter (); + + /* XXX: This check isn't right it won't handle a recursive GLib main + * loop run within an outer CFRunLoop run. Such loops will pile up + * memory. Fixing this requires setting a flag *only* when we call + * g_main_context_check() from within the run loop iteraton code, + * and also maintaining our own stack of run loops... allocating and + * releasing NSAutoReleasePools not properly nested with CFRunLoop + * runs seems to cause problems. + */ + if (current_loop_level == 0) + { + if (autorelease_pool) + [autorelease_pool release]; + autorelease_pool = [[NSAutoreleasePool alloc] init]; + } + + retval = (clutter_events_pending () || _clutter_osx_event_loop_check_pending ()); + + clutter_threads_leave (); + + return retval; +} + +static gboolean +clutter_event_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + NSEvent *nsevent; + ClutterEvent *event; + + clutter_threads_enter (); + + nsevent = _clutter_osx_event_loop_get_pending (); + if (nsevent) { + clutter_threads_leave (); + [NSApp sendEvent:nsevent]; + clutter_threads_enter (); + + _clutter_osx_event_loop_release_event (nsevent); + } + + event = clutter_event_get (); + + if (event) + { + /* forward the event into clutter for emission etc. */ + clutter_do_event (event); + clutter_event_free (event); + } + + clutter_threads_leave (); + + return TRUE; +} + +static GSourceFuncs event_funcs = { + clutter_event_prepare, + clutter_event_check, + clutter_event_dispatch, + NULL +}; + +/************************************************************ + ********* Our Poll Function ********* + ************************************************************/ + +static gint +poll_func (GPollFD *ufds, + guint nfds, + gint timeout_) +{ + NSEvent *event; + NSDate *limit_date; + gint n_ready; + + n_ready = select_thread_start_poll (ufds, nfds, timeout_); + if (n_ready > 0) + timeout_ = 0; + + if (timeout_ == -1) + limit_date = [NSDate distantFuture]; + else if (timeout_ == 0) + limit_date = [NSDate distantPast]; + else + limit_date = [NSDate dateWithTimeIntervalSinceNow:timeout_/1000.0]; + + getting_events = TRUE; + event = [NSApp nextEventMatchingMask: NSAnyEventMask + untilDate: limit_date + inMode: NSDefaultRunLoopMode + dequeue: YES]; + getting_events = FALSE; + + if (n_ready < 0) + n_ready = select_thread_collect_poll (ufds, nfds); + + if (event && + [event type] == NSApplicationDefined && + [event subtype] == CLUTTER_OSX_EVENT_SUBTYPE_EVENTLOOP) + { + /* Just used to wake us up; if an event and a FD arrived at the same + * time; could have come from a previous iteration in some cases, + * but the spurious wake up is harmless if a little inefficient. + */ + event = NULL; + } + + if (event) + { + if (!current_events) + current_events = g_queue_new (); + g_queue_push_head (current_events, [event retain]); + } + + return n_ready; +} + +/************************************************************ + ********* Running the main loop out of CFRunLoop ********* + ************************************************************/ + +/* Wrapper around g_main_context_query() that handles reallocating + * run_loop_pollfds up to the proper size + */ +static gint +query_main_context (GMainContext *context, + int max_priority, + int *timeout) +{ + gint nfds; + + if (!run_loop_pollfds) + { + run_loop_pollfds_size = RUN_LOOP_POLLFDS_INITIAL_SIZE; + run_loop_pollfds = g_new (GPollFD, run_loop_pollfds_size); + } + + while ((nfds = g_main_context_query (context, max_priority, timeout, + run_loop_pollfds, + run_loop_pollfds_size)) > run_loop_pollfds_size) + { + g_free (run_loop_pollfds); + run_loop_pollfds_size = nfds; + run_loop_pollfds = g_new (GPollFD, nfds); + } + + return nfds; +} + +static void +run_loop_entry (void) +{ + current_loop_level++; + + if (acquired_loop_level == -1) + { + if (g_main_context_acquire (NULL)) + { + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Beginning tracking run loop activity\n"); + acquired_loop_level = current_loop_level; + } + else + { + /* If we fail to acquire the main context, that means someone is iterating + * the main context in a different thread; we simply wait until this loop + * exits and then try again at next entry. In general, iterating the loop + * from a different thread is rare: it is only possible when GDK threading + * is initialized and is not frequently used even then. So, we hope that + * having GLib main loop iteration blocked in the combination of that and + * a native modal operation is a minimal problem. We could imagine using a + * thread that does g_main_context_wait() and then wakes us back up, but + * the gain doesn't seem worth the complexity. + */ + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Can't acquire main loop; skipping tracking run loop activity\n"); + } + } +} + +static void +run_loop_before_timers (void) +{ +} + +static void +run_loop_before_sources (void) +{ + GMainContext *context = g_main_context_default (); + gint max_priority; + gint nfds; + + /* Before we let the CFRunLoop process sources, we want to check if there + * are any pending GLib main loop sources more urgent than + * G_PRIORITY_DEFAULT that need to be dispatched. (We consider all activity + * from the CFRunLoop to have a priority of G_PRIORITY_DEFAULT.) If no + * sources are processed by the CFRunLoop, then processing will continue + * on to the BeforeWaiting stage where we check for lower priority sources. + */ + + g_main_context_prepare (context, &max_priority); + max_priority = MIN (max_priority, G_PRIORITY_DEFAULT); + + /* We ignore the timeout that query_main_context () returns since we'll + * always query again before waiting. + */ + nfds = query_main_context (context, max_priority, NULL); + + if (nfds) + old_poll_func (run_loop_pollfds, nfds, 0); + + if (g_main_context_check (context, max_priority, run_loop_pollfds, nfds)) + { + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Dispatching high priority sources\n"); + g_main_context_dispatch (context); + } +} + +static void +dummy_timer_callback (CFRunLoopTimerRef timer, + void *info) +{ + /* Nothing; won't normally even be called */ +} + +static void +run_loop_before_waiting (void) +{ + GMainContext *context = g_main_context_default (); + gint timeout; + gint n_ready; + + /* At this point, the CFRunLoop is ready to wait. We start a GMain loop + * iteration by calling the check() and query() stages. We start a + * poll, and if it doesn't complete immediately we let the run loop + * go ahead and sleep. Before doing that, if there was a timeout from + * GLib, we set up a CFRunLoopTimer to wake us up. + */ + + g_main_context_prepare (context, &run_loop_max_priority); + + run_loop_n_pollfds = query_main_context (context, run_loop_max_priority, &timeout); + + n_ready = select_thread_start_poll (run_loop_pollfds, run_loop_n_pollfds, timeout); + + if (n_ready > 0 || timeout == 0) + { + /* We have stuff to do, no sleeping allowed! */ + CFRunLoopWakeUp (main_thread_run_loop); + } + else if (timeout > 0) + { + /* We need to get the run loop to break out of it's wait when our timeout + * expires. We do this by adding a dummy timer that we'll remove immediately + * after the wait wakes up. + */ + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Adding timer to wake us up in %d milliseconds\n", timeout); + + run_loop_timer = CFRunLoopTimerCreate (NULL, /* allocator */ + CFAbsoluteTimeGetCurrent () + timeout / 1000., + 0, /* interval (0=does not repeat) */ + 0, /* flags */ + 0, /* order (priority) */ + dummy_timer_callback, + NULL); + + CFRunLoopAddTimer (main_thread_run_loop, run_loop_timer, kCFRunLoopCommonModes); + } + + run_loop_polling_async = n_ready < 0; +} + +static void +run_loop_after_waiting (void) +{ + GMainContext *context = g_main_context_default (); + + /* After sleeping, we finish of the GMain loop iteratin started in before_waiting() + * by doing the check() and dispatch() stages. + */ + + if (run_loop_timer) + { + CFRunLoopRemoveTimer (main_thread_run_loop, run_loop_timer, kCFRunLoopCommonModes); + CFRelease (run_loop_timer); + run_loop_timer = NULL; + } + + if (run_loop_polling_async) + { + select_thread_collect_poll (run_loop_pollfds, run_loop_n_pollfds); + run_loop_polling_async = FALSE; + } + + if (g_main_context_check (context, run_loop_max_priority, run_loop_pollfds, run_loop_n_pollfds)) + { + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Dispatching after waiting\n"); + g_main_context_dispatch (context); + } +} + +static void +run_loop_exit (void) +{ + g_return_if_fail (current_loop_level > 0); + + if (current_loop_level == acquired_loop_level) + { + g_main_context_release (NULL); + acquired_loop_level = -1; + CLUTTER_NOTE (EVENTLOOP, "EventLoop: Ended tracking run loop activity\n"); + } + + current_loop_level--; +} + +static void +run_loop_observer_callback (CFRunLoopObserverRef observer, + CFRunLoopActivity activity, + void *info) +{ + if (getting_events) /* Activity we triggered */ + return; + + switch (activity) + { + case kCFRunLoopEntry: + run_loop_entry (); + break; + case kCFRunLoopBeforeTimers: + run_loop_before_timers (); + break; + case kCFRunLoopBeforeSources: + run_loop_before_sources (); + break; + case kCFRunLoopBeforeWaiting: + run_loop_before_waiting (); + break; + case kCFRunLoopAfterWaiting: + run_loop_after_waiting (); + break; + case kCFRunLoopExit: + run_loop_exit (); + break; + default: + break; + } +} + +/************************************************************/ + +void +_clutter_osx_event_loop_init (void) +{ + GSource *source; + CFRunLoopObserverRef observer; + + g_assert (old_poll_func == NULL); + + /* Hook into the GLib main loop */ + + event_poll_fd.events = G_IO_IN; + event_poll_fd.fd = -1; + + source = g_source_new (&event_funcs, sizeof (GSource)); + g_source_set_name (source, "Clutter OS X event source"); + g_source_add_poll (source, &event_poll_fd); + g_source_set_priority (source, CLUTTER_PRIORITY_EVENTS); + g_source_set_can_recurse (source, TRUE); + g_source_attach (source, NULL); + + old_poll_func = g_main_context_get_poll_func (NULL); + g_main_context_set_poll_func (NULL, poll_func); + + /* Hook into the the CFRunLoop for the main thread */ + + main_thread_run_loop = CFRunLoopGetCurrent (); + + observer = CFRunLoopObserverCreate (NULL, /* default allocator */ + kCFRunLoopAllActivities, + true, /* repeats: not one-shot */ + 0, /* order (priority) */ + run_loop_observer_callback, + NULL); + + CFRunLoopAddObserver (main_thread_run_loop, observer, kCFRunLoopCommonModes); + + /* Initialize our autorelease pool */ + + autorelease_pool = [[NSAutoreleasePool alloc] init]; +} diff --git a/clutter/osx/clutter-event-loop-osx.h b/clutter/osx/clutter-event-loop-osx.h new file mode 100644 index 000000000..1f7dd5698 --- /dev/null +++ b/clutter/osx/clutter-event-loop-osx.h @@ -0,0 +1,32 @@ +/* Clutter - An OpenGL based 'interactive canvas' library. + * OSX backend - event loop + * + * Copyright (C) 2005-2007 Imendio AB + * Copyright (C) 2011 Crystalnix + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * + */ +#ifndef __CLUTTER_EVENT_LOOP_OSX_H__ +#define __CLUTTER_EVENT_LOOP_OSX_H__ + +G_BEGIN_DECLS + +/* Initialization */ +void _clutter_osx_event_loop_init (void); + +G_END_DECLS + +#endif diff --git a/clutter/osx/clutter-event-osx.c b/clutter/osx/clutter-event-osx.c index e940fc4ec..859d44c91 100644 --- a/clutter/osx/clutter-event-osx.c +++ b/clutter/osx/clutter-event-osx.c @@ -3,6 +3,7 @@ * * Copyright (C) 2007-2008 Tommi Komulainen * Copyright (C) 2007 OpenedHand Ltd. + * Copyright (C) 2011 Crystalnix * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -31,11 +32,9 @@ #include #include -/* Overriding the poll function because the events are not delivered over file - * descriptors and setting up a GSource would just introduce polling. - */ +#include "clutter-event-loop-osx.h" -static GPollFunc old_poll_func = NULL; +#define WHEEL_DELTA 1 /*************************************************************************/ @interface NSEvent (Clutter) @@ -206,9 +205,74 @@ static GPollFunc old_poll_func = NULL; @end /*************************************************************************/ + +static void +take_and_queue_event (ClutterEvent *event) +{ + ClutterMainContext *clutter_context; + + clutter_context = _clutter_context_get_default (); + + /* The event is added directly to the queue instead of using + clutter_event_put so that it can avoid a copy. This takes + ownership of the event */ + g_queue_push_head (clutter_context->events_queue, event); +} + +static void +process_scroll_event(ClutterEvent *event, gboolean isVertical) +{ + ClutterStageWindow *impl; + ClutterStageOSX *stage_osx; + + impl = _clutter_stage_get_window (event->any.stage); + stage_osx = CLUTTER_STAGE_OSX (impl); + + gfloat *scroll_pos = isVertical ? &(stage_osx->scroll_pos_y) : &(stage_osx->scroll_pos_x); + + while (abs (*scroll_pos) >= WHEEL_DELTA) + { + ClutterEvent *event_gen = clutter_event_new (CLUTTER_SCROLL); + + event_gen->scroll.time = event->any.time; + event_gen->scroll.modifier_state = event->scroll.modifier_state; + event_gen->any.stage = event->any.stage; + + event_gen->scroll.x = event->scroll.x; + event_gen->scroll.y = event->scroll.y; + + if (*scroll_pos > 0) + { + event_gen->scroll.direction = isVertical ? CLUTTER_SCROLL_UP : CLUTTER_SCROLL_RIGHT; + *scroll_pos -= WHEEL_DELTA; + } + else + { + event_gen->scroll.direction = isVertical ? CLUTTER_SCROLL_DOWN : CLUTTER_SCROLL_LEFT; + *scroll_pos += WHEEL_DELTA; + } + + take_and_queue_event (event_gen); + + CLUTTER_NOTE (EVENT, "scroll %s at %f,%f", + (event_gen->scroll.direction == CLUTTER_SCROLL_UP) ? "UP" : + ( + (event_gen->scroll.direction == CLUTTER_SCROLL_DOWN) ? "DOWN" : + ( + (event_gen->scroll.direction == CLUTTER_SCROLL_RIGHT) ? "RIGHT" : "LEFT")), + (float)event->scroll.x, (float)event->scroll.y); + } +} + static gboolean clutter_event_osx_translate (NSEvent *nsevent, ClutterEvent *event) -{ +{ + ClutterStageWindow *impl; + ClutterStageOSX *stage_osx; + + impl = _clutter_stage_get_window (event->any.stage); + stage_osx = CLUTTER_STAGE_OSX (impl); + event->any.time = [nsevent clutterTime]; switch ([nsevent type]) @@ -250,6 +314,18 @@ clutter_event_osx_translate (NSEvent *nsevent, ClutterEvent *event) (float)event->button.x, (float)event->button.y); return TRUE; + case NSScrollWheel: + stage_osx->scroll_pos_x += [nsevent deltaX]; + stage_osx->scroll_pos_y += [nsevent deltaY]; + + [nsevent clutterX:&(event->scroll.x) y:&(event->scroll.y)]; + event->scroll.modifier_state = [nsevent clutterModifierState]; + + process_scroll_event(event, TRUE); + process_scroll_event(event, FALSE); + + return FALSE; + case NSKeyDown: event->type = CLUTTER_KEY_PRESS; /* fall through */ @@ -292,147 +368,14 @@ _clutter_event_osx_put (NSEvent *nsevent, ClutterStage *wrapper) } } -typedef struct { - CFSocketRef sock; - CFRunLoopSourceRef source; - - gushort revents; -} SocketInfo; - -static void -socket_activity_cb (CFSocketRef sock, - CFSocketCallBackType cbtype, - CFDataRef address, - const void *data, - void *info) -{ - SocketInfo *si = info; - - if (cbtype & kCFSocketReadCallBack) - si->revents |= G_IO_IN; - if (cbtype & kCFSocketWriteCallBack) - si->revents |= G_IO_OUT; -} - -static gint -clutter_event_osx_poll_func (GPollFD *ufds, guint nfds, gint timeout) -{ - NSDate *until_date; - NSEvent *nsevent; - SocketInfo *sockets = NULL; - gint n_active = 0; - - CLUTTER_OSX_POOL_ALLOC(); - - if (timeout == -1) - until_date = [NSDate distantFuture]; - else if (timeout == 0) - until_date = [NSDate distantPast]; - else - until_date = [NSDate dateWithTimeIntervalSinceNow:timeout/1000.0]; - - /* File descriptors appear to be similar enough to sockets so that they can - * be used in CFRunLoopSource. - * - * We could also launch a thread to call old_poll_func and signal the main - * thread. No idea which way is better. - */ - if (nfds > 0) - { - CFRunLoopRef run_loop; - - run_loop = [[NSRunLoop currentRunLoop] getCFRunLoop]; - sockets = g_new (SocketInfo, nfds); - - int i; - for (i = 0; i < nfds; i++) - { - SocketInfo *si = &sockets[i]; - CFSocketCallBackType cbtype; - - cbtype = 0; - if (ufds[i].events & G_IO_IN) - cbtype |= kCFSocketReadCallBack; - if (ufds[i].events & G_IO_OUT) - cbtype |= kCFSocketWriteCallBack; - /* FIXME: how to handle G_IO_HUP and G_IO_ERR? */ - - const CFSocketContext ctxt = { - 0, si, NULL, NULL, NULL - }; - si->sock = CFSocketCreateWithNative (NULL, ufds[i].fd, cbtype, socket_activity_cb, &ctxt); - si->source = CFSocketCreateRunLoopSource (NULL, si->sock, 0); - si->revents = 0; - - CFRunLoopAddSource (run_loop, si->source, kCFRunLoopCommonModes); - } - } - - nsevent = [NSApp nextEventMatchingMask: NSAnyEventMask - untilDate: until_date - inMode: NSDefaultRunLoopMode - dequeue: YES]; - - /* Push the events to NSApplication which will do some magic(?) and forward - * interesting events to our view. While we could do event translation here - * we'd also need to filter out clicks on titlebar, and perhaps do special - * handling for the first click (couldn't figure it out - always ended up - * missing a screen refresh) and maybe other things. - */ - [NSApp sendEvent:nsevent]; - - if (nfds > 0) - { - int i; - for (i = 0; i < nfds; i++) - { - SocketInfo *si = &sockets[i]; - - if ((ufds[i].revents = si->revents) != 0) - n_active++; - - /* Invalidating the source also removes it from run loop and - * guarantees the callback is never called again. - * CFRunLoopRemoveSource removes the source from the loop, but might - * still call the callback which would be badly timed. - */ - CFRunLoopSourceInvalidate (si->source); - CFRelease (si->source); - CFRelease (si->sock); - } - - g_free (sockets); - } - - /* FIXME this could result in infinite loop */ - ClutterEvent *event = clutter_event_get (); - while (event) - { - clutter_do_event (event); - clutter_event_free (event); - event = clutter_event_get (); - } - - CLUTTER_OSX_POOL_RELEASE(); - - return n_active; -} - void _clutter_events_osx_init (void) { - g_assert (old_poll_func == NULL); - - old_poll_func = g_main_context_get_poll_func (NULL); - g_main_context_set_poll_func (NULL, clutter_event_osx_poll_func); + _clutter_osx_event_loop_init (); } void _clutter_events_osx_uninit (void) { - if (old_poll_func) - { - g_main_context_set_poll_func (NULL, old_poll_func); - old_poll_func = NULL; - } + g_assert_not_reached (); } diff --git a/clutter/osx/clutter-stage-osx.h b/clutter/osx/clutter-stage-osx.h index 923a95b0c..7344eb6fa 100644 --- a/clutter/osx/clutter-stage-osx.h +++ b/clutter/osx/clutter-stage-osx.h @@ -67,6 +67,9 @@ struct _ClutterStageOSX ClutterStageState stage_state; gboolean acceptFocus; + + gfloat scroll_pos_x; + gfloat scroll_pos_y; }; struct _ClutterStageOSXClass