From a54361853bdfa08437c2858e603ce6202fb341b2 Mon Sep 17 00:00:00 2001 From: David Robillard Date: Mon, 16 Mar 2020 20:32:28 +0100 Subject: Add timer events --- pugl/detail/mac.h | 1 + pugl/detail/mac.m | 44 +++++++++++++++ pugl/detail/win.c | 22 ++++++++ pugl/detail/x11.c | 139 +++++++++++++++++++++++++++++++++++++++++++++++ pugl/detail/x11.h | 11 ++++ pugl/pugl.h | 61 ++++++++++++++++++++- test/test_timer.c | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test_utils.h | 2 + wscript | 15 +++++- 9 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 test/test_timer.c 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: @@ -741,6 +745,24 @@ puglRequestAttention(PuglView* view) return PUGL_SUCCESS; } +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) { 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 #include +#ifdef HAVE_XSYNC +# include +#endif + #include #include @@ -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; /** @@ -404,6 +405,22 @@ typedef struct { uintptr_t data2; ///< Client-specific data } 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. @@ -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. @{ */ @@ -1034,6 +1052,45 @@ puglGetClipboard(PuglView* view, const char** type, size_t* len); 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. 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 + + 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 +#include +#include +#include + +#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 + #include + 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 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: -- cgit v1.2.1