gtk/quartz ... a tale of nested incompatible event loops



The following text is take from a comment that is part of a recent (3 week old) commit to Ardour. Hopefully it will speak for itself. I'm emailing it just in case anybody else decides to wade into a complete overhaul of the design of the glib event loop on Quartz.

First, here's the core of the code in the commit. Apologies to those who don't read Objective C, and even if you do, it isn't exactly self-explanatory. Btw, the original name for this commit was "Gandalf", for reasons that might be slightly more clear.

----------------------------------------------------------------------------

static void interposed_drawIfNeeded (id receiver, SEL selector, NSRect rect)
{
    if (block_plugin_redraws && (find (plugin_views.begin(), plugin_views.end(), receiver) != plugin_views.end())) {
        block_plugin_redraws--;
#ifdef AU_DEBUG_PRINT
        std::cerr << "Plugin redraw blocked\n";
#endif
        /* YOU ... SHALL .... NOT ... DRAW!!!! */
        return;
    }
    (void) ((int (*)(id,SEL,NSRect)) original_nsview_drawIfNeeded) (receiver, selector, rect);
}

@implementation NSView (Tracking)
+ (void) load {
    static dispatch_once_t once_token;

    /* this swizzles NSView::displayIfNeeded and replaces it with
     * interposed_drawIfNeeded(), which allows us to interpose and block
     * the redrawing of plugin UIs when their redrawing behaviour
     * is interfering with event loop behaviour.
     */

    dispatch_once (&once_token, ^{
            Method target = class_getInstanceMethod ([NSView class], @selector(displayIfNeeded));
            original_nsview_drawIfNeeded = method_setImplementation (target, (IMP) interposed_drawIfNeeded);
        });
}

@end

----------------------------------------------------------------------------

And now here's the explanation:

This deeply hacky block of code exists for a rather convoluted reason.

The proximal reason is that there are plugins (such as XLN's Addictive Drums 2)
which redraw their GUI/editor windows using a timer, and use a drawing technique
that on Retina displays ends up calling arg32_image_mark_RGB32, a function that
for some reason (probably byte-swapping or pixel-doubling) is many times slower
than the function used on non-Retina displays.

We are not the first people to discover the problem with arg32_image_mark_RGB32.

Justin Fraenkel, the lead author of Reaper, wrote a very detailed account of the
performance issues with arg32_image_mark_RGB32 here:
http://www.1014.org/?article=516

The problem was also seen by Robert O'allahan (lead developer of rr, the reverse
debugger) as far back as 2010:
http://robert.ocallahan.org/2010/05/cglayer-performance-trap-with-isflipped_03.html

In fact, it is so slow that the drawing takes up close to 100% of a single core,
and the event loop that the drawing occurs in never sleeps or "idles".

In AU hosts built directly on top of ocoa, or some other toolkits, this isn't
inherently a major problem - it just makes the entire GUI of the application
slow.

However, there is an additional problem for Ardour because GTK+ is built on top
of the GDK/Quartz event loop integration. This integration is rather baroque,
mostly because it was written at a time when FRunLoop did not offer a way to
wait for "input" from file descriptors (which arrived in OS X 10.5). As a
result, it uses a hair-raising design involving an additional thread. This
design has a major problem, which is that it effectively creates two nested run
loops.

The GTK+/GDK/glib one runs until it has nothing to do, at which time it calls a
function to wait until there is something to do. On Linux or Windows that would
involve some variant or relative of poll(2), which puts the process to sleep
until there is something to do.

On OS X, glib ends up calling [FRunLoop waitForNextEventMatchingMask] which will
eventually put the process to sleep, but won't do so until the FRunLoop also has
nothing to do. This includes (at least) a complete redraw cycle. If redrawing
takes too long, and there are timers expired for another redraw (e.g. Addictive
Drums 2, again), then the FRunLoop will just start another redraw cycle after
processing any events and other stuff.

If the CFRunLoop stays busy, then it will never return to the glib level at all,
thus stopping any further GTK+ level activity (events, drawing) from taking
place. In short, the current (spring 2016) design of the GDK/Quartz event loop
integration relies on the idea that the internal FRunLoop will go idle, and
totally breaks if this does not happen.

So take a fully functional Ardour, add in XLN's Addictive Drums 2, and a Retina
display, and Apple's ridiculously slow blitting code, and the FRunLoop never
goes idle. As soon as Addictive Drums starts drawing (over and over again), the
GTK+ event loop stops receiving events and stops drawing.

One fix for this was to run a nested GTK+ event loop iteration (or two) whenever
a plugin window was redrawn. This works in the sense that the immediate issue
(no GTK+ events or drawing) is fixed. But the recursive GTK+ event loop causes
its own (very subtle) problems too.

This code takes a rather radical approach. We use Objective 's ability to
swizzle object methods. Specifically, we replace [NSView displayIfNeeded] with
our own version which will skip redraws of plugin windows if we tell it too. If
we haven't done that, or if the redraw is of a non-plugin window, then we invoke
the original displayIfNeeded method.

After every 10 redraws of a given plugin GUI/editor window, we queue up a
GTK/glib idle callback to measure the interval between those idle callbacks. We
do this globally across all plugin windows, so if the callback is already
queued, we don't requeue it.

If the interval is longer than 40msec (a 25fps redraw rate), we set
block_plugin_redraws to some number. Each successive call to our interposed
displayIfNeeded method will (a) check this value and if non-zero (b) check if
the call is for a plugin-related NSView/NSWindow. If it is, then we will skip
the redisplay entirely, hopefully avoiding any calls to argb32_image_mark_RGB32
or any other slow drawing code, and thus allowing the FRunLoop to go idle. If
the value is zero or the call is for a non-plugin window, then we just invoke
the "original" displayIfNeeded method.

This hack adds a tiny bit of overhead onto redrawing of the entire
application. But in the common case this consists of 1 conditional (the check on
block_plugin_redraws, which will find it to be zero) and the invocation of the
original method. Given how much work is typically done during drawing, this
seems acceptable.

The correct fix for this is to redesign the relationship between GTK+/GDK/glib
so that a glib run loop is actually a FRunLoop, with all GSources represented as
FRunLoopSources, without any nesting and without any additional thread. This is
not a task to be undertaken lightly, and is certainly substantially more work
than this was. It may never be possible to do that work in a way that could be
integrated back into glib, because of the rather specific semantics and types of
GSources, but it would almost certainly be possible to make it work for Ardour.



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]