#undef Marker
#define Marker FuckYouAppleAndYourLackOfNameSpaces
+#include <sys/time.h>
#include <gtkmm/button.h>
#include <gdk/gdkquartz.h>
#include "gui_thread.h"
#include "processor_box.h"
+// yes, yes we know (see wscript for various available OSX compat modes)
+#if defined (__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
#include "CAAudioUnit.h"
#include "CAComponent.h"
+#if defined (__clang__)
+# pragma clang diagnostic pop
+#endif
+
#import <AudioUnit/AUCocoaUIView.h>
#import <CoreAudioKit/AUGenericView.h>
+#import <objc/runtime.h>
+
+#ifndef __ppc__
+#include <dispatch/dispatch.h>
+#endif
#undef Marker
#include "keyboard.h"
#include "utils.h"
#include "public_editor.h"
-#include "i18n.h"
+#include "pbd/i18n.h"
+
+#include "gtk2ardour-config.h"
#ifdef COREAUDIO105
#define ArdourCloseComponent CloseComponent
using namespace PBD;
vector<string> AUPluginUI::automation_mode_strings;
+int64_t AUPluginUI::last_timer = 0;
+bool AUPluginUI::timer_needed = true;
+CFRunLoopTimerRef AUPluginUI::cf_timer;
+sigc::connection AUPluginUI::timer_connection;
static const gchar* _automation_mode_strings[] = {
X_("Manual"),
NSArray* subviews = [view subviews];
unsigned long cnt = [subviews count];
+ if (depth == 0) {
+ NSView* su = [view superview];
+ if (su) {
+ NSRect sf = [su frame];
+ cerr << " PARENT view " << su << " @ " << sf.origin.x << ", " << sf.origin.y
+ << ' ' << sf.size.width << " x " << sf.size.height
+ << endl;
+ }
+ }
+
for (int d = 0; d < depth; d++) {
cerr << '\t';
}
NSRect frame = [view frame];
- cerr << " view @ " << frame.origin.x << ", " << frame.origin.y
+ cerr << " view " << view << " @ " << frame.origin.x << ", " << frame.origin.y
<< ' ' << frame.size.width << " x " << frame.size.height
<< endl;
}
}
+/* 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'Callahan (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 Cocoa, 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 CFRunLoop 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 [CFRunLoop waitForNextEventMatchingMask] which
+ * will eventually put the process to sleep, but won't do so until the
+ * CFRunLoop 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 CFRunLoop 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
+ * CFRunLoop 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
+ * CFRunLoop 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 C'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 CFRunLoop 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 CFRunLoop, with all
+ * GSources represented as CFRunLoopSources, 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.
+ */
+
+static uint32_t block_plugin_redraws = 0;
+static const uint32_t minimum_redraw_rate = 30; /* frames per second */
+static const uint32_t block_plugin_redraw_count = 15; /* number of combined plugin redraws to block, if blocking */
+
+#ifdef __ppc__
+
+/* PowerPC versions of OS X do not support libdispatch, which we use below when swizzling objective C. But they also don't have Retina
+ * which is the underlying reason for this code. So just skip it on those CPUs.
+ */
+
+
+static void add_plugin_view (id view) {}
+static void remove_plugin_view (id view) {}
+
+#else
+
+static IMP original_nsview_drawIfNeeded;
+static std::vector<id> plugin_views;
+
+static void add_plugin_view (id view)
+{
+ if (plugin_views.empty()) {
+ AUPluginUI::start_cf_timer ();
+ }
+
+ plugin_views.push_back (view);
+
+}
+
+static void remove_plugin_view (id view)
+{
+ std::vector<id>::iterator x = find (plugin_views.begin(), plugin_views.end(), view);
+ if (x != plugin_views.end()) {
+ plugin_views.erase (x);
+ }
+ if (plugin_views.empty()) {
+ AUPluginUI::stop_cf_timer ();
+ }
+}
+
+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
+
+#endif /* __ppc__ */
+
+/* END OF THE PLUGIN REDRAW HACK */
+
@implementation NotificationObject
- (NotificationObject*) initWithPluginUI: (AUPluginUI*) apluginui andCocoaParent: (NSWindow*) cp andTopLevelParent: (NSWindow*) tlp
: PlugUIBase (insert)
, automation_mode_label (_("Automation"))
, preset_label (_("Presets"))
- , mapped (false)
, resizable (false)
- , min_width (0)
- , min_height (0)
, req_width (0)
, req_height (0)
- , alo_width (0)
- , alo_height (0)
, cocoa_window (0)
, au_view (0)
, in_live_resize (false)
, cocoa_parent (0)
, _notify (0)
, _resize_notify (0)
-
{
if (automation_mode_strings.empty()) {
automation_mode_strings = I18N (_automation_mode_strings);
low_box.signal_size_allocate ().connect (mem_fun (this, &AUPluginUI::lower_box_size_allocate));
low_box.signal_map ().connect (mem_fun (this, &AUPluginUI::lower_box_map));
low_box.signal_unmap ().connect (mem_fun (this, &AUPluginUI::lower_box_unmap));
- low_box.signal_expose_event ().connect (mem_fun (this, &AUPluginUI::lower_box_expose));
}
}
[[NSNotificationCenter defaultCenter] removeObserver:_resize_notify];
}
+ NSWindow* win = get_nswindow();
+ if (au_view) {
+ remove_plugin_view ([[win contentView] superview]);
+ }
+
+#ifdef WITH_CARBON
if (cocoa_parent) {
- NSWindow* win = get_nswindow();
[win removeChildWindow:cocoa_parent];
}
-#ifdef WITH_CARBON
if (carbon_window) {
/* not parented, just overlaid on top of our window */
DisposeWindow (carbon_window);
/* remove whatever we packed into low_box so that GTK doesn't
mess with it.
*/
-
[au_view removeFromSuperview];
}
}
// Get the initial size of the new AU View's frame
NSRect frame = [au_view frame];
- min_width = req_width = frame.size.width;
- min_height = req_height = frame.size.height;
+ req_width = frame.size.width;
+ req_height = frame.size.height;
resizable = [au_view autoresizingMask];
- std::cerr << plugin->name() << " initial frame = " << [NSStringFromRect (frame) UTF8String] << " resizable ? " << resizable << std::endl;
low_box.queue_resize ();
last_au_frame = [au_view frame];
}
+bool
+AUPluginUI::timer_callback ()
+{
+ block_plugin_redraws = 0;
+#ifdef AU_DEBUG_PRINT
+ std::cerr << "Resume redraws after idle\n";
+#endif
+ return false;
+}
+
+void
+au_cf_timer_callback (CFRunLoopTimerRef timer, void* info)
+{
+ reinterpret_cast<AUPluginUI*> (info)->cf_timer_callback ();
+}
+
+void
+AUPluginUI::cf_timer_callback ()
+{
+ int64_t now = ARDOUR::get_microseconds ();
+
+ if (!last_timer || block_plugin_redraws) {
+ last_timer = now;
+ return;
+ }
+
+ const int64_t usecs_slop = (1400000 / minimum_redraw_rate); // 140%
+
+#ifdef AU_DEBUG_PRINT
+ std::cerr << "Timer elapsed : " << now - last_timer << std::endl;
+#endif
+
+ if ((now - last_timer) > (usecs_slop + (1000000/minimum_redraw_rate))) {
+ block_plugin_redraws = block_plugin_redraw_count;
+ timer_connection.disconnect ();
+ timer_connection = Glib::signal_timeout().connect (&AUPluginUI::timer_callback, 40);
+#ifdef AU_DEBUG_PRINT
+ std::cerr << "Timer too slow, block plugin redraws\n";
+#endif
+ }
+
+ last_timer = now;
+}
+
+void
+AUPluginUI::start_cf_timer ()
+{
+ if (!timer_needed) {
+ return;
+ }
+
+ CFTimeInterval interval = 1.0 / (float) minimum_redraw_rate;
+
+ cf_timer = CFRunLoopTimerCreate (kCFAllocatorDefault,
+ CFAbsoluteTimeGetCurrent() + interval,
+ interval, 0, 0,
+ au_cf_timer_callback,
+ 0);
+
+ CFRunLoopAddTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
+ timer_needed = false;
+}
+
+void
+AUPluginUI::stop_cf_timer ()
+{
+ if (timer_needed) {
+ return;
+ }
+
+ CFRunLoopRemoveTimer (CFRunLoopGetCurrent(), cf_timer, kCFRunLoopCommonModes);
+ timer_needed = true;
+ last_timer = 0;
+}
+
void
AUPluginUI::cocoa_view_resized ()
{
if (plugin_requested_resize) {
/* we tried to change the plugin frame from inside this method
- * (to adjust the origin), and the plugin changed its size
- * again. Ignore this second call.
+ * (to adjust the origin), which changes the frame of the AU
+ * NSView, resulting in a reentrant call to the FrameDidChange
+ * handler (this method). Ignore this reentrant call.
*/
+#ifdef AU_DEBUG_PRINT
std::cerr << plugin->name() << " re-entrant call to cocoa_view_resized, ignored\n";
+#endif
return;
}
NSRect new_frame = [au_view frame];
- std::cerr << "Plugin " << plugin->name() << " requested update (prs now = " << plugin_requested_resize << ")\n";
- std::cerr << "\tAU NSView frame : " << [ NSStringFromRect (new_frame) UTF8String] << std::endl;
- std::cerr << "\tlast au frame : " << [ NSStringFromRect (last_au_frame) UTF8String] << std::endl;
-
/* from here on, we know that we've been called because the plugin
* decided to change the NSView frame itself.
*/
windowFrame.size.height += dy;
windowFrame.size.width += dx;
- std::cerr << "\tChange size by " << dx << " x " << dy << std::endl;
-
NSUInteger old_auto_resize = [au_view autoresizingMask];
- /* Stop the AU NSView from resizing itself *again* in response to
- us changing the window size.
- */
-
-
- [au_view setAutoresizingMask:NSViewNotSizable];
-
- /* this resizes the window. it will eventually trigger a new
- * size_allocate event/callback, and we'll end up in
- * ::update_view_size(). We want to stop that from doing anything,
- * because we've already resized the window to fit the new new view,
- * so there's no need to actually update the view size again.
- */
-
- [window setFrame:windowFrame display:1];
-
/* Some stupid AU Views change the origin of the original AU View when
they are resized (I'm looking at you AUSampler). If the origin has
been moved, move it back.
if (last_au_frame.origin.x != new_frame.origin.x ||
last_au_frame.origin.y != new_frame.origin.y) {
new_frame.origin = last_au_frame.origin;
- std::cerr << "Move AU NSView origin back to "
- << new_frame.origin.x << ", " << new_frame.origin.y
- << std::endl;
[au_view setFrame:new_frame];
/* also be sure to redraw the topbox because this can
also go wrong.
*/
top_box.queue_draw ();
- } else {
- std::cerr << "No need to move origin, last au origin " << [NSStringFromPoint(last_au_frame.origin) UTF8String]
- << " == new au origin " << [NSStringFromPoint(new_frame.origin) UTF8String]
- << std::endl;
}
+ /* We resize the window using Cocoa. We can't use GTK mechanisms
+ * because of this:
+ *
+ * http://www.lists.apple.com/archives/coreaudio-api/2005/Aug/msg00245.html
+ *
+ * "The host needs to be aware that changing the size of the window in
+ * response to the NSViewFrameDidChangeNotification can cause the view
+ * size to change depending on the autoresizing mask of the view. The
+ * host may need to cache the autoresizing mask of the view, set it to
+ * NSViewNotSizable, resize the window, and then reset the autoresizing
+ * mask of the view once the window has been sized."
+ *
+ */
+
+ [au_view setAutoresizingMask:NSViewNotSizable];
+ [window setFrame:windowFrame display:1];
[au_view setAutoresizingMask:old_auto_resize];
- /* keep a copy of the size of the AU NSView. We didn't set - the plugin did */
+ /* keep a copy of the size of the AU NSView. We didn't set it - the plugin did */
last_au_frame = new_frame;
- min_width = req_width = new_frame.size.width;
- min_height = req_height = new_frame.size.height;
+ req_width = new_frame.size.width;
+ req_height = new_frame.size.height;
plugin_requested_resize = 0;
}
NSView* view = gdk_quartz_window_get_nsview (low_box.get_window()->gobj());
[view addSubview:au_view];
+ /* despite the fact that the documentation says that [NSWindow
+ contentView] is the highest "accessible" NSView in an NSWindow, when
+ the redraw cycle is executed, displayIfNeeded is actually executed
+ on the parent of the contentView. To provide a marginal speedup when
+ checking if a given redraw is for a plugin, use this "hidden" NSView
+ to identify the plugin, so that we do not have to call [superview]
+ every time in interposed_drawIfNeeded().
+ */
+ add_plugin_view ([[win contentView] superview]);
/* this moves the AU NSView down and over to provide a left-hand margin
* and to clear the Ardour "task bar" (with plugin preset mgmt buttons,
// catch notifications that live resizing is about to start
+#if HAVE_COCOA_LIVE_RESIZING
_resize_notify = [ [ LiveResizeNotificationObject alloc] initWithPluginUI:this ];
[[NSNotificationCenter defaultCenter] addObserver:_resize_notify
[[NSNotificationCenter defaultCenter] addObserver:_resize_notify
selector:@selector(windowWillEndLiveResizeHandler:) name:NSWindowDidEndLiveResizeNotification
object:win];
+#else
+ /* No way before 10.6 to identify the start of a live resize (drag
+ * resize) without subclassing NSView and overriding two of its
+ * methods. Instead of that, we make the window non-resizable, thus
+ * ending confusion about whether or not resizes are plugin or user
+ * driven (they are always plugin-driven).
+ */
+
+ Gtk::Container* toplevel = get_toplevel();
+ Requisition req;
+
+ resizable = false;
+
+ if (toplevel && toplevel->is_toplevel()) {
+ toplevel->size_request (req);
+ toplevel->set_size_request (req.width, req.height);
+ dynamic_cast<Gtk::Window*>(toplevel)->set_resizable (false);
+ }
+#endif
return 0;
}
void
AUPluginUI::lower_box_map ()
{
- mapped = true;
[au_view setHidden:0];
update_view_size ();
}
void
AUPluginUI::lower_box_unmap ()
{
- mapped = false;
[au_view setHidden:1];
}
void
AUPluginUI::lower_box_size_allocate (Gtk::Allocation& allocation)
{
- alo_width = allocation.get_width ();
- alo_height = allocation.get_height ();
- std::cerr << "lower box size reallocated to " << alo_width << " x " << alo_height << std::endl;
update_view_size ();
- std::cerr << "low box draw (0, 0, " << alo_width << " x " << alo_height << ")\n";
- low_box.queue_draw_area (0, 0, alo_width, alo_height);
-}
-
-gboolean
-AUPluginUI::lower_box_expose (GdkEventExpose* event)
-{
- std::cerr << "lower box expose: " << event->area.x << ", " << event->area.y
- << ' '
- << event->area.width << " x " << event->area.height
- << " ALLOC "
- << get_allocation().get_width() << " x " << get_allocation().get_height()
- << std::endl;
-
- /* hack to keep ardour responsive
- * some UIs (e.g Addictive Drums) completely hog the CPU
- */
- ARDOUR::GUIIdle();
-
- return true;
}
void
void
AUPluginUI::start_live_resize ()
{
- std::cerr << "\n\n\n++++ Entering Live Resize\n";
- in_live_resize = true;
+ in_live_resize = true;
}
void
AUPluginUI::end_live_resize ()
{
- std::cerr << "\n\n\n ----Leaving Live Resize\n";
- in_live_resize = false;
+ in_live_resize = false;
}