aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2020-03-16 20:32:28 +0100
committerDavid Robillard <d@drobilla.net>2020-03-16 21:21:15 +0100
commita54361853bdfa08437c2858e603ce6202fb341b2 (patch)
tree5e86a841cc14dd4ca04d7b7b54b05f9c37e6feaf
parent7de08cd2a57d26f546060183944632da71f643f2 (diff)
Add timer events
-rw-r--r--pugl/detail/mac.h1
-rw-r--r--pugl/detail/mac.m44
-rw-r--r--pugl/detail/win.c22
-rw-r--r--pugl/detail/x11.c139
-rw-r--r--pugl/detail/x11.h11
-rw-r--r--pugl/pugl.h61
-rw-r--r--test/test_timer.c158
-rw-r--r--test/test_utils.h2
-rw-r--r--wscript15
9 files changed, 449 insertions, 4 deletions
diff --git a/pugl/detail/mac.h b/pugl/detail/mac.h
index adeebe9..d167f76 100644
--- a/pugl/detail/mac.h
+++ b/pugl/detail/mac.h
@@ -33,6 +33,7 @@
NSMutableAttributedString* markedText;
NSTimer* timer;
NSTimer* urgentTimer;
+ NSMutableDictionary* userTimers;
bool reshaped;
}
diff --git a/pugl/detail/mac.m b/pugl/detail/mac.m
index 23fad7b..1c81abe 100644
--- a/pugl/detail/mac.m
+++ b/pugl/detail/mac.m
@@ -640,6 +640,14 @@ handleCrossing(PuglWrapperView* view, NSEvent* event, const PuglEventType type)
[puglview->world->impl->app requestUserAttention:NSInformationalRequest];
}
+- (void) timerTick:(NSTimer*)userTimer
+{
+ const NSNumber* userInfo = userTimer.userInfo;
+ const PuglEventTimer ev = {PUGL_TIMER, 0, userInfo.unsignedLongValue};
+
+ puglDispatchEvent(puglview, (const PuglEvent*)&ev);
+}
+
- (void) viewDidEndLiveResize
{
[super viewDidEndLiveResize];
@@ -763,6 +771,7 @@ puglCreateWindow(PuglView* view, const char* title)
// Create wrapper view to handle input
impl->wrapperView = [PuglWrapperView alloc];
impl->wrapperView->puglview = view;
+ impl->wrapperView->userTimers = [[NSMutableDictionary alloc] init];
impl->wrapperView->markedText = [[NSMutableAttributedString alloc] init];
[impl->wrapperView setAutoresizesSubviews:YES];
[impl->wrapperView initWithFrame:
@@ -920,6 +929,41 @@ puglRequestAttention(PuglView* view)
return PUGL_SUCCESS;
}
+PuglStatus
+puglStartTimer(PuglView* view, uintptr_t id, double timeout)
+{
+ puglStopTimer(view, id);
+
+ NSNumber* idNumber = [NSNumber numberWithUnsignedLong:id];
+
+ NSTimer* timer = [NSTimer timerWithTimeInterval:timeout
+ target:view->impl->wrapperView
+ selector:@selector(timerTick:)
+ userInfo:idNumber
+ repeats:YES];
+
+ [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
+
+ view->impl->wrapperView->userTimers[idNumber] = timer;
+
+ return PUGL_SUCCESS;
+}
+
+PuglStatus
+puglStopTimer(PuglView* view, uintptr_t id)
+{
+ NSNumber* idNumber = [NSNumber numberWithUnsignedLong:id];
+ NSTimer* timer = view->impl->wrapperView->userTimers[idNumber];
+
+ if (timer) {
+ [view->impl->wrapperView->userTimers removeObjectForKey:timer];
+ [timer invalidate];
+ return PUGL_SUCCESS;
+ }
+
+ return PUGL_UNKNOWN_ERROR;
+}
+
PuglStatus puglSendEvent(PuglView* view, const PuglEvent* event)
{
if (event->type == PUGL_CLIENT) {
diff --git a/pugl/detail/win.c b/pugl/detail/win.c
index cb4dfad..971ecdd 100644
--- a/pugl/detail/win.c
+++ b/pugl/detail/win.c
@@ -51,6 +51,7 @@
#define PUGL_LOCAL_CLIENT_MSG (WM_USER + 52)
#define PUGL_RESIZE_TIMER_ID 9461
#define PUGL_URGENT_TIMER_ID 9462
+#define PUGL_USER_TIMER_MIN 9470
typedef BOOL (WINAPI *PFN_SetProcessDPIAware)(void);
@@ -589,6 +590,9 @@ handleMessage(PuglView* view, UINT message, WPARAM wParam, LPARAM lParam)
RDW_INVALIDATE|RDW_ALLCHILDREN|RDW_INTERNALPAINT);
} else if (wParam == PUGL_URGENT_TIMER_ID) {
FlashWindow(view->impl->hwnd, TRUE);
+ } else if (wParam >= PUGL_USER_TIMER_MIN) {
+ const PuglEventTimer ev = {PUGL_TIMER, 0, wParam - PUGL_USER_TIMER_MIN};
+ puglDispatchEvent(view, (const PuglEvent*)&ev);
}
break;
case WM_EXITSIZEMOVE:
@@ -742,6 +746,24 @@ puglRequestAttention(PuglView* view)
}
PuglStatus
+puglStartTimer(PuglView* view, uintptr_t id, double timeout)
+{
+ const UINT msec = (UINT)floor(timeout * 1000.0);
+
+ return (SetTimer(view->impl->hwnd, PUGL_USER_TIMER_MIN + id, msec, NULL)
+ ? PUGL_SUCCESS
+ : PUGL_UNKNOWN_ERROR);
+}
+
+PuglStatus
+puglStopTimer(PuglView* view, uintptr_t id)
+{
+ return (KillTimer(view->impl->hwnd, PUGL_USER_TIMER_MIN + id)
+ ? PUGL_SUCCESS
+ : PUGL_UNKNOWN_ERROR);
+}
+
+PuglStatus
puglSendEvent(PuglView* view, const PuglEvent* event)
{
if (event->type == PUGL_CLIENT) {
diff --git a/pugl/detail/x11.c b/pugl/detail/x11.c
index 3684e46..9613dfb 100644
--- a/pugl/detail/x11.c
+++ b/pugl/detail/x11.c
@@ -35,6 +35,10 @@
#include <X11/Xutil.h>
#include <X11/keysym.h>
+#ifdef HAVE_XSYNC
+# include <X11/extensions/sync.h>
+#endif
+
#include <sys/select.h>
#include <sys/time.h>
@@ -65,6 +69,37 @@ static const long eventMask =
EnterWindowMask | LeaveWindowMask | PointerMotionMask |
ButtonPressMask | ButtonReleaseMask | KeyPressMask | KeyReleaseMask);
+static bool
+puglInitXSync(PuglWorldInternals* impl)
+{
+#ifdef HAVE_XSYNC
+ int syncMajor;
+ int syncMinor;
+ int errorBase;
+ XSyncSystemCounter* counters;
+ int numCounters;
+
+ if (XSyncQueryExtension(impl->display, &impl->syncEventBase, &errorBase) &&
+ XSyncInitialize(impl->display, &syncMajor, &syncMinor) &&
+ (counters = XSyncListSystemCounters(impl->display, &numCounters))) {
+
+ for (int n = 0; n < numCounters; ++n) {
+ if (!strcmp(counters[n].name, "SERVERTIME")) {
+ impl->serverTimeCounter = counters[n].counter;
+ impl->syncSupported = true;
+ break;
+ }
+ }
+
+ XSyncFreeSystemCounterList(counters);
+ }
+#else
+ (void)impl;
+#endif
+
+ return false;
+}
+
PuglWorldInternals*
puglInitWorldInternals(PuglWorldType type, PuglWorldFlags flags)
{
@@ -100,6 +135,7 @@ puglInitWorldInternals(PuglWorldType type, PuglWorldFlags flags)
impl->xim = XOpenIM(display, NULL, NULL, NULL);
}
+ puglInitXSync(impl);
XFlush(display);
return impl;
@@ -301,6 +337,7 @@ puglFreeWorldInternals(PuglWorld* world)
XCloseIM(world->impl->xim);
}
XCloseDisplay(world->impl->display);
+ free(world->impl->timers);
free(world->impl);
}
@@ -588,6 +625,80 @@ puglRequestAttention(PuglView* view)
return PUGL_SUCCESS;
}
+PuglStatus
+puglStartTimer(PuglView* view, uintptr_t id, double timeout)
+{
+#ifdef HAVE_XSYNC
+ if (view->world->impl->syncSupported) {
+ XSyncValue value;
+ XSyncIntToValue(&value, (int)floor(timeout * 1000.0));
+
+ PuglWorldInternals* w = view->world->impl;
+ Display* const display = w->display;
+ const XSyncCounter counter = w->serverTimeCounter;
+ const XSyncTrigger trigger = {counter, XSyncRelative, value, 0};
+ XSyncAlarmAttributes attr = {trigger, value, True, XSyncAlarmActive};
+ const XSyncAlarm alarm = XSyncCreateAlarm(display, 0x17, &attr);
+ const PuglTimer timer = {alarm, view, id};
+
+ if (alarm != None) {
+ for (size_t i = 0; i < w->numTimers; ++i) {
+ if (w->timers[i].view == view && w->timers[i].id == id) {
+ // Replace existing timer
+ XSyncDestroyAlarm(w->display, w->timers[i].alarm);
+ w->timers[i] = timer;
+ return PUGL_SUCCESS;
+ }
+ }
+
+ // Add new timer
+ const size_t size = ++w->numTimers * sizeof(timer);
+ w->timers = (PuglTimer*)realloc(w->timers, size);
+ w->timers[w->numTimers - 1] = timer;
+ return PUGL_SUCCESS;
+ }
+ }
+#else
+ (void)view;
+ (void)id;
+ (void)timeout;
+#endif
+
+ return PUGL_FAILURE;
+}
+
+PuglStatus
+puglStopTimer(PuglView* view, uintptr_t id)
+{
+#ifdef HAVE_XSYNC
+ PuglWorldInternals* w = view->world->impl;
+
+ for (size_t i = 0; i < w->numTimers; ++i) {
+ if (w->timers[i].view == view && w->timers[i].id == id) {
+ XSyncDestroyAlarm(w->display, w->timers[i].alarm);
+
+ if (i == w->numTimers - 1) {
+ memset(&w->timers[i], 0, sizeof(PuglTimer));
+ } else {
+ memmove(w->timers + i,
+ w->timers + i + 1,
+ sizeof(PuglTimer) * (w->numTimers - i - 1));
+
+ memset(&w->timers[i], 0, sizeof(PuglTimer));
+ }
+
+ --w->numTimers;
+ return PUGL_SUCCESS;
+ }
+ }
+#else
+ (void)view;
+ (void)id;
+#endif
+
+ return PUGL_FAILURE;
+}
+
static XEvent
puglEventToX(PuglView* view, const PuglEvent* event)
{
@@ -765,6 +876,30 @@ flushExposures(PuglWorld* world)
}
}
+static bool
+handleTimerEvent(PuglWorld* world, XEvent xevent)
+{
+#ifdef HAVE_XSYNC
+ if (xevent.type == world->impl->syncEventBase + XSyncAlarmNotify) {
+ XSyncAlarmNotifyEvent* notify = ((XSyncAlarmNotifyEvent*)&xevent);
+
+ for (size_t i = 0; i < world->impl->numTimers; ++i) {
+ if (world->impl->timers[i].alarm == notify->alarm) {
+ const PuglEventTimer ev = {PUGL_TIMER, 0, world->impl->timers[i].id};
+ puglDispatchEvent(world->impl->timers[i].view, (const PuglEvent*)&ev);
+ }
+ }
+
+ return true;
+ }
+#else
+ (void)world;
+ (void)xevent;
+#endif
+
+ return false;
+}
+
static PuglStatus
puglDispatchX11Events(PuglWorld* world)
{
@@ -779,6 +914,10 @@ puglDispatchX11Events(PuglWorld* world)
XEvent xevent;
XNextEvent(display, &xevent);
+ if (handleTimerEvent(world, xevent)) {
+ continue;
+ }
+
PuglView* view = puglFindView(world, xevent.xany.window);
if (!view) {
continue;
diff --git a/pugl/detail/x11.h b/pugl/detail/x11.h
index fe8ce01..6f86a90 100644
--- a/pugl/detail/x11.h
+++ b/pugl/detail/x11.h
@@ -38,10 +38,21 @@ typedef struct {
Atom NET_WM_STATE_DEMANDS_ATTENTION;
} PuglX11Atoms;
+typedef struct {
+ XID alarm;
+ PuglView* view;
+ uint64_t id;
+} PuglTimer;
+
struct PuglWorldInternalsImpl {
Display* display;
PuglX11Atoms atoms;
XIM xim;
+ PuglTimer* timers;
+ size_t numTimers;
+ XID serverTimeCounter;
+ int syncEventBase;
+ bool syncSupported;
bool dispatchingEvents;
};
diff --git a/pugl/pugl.h b/pugl/pugl.h
index a796e56..8e20f35 100644
--- a/pugl/pugl.h
+++ b/pugl/pugl.h
@@ -185,7 +185,8 @@ typedef enum {
PUGL_SCROLL, ///< Scrolled, a #PuglEventScroll
PUGL_FOCUS_IN, ///< Keyboard focus entered view, a #PuglEventFocus
PUGL_FOCUS_OUT, ///< Keyboard focus left view, a #PuglEventFocus
- PUGL_CLIENT ///< Custom client message, a #PuglEventClient
+ PUGL_CLIENT, ///< Custom client message, a #PuglEventClient
+ PUGL_TIMER ///< Timer triggered, a #PuglEventTimer
} PuglEventType;
/**
@@ -405,6 +406,22 @@ typedef struct {
} PuglEventClient;
/**
+ Timer event.
+
+ This event is sent at the regular interval specified in the call to
+ puglStartTimer() that activated it.
+
+ The #id is the application-specific ID given to puglStartTimer() which
+ distinguishes this timer from others. It should always be checked in the
+ event handler, even in applications that register only one timer.
+*/
+typedef struct {
+ PuglEventType type; ///< #PUGL_TIMER
+ PuglEventFlags flags; ///< Bitwise OR of #PuglEventFlag values
+ uintptr_t id; ///< Timer ID
+} PuglEventTimer;
+
+/**
View event.
This is a union of all event types. The #type must be checked to determine
@@ -428,6 +445,7 @@ typedef union {
PuglEventScroll scroll; ///< #PUGL_SCROLL
PuglEventFocus focus; ///< #PUGL_FOCUS_IN, #PUGL_FOCUS_OUT
PuglEventClient client; ///< #PUGL_CLIENT
+ PuglEventTimer timer; ///< #PUGL_TIMER
} PuglEvent;
/**
@@ -977,7 +995,7 @@ puglPostRedisplayRect(PuglView* view, PuglRect rect);
@}
@anchor interaction
@name Interaction
- Functions for interacting with the user.
+ Functions for interacting with the user and window system.
@{
*/
@@ -1035,6 +1053,45 @@ PUGL_API PuglStatus
puglRequestAttention(PuglView* view);
/**
+ Activate a repeating timer event.
+
+ This starts a timer which will send a #PuglEventTimer to `view` every
+ `timeout` seconds. This can be used to perform some action in a view at a
+ regular interval with relatively low frequency. Note that the frequency of
+ timer events may be limited by how often puglUpdate() is called.
+
+ If the given timer already exists, it is replaced.
+
+ @param view The view to begin seding #PUGL_TIMER events to.
+
+ @param id The identifier for this timer. This is an application-specific ID
+ that should be a low number, typically the value of a constant or `enum`
+ that starts from 0. There is a platform-specific limit to the number of
+ supported timers, and overhead associated with each, so applications should
+ create only a few timers and perform several tasks in one if necessary.
+
+ @param timeout The period, in seconds, of this timer. This is not
+ guaranteed to have a resolution better than 10ms (the maximum timer
+ resolution on Windows) and may be rounded up if it is too short. On X11 and
+ MacOS, a resolution of about 1ms can usually be relied on.
+
+ @return #PUGL_SUCCESS, #PUGL_FAILURE if timers are not supported on this
+ system, or an error code.
+*/
+PUGL_API PuglStatus
+puglStartTimer(PuglView* view, uintptr_t id, double timeout);
+
+/**
+ Stop an active timer.
+
+ @param view The view that the timer is set for.
+ @param id The ID previously passed to puglStartTimer().
+ @return #PUGL_SUCCESS, or #PUGL_FAILURE if no such timer was found.
+*/
+PUGL_API PuglStatus
+puglStopTimer(PuglView* view, uintptr_t id);
+
+/**
Send an event to a view via the window system.
If supported, the event will be delivered to the view via the event loop
diff --git a/test/test_timer.c b/test/test_timer.c
new file mode 100644
index 0000000..341b4f3
--- /dev/null
+++ b/test/test_timer.c
@@ -0,0 +1,158 @@
+/*
+ Copyright 2020 David Robillard <http://drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+/*
+ Tests that update events are received and than redisplays they trigger happen
+ immediately in the same event loop iteration.
+*/
+
+#undef NDEBUG
+
+#include "test_utils.h"
+
+#include "pugl/pugl.h"
+#include "pugl/pugl_stub.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#ifdef __APPLE__
+static const double timeout = 1 / 60.0;
+#else
+static const double timeout = -1.0;
+#endif
+
+#ifdef _WIN32
+// Windows SetTimer has a maximum resolution of 10ms
+static const double tolerance = 0.011;
+#else
+static const double tolerance = 0.002;
+#endif
+
+static const uintptr_t timerId = 1u;
+static const double timerPeriod = 1 / 60.0;
+
+typedef enum {
+ START,
+ EXPOSED,
+} State;
+
+typedef struct {
+ PuglTestOptions opts;
+ PuglWorld* world;
+ PuglView* view;
+ size_t numAlarms;
+ State state;
+} PuglTest;
+
+static PuglStatus
+onEvent(PuglView* view, const PuglEvent* event)
+{
+ PuglTest* test = (PuglTest*)puglGetHandle(view);
+
+ if (test->opts.verbose) {
+ printEvent(event, "Event: ", true);
+ }
+
+ switch (event->type) {
+ case PUGL_EXPOSE:
+ test->state = EXPOSED;
+ break;
+
+ case PUGL_TIMER:
+ assert(event->timer.id == timerId);
+ ++test->numAlarms;
+ break;
+
+ default:
+ break;
+ }
+
+ return PUGL_SUCCESS;
+}
+
+static double
+roundPeriod(const double period)
+{
+ return floor(period * 1000.0) / 1000.0; // Round down to milliseconds
+}
+
+int
+main(int argc, char** argv)
+{
+ PuglTest app = {puglParseTestOptions(&argc, &argv),
+ puglNewWorld(PUGL_PROGRAM, 0),
+ NULL,
+ 0,
+ START};
+
+ // Set up view
+ app.view = puglNewView(app.world);
+ puglSetClassName(app.world, "Pugl Test");
+ puglSetBackend(app.view, puglStubBackend());
+ puglSetHandle(app.view, &app);
+ puglSetEventFunc(app.view, onEvent);
+
+ // Create and show window
+ assert(!puglCreateWindow(app.view, "Pugl Test"));
+ assert(!puglShowWindow(app.view));
+ while (app.state != EXPOSED) {
+ assert(!puglUpdate(app.world, timeout));
+ }
+
+ // Register a timer with a longer period first
+ assert(!puglStartTimer(app.view, timerId, timerPeriod * 2.0));
+
+ // Replace it with the one we want (to ensure timers are replaced)
+ assert(!puglStartTimer(app.view, timerId, timerPeriod));
+
+ const double startTime = puglGetTime(app.world);
+
+ puglUpdate(app.world, 1.0);
+
+ // Calculate the actual period of the timer
+ const double endTime = puglGetTime(app.world);
+ const double duration = endTime - startTime;
+ const double expectedPeriod = roundPeriod(timerPeriod);
+ const double actualPeriod = roundPeriod(duration / (double)app.numAlarms);
+ const double difference = fabs(actualPeriod - expectedPeriod);
+
+ if (difference > tolerance) {
+ fprintf(stderr,
+ "error: Period not within %f of %f\n",
+ tolerance,
+ expectedPeriod);
+ fprintf(stderr, "note: Actual period %f\n", actualPeriod);
+ }
+
+ assert(difference <= tolerance);
+
+ // Deregister timer and tick once to synchronize
+ assert(!puglStopTimer(app.view, timerId));
+ puglUpdate(app.world, 0.0);
+
+ // Update for a half second and check that we receive no more alarms
+ app.numAlarms = 0;
+ puglUpdate(app.world, 0.5);
+ assert(app.numAlarms == 0);
+
+ puglFreeView(app.view);
+ puglFreeWorld(app.world);
+
+ return 0;
+}
diff --git a/test/test_utils.h b/test/test_utils.h
index 3e62714..cef94dd 100644
--- a/test/test_utils.h
+++ b/test/test_utils.h
@@ -126,6 +126,8 @@ printEvent(const PuglEvent* event, const char* prefix, const bool verbose)
prefix,
event->client.data1,
event->client.data2);
+ case PUGL_TIMER:
+ return PRINT("%sTimer %" PRIuPTR "\n", prefix, event->timer.id);
default:
break;
}
diff --git a/wscript b/wscript
index 5e2b833..797f321 100644
--- a/wscript
+++ b/wscript
@@ -105,6 +105,17 @@ def configure(conf):
else:
conf.check_cc(lib='X11', uselib_store='X11')
+
+ xsync_fragment = """#include <X11/Xlib.h>
+ #include <X11/extensions/sync.h>
+ int main(void) { XSyncQueryExtension(0, 0, 0); return 0; }"""
+ if conf.check_cc(fragment=xsync_fragment,
+ uselib_store='XSYNC',
+ lib='Xext',
+ mandatory=False,
+ msg='Checking for function XSyncQueryExtension'):
+ conf.define('HAVE_XSYNC', 1)
+
if not Options.options.no_gl:
glx_fragment = """#include <GL/glx.h>
int main(void) { glXSwapBuffers(0, 0); return 0; }"""
@@ -172,7 +183,7 @@ def _build_pc_file(bld, name, desc, target, libname, deps={}, requires=[]):
LIBS=' '.join(link_flags))
-tests = ['redisplay', 'show_hide', 'update']
+tests = ['redisplay', 'show_hide', 'update', 'timer']
def build(bld):
@@ -269,7 +280,7 @@ def build(bld):
else:
platform = 'x11'
build_platform('x11',
- uselib=['M', 'X11'],
+ uselib=['M', 'X11', 'XSYNC'],
source=lib_source + ['pugl/detail/x11.c'])
if bld.env.HAVE_GL: