From http://www.jwz.org/xscreensaver/xscreensaver-5.33.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
index 602629533f4c691cb90e5bc904b5cf08a9cf34a0..9ab94bc48e26fd2b3b53557a783ce43afe4fb63e 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2014 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2015 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -16,6 +16,7 @@
  */
 
 #import <QuartzCore/QuartzCore.h>
+#import <sys/mman.h>
 #import <zlib.h>
 #import "XScreenSaverView.h"
 #import "XScreenSaverConfigSheet.h"
@@ -24,6 +25,9 @@
 #import "xlockmoreI.h"
 #import "jwxyz-timers.h"
 
+#ifndef USE_IPHONE
+# import <OpenGL/glu.h>
+#endif
 
 /* Garbage collection only exists if we are being compiled against the 
    10.6 SDK or newer, not if we are building against the 10.4 SDK.
 # define DO_GC_HACKERY
 #endif
 
+/* Duplicated in xlockmoreI.h and XScreenSaverGLView.m. */
+extern void clear_gl_error (void);
+extern void check_gl_error (const char *type);
+
 extern struct xscreensaver_function_table *xscreensaver_function_table;
 
 /* Global variables used by the screen savers
@@ -259,7 +267,7 @@ add_default_options (const XrmOptionDescRec *opts,
 # endif
  // ".textLiteral:        ",
  // ".textFile:           ",
-    ".textURL:            http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
+    ".textURL:            https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
  // ".textProgram:        ",
     ".grabDesktopImages:  yes",
 # ifndef USE_IPHONE
@@ -416,6 +424,13 @@ orientname(unsigned long o)
 
   next_frame_time = 0;
 
+# ifndef USE_IPHONE
+  // When the view fills the screen and double buffering is enabled, OS X will
+  // use page flipping for a minor CPU/FPS boost. In windowed mode, double
+  // buffering reduces the frame rate to 1/2 the screen's refresh rate.
+  double_buffered_p = !isPreview;
+# endif
+
 # ifdef USE_IPHONE
   double s = [self hackedContentScaleFactor];
 # else
@@ -436,25 +451,25 @@ orientname(unsigned long o)
 
   // So we can tell when we're docked.
   [UIDevice currentDevice].batteryMonitoringEnabled = YES;
-# endif // USE_IPHONE
 
-# ifdef USE_BACKBUFFER
-  [self createBackbuffer:bb_size];
-  [self initLayer];
-# endif
+  [self setBackgroundColor:[NSColor blackColor]];
+
+  [[NSNotificationCenter defaultCenter]
+   addObserver:self
+   selector:@selector(didRotate:)
+   name:UIDeviceOrientationDidChangeNotification object:nil];
+# endif // USE_IPHONE
 
   return self;
 }
 
-- (void) initLayer
+
+#ifdef USE_IPHONE
++ (Class) layerClass
 {
-# if !defined(USE_IPHONE) && defined(BACKBUFFER_CALAYER)
-  [self setLayer: [CALayer layer]];
-  self.layer.delegate = self;
-  self.layer.opaque = YES;
-  [self setWantsLayer: YES];
-# endif  // !USE_IPHONE && BACKBUFFER_CALAYER
+  return [CAEAGLLayer class];
 }
+#endif
 
 
 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
@@ -465,22 +480,26 @@ orientname(unsigned long o)
 
 - (void) dealloc
 {
-  NSAssert(![self isAnimating], @"still animating");
+  if ([self isAnimating])
+    [self stopAnimation];
   NSAssert(!xdata, @"xdata not yet freed");
   NSAssert(!xdpy, @"xdpy not yet freed");
 
+# ifdef USE_IPHONE
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+# endif
+
 # ifdef USE_BACKBUFFER
-  if (backbuffer)
-    CGContextRelease (backbuffer);
+
+#  ifdef BACKBUFFER_OPENGL
+  [ogl_ctx release];
+  // Releasing the OpenGL context should also free any OpenGL objects,
+  // including the backbuffer texture and frame/render/depthbuffers.
+#  endif // BACKBUFFER_OPENGL
 
   if (colorspace)
     CGColorSpaceRelease (colorspace);
 
-#  ifdef BACKBUFFER_CGCONTEXT
-  if (window_ctx)
-    CGContextRelease (window_ctx);
-#  endif // BACKBUFFER_CGCONTEXT
-
 # endif // USE_BACKBUFFER
 
   [prefsReader release];
@@ -527,7 +546,7 @@ orientname(unsigned long o)
   NSAssert(!initted_p && !xdata, @"already initialized");
 
   // See comment in render_x11() for why this value is important:
-  [self setAnimationTimeInterval: 1.0 / 120.0];
+  [self setAnimationTimeInterval: 1.0 / 240.0];
 
   [super startAnimation];
   /* We can't draw on the window from this method, so we actually do the
@@ -536,6 +555,16 @@ orientname(unsigned long o)
    */
 
 # ifdef USE_IPHONE
+  {
+    CGSize b = self.bounds.size;
+    double s = [self hackedContentScaleFactor];
+    b.width  *= s;
+    b.height *= s;
+    NSAssert (initial_bounds.width == b.width &&
+              initial_bounds.height == b.height,
+              @"bounds changed unexpectedly");
+  }
+
   if (crash_timer)
     [crash_timer invalidate];
 
@@ -560,8 +589,165 @@ orientname(unsigned long o)
   [[UIApplication sharedApplication]
     setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
 # endif
-}
 
+#ifdef BACKBUFFER_OPENGL
+  CGSize new_backbuffer_size;
+
+  {
+# ifndef USE_IPHONE
+    if (!ogl_ctx) {
+
+      NSOpenGLPixelFormat *pixfmt = [self getGLPixelFormat];
+
+      NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");
+
+      [pixfmt retain]; // #### ???
+
+      // Fun: On OS X 10.7, the second time an OpenGL context is created, after
+      // the preferences dialog is launched in SaverTester, the context only
+      // lasts until the first full GC. Then it turns black. Solution is to
+      // reuse the OpenGL context after this point.
+      ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
+                                         shareContext:nil];
+
+      // Sync refreshes to the vertical blanking interval
+      GLint r = 1;
+      [ogl_ctx setValues:&r forParameter:NSOpenGLCPSwapInterval];
+//    check_gl_error ("NSOpenGLCPSwapInterval");  // SEGV sometimes. Too early?
+    }
+
+    [ogl_ctx makeCurrentContext];
+    check_gl_error ("makeCurrentContext");
+
+    // NSOpenGLContext logs an 'invalid drawable' when this is called
+    // from initWithFrame.
+    [ogl_ctx setView:self];
+
+    // Clear frame buffer ASAP, else there are bits left over from other apps.
+    glClearColor (0, 0, 0, 1);
+    glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+//    glFinish ();
+//    glXSwapBuffers (mi->dpy, mi->window);
+
+
+    // Enable multi-threading, if possible.  This runs most OpenGL commands
+    // and GPU management on a second CPU.
+    {
+#  ifndef  kCGLCEMPEngine
+#   define kCGLCEMPEngine 313  // Added in MacOS 10.4.8 + XCode 2.4.
+#  endif
+      CGLContextObj cctx = CGLGetCurrentContext();
+      CGLError err = CGLEnable (cctx, kCGLCEMPEngine);
+      if (err != kCGLNoError) {
+        NSLog (@"enabling multi-threaded OpenGL failed: %d", err);
+      }
+    }
+
+    new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
+
+# else  // USE_IPHONE
+    if (!ogl_ctx) {
+      CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
+      eagl_layer.opaque = TRUE;
+      eagl_layer.drawableProperties = [self getGLProperties];
+
+      // Without this, the GL frame buffer is half the screen resolution!
+      eagl_layer.contentsScale = [UIScreen mainScreen].scale;
+
+      ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
+    }
+
+    [EAGLContext setCurrentContext: ogl_ctx];
+
+    CGSize screen_size = [[[UIScreen mainScreen] currentMode] size];
+    // iPad, simulator: 768x1024
+    // iPad, physical: 1024x768
+    if (screen_size.width > screen_size.height) {
+      CGFloat w = screen_size.width;
+      screen_size.width = screen_size.height;
+      screen_size.height = w;
+    }
+
+    if (gl_framebuffer)  glDeleteFramebuffersOES  (1, &gl_framebuffer);
+    if (gl_renderbuffer) glDeleteRenderbuffersOES (1, &gl_renderbuffer);
+
+    glGenFramebuffersOES  (1, &gl_framebuffer);
+    glBindFramebufferOES  (GL_FRAMEBUFFER_OES,  gl_framebuffer);
+
+    glGenRenderbuffersOES (1, &gl_renderbuffer);
+    glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
+
+//   redundant?
+//     glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES,
+//                               (int)size.width, (int)size.height);
+    [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES
+                    fromDrawable:(CAEAGLLayer*)self.layer];
+
+    glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES,  GL_COLOR_ATTACHMENT0_OES,
+                                  GL_RENDERBUFFER_OES, gl_renderbuffer);
+
+    [self addExtraRenderbuffers:screen_size];
+
+    int err = glCheckFramebufferStatusOES (GL_FRAMEBUFFER_OES);
+    switch (err) {
+      case GL_FRAMEBUFFER_COMPLETE_OES:
+        break;
+      case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
+        NSAssert (0, @"framebuffer incomplete attachment");
+        break;
+      case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
+        NSAssert (0, @"framebuffer incomplete missing attachment");
+        break;
+      case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
+        NSAssert (0, @"framebuffer incomplete dimensions");
+        break;
+      case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
+        NSAssert (0, @"framebuffer incomplete formats");
+        break;
+      case GL_FRAMEBUFFER_UNSUPPORTED_OES:
+        NSAssert (0, @"framebuffer unsupported");
+        break;
+/*
+      case GL_FRAMEBUFFER_UNDEFINED:
+        NSAssert (0, @"framebuffer undefined");
+        break;
+      case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
+        NSAssert (0, @"framebuffer incomplete draw buffer");
+        break;
+      case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
+        NSAssert (0, @"framebuffer incomplete read buffer");
+        break;
+      case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
+        NSAssert (0, @"framebuffer incomplete multisample");
+        break;
+      case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS:
+        NSAssert (0, @"framebuffer incomplete layer targets");
+        break;
+ */
+    default:
+      NSAssert (0, @"framebuffer incomplete, unknown error 0x%04X", err);
+      break;
+    }
+
+    glViewport (0, 0, screen_size.width, screen_size.height);
+
+    new_backbuffer_size = initial_bounds;
+
+# endif // USE_IPHONE
+
+    check_gl_error ("startAnimation");
+
+//  NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
+//         glGetString (GL_RENDERER), glGetString (GL_VERSION));
+
+    [self enableBackbuffer:new_backbuffer_size];
+  }
+#endif // BACKBUFFER_OPENGL
+
+#ifdef USE_BACKBUFFER
+  [self createBackbuffer:new_backbuffer_size];
+#endif
+}
 
 - (void)stopAnimation
 {
@@ -611,19 +797,38 @@ orientname(unsigned long o)
   [[UIApplication sharedApplication]
     setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
 # endif
-}
 
+  // Without this, the GL frame stays on screen when switching tabs
+  // in System Preferences.
+  // (Or perhaps it used to. It doesn't seem to matter on 10.9.)
+  //
+# ifndef USE_IPHONE
+  [NSOpenGLContext clearCurrentContext];
+# endif // !USE_IPHONE
 
-/* Hook for the XScreenSaverGLView subclass
- */
-- (void) prepareContext
-{
+  clear_gl_error();    // This hack is defunct, don't let this linger.
+
+  CGContextRelease (backbuffer);
+  backbuffer = nil;
+
+  if (backbuffer_len)
+    munmap (backbuffer_data, backbuffer_len);
+  backbuffer_data = NULL;
+  backbuffer_len = 0;
 }
 
-/* Hook for the XScreenSaverGLView subclass
- */
-- (void) resizeContext
+
+// #### maybe this could/should just be on 'lockFocus' instead?
+- (void) prepareContext
 {
+  if (ogl_ctx) {
+#ifdef USE_IPHONE
+    [EAGLContext setCurrentContext:ogl_ctx];
+#else  // !USE_IPHONE
+    [ogl_ctx makeCurrentContext];
+//    check_gl_error ("makeCurrentContext");
+#endif // !USE_IPHONE
+  }
 }
 
 
@@ -657,14 +862,27 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
   NSSize ssize = [[[UIScreen mainScreen] currentMode] size];
   NSSize bsize = [self bounds].size;
 
+  CGFloat
+    max_ssize = ssize.width > ssize.height ? ssize.width : ssize.height,
+    max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height;
+
   // Ratio of screen size in pixels to view size in points.
-  GLfloat s = ((ssize.width > ssize.height ? ssize.width : ssize.height) /
-               (bsize.width > bsize.height ? bsize.width : bsize.height));
+  CGFloat s = max_ssize / max_bsize;
+
+  // Two constraints:
+
+  // 1. Don't exceed -- let's say 1280 pixels in either direction.
+  //    (Otherwise the frame rate gets bad.)
+  CGFloat mag0 = ceil(max_ssize / 1280);
 
-  if (ssize.width >= 1024 && ssize.height >= 1024)
-    s = 1;
+  // 2. Don't let the pixel size get too small.
+  //    (Otherwise pixels in IFS and similar are too fine.)
+  //    So don't let the result be > 2 pixels per point.
+  CGFloat mag1 = ceil(s / 2);
 
-  return s;
+  // As of iPhone 6, mag0 is always >= mag1. This may not be true in the future.
+  // (desired scale factor) = s / (desired magnification factor)
+  return s / (mag0 > mag1 ? mag0 : mag1);
 }
 
 
@@ -692,10 +910,10 @@ double current_device_rotation (void)
     rot_current_angle = f - rotation_ratio * dist;
 
     // Intermediate frame size.
-    rot_current_size.width = rot_from.width + 
-      rotation_ratio * (rot_to.width - rot_from.width);
-    rot_current_size.height = rot_from.height + 
-      rotation_ratio * (rot_to.height - rot_from.height);
+    rot_current_size.width = floor(rot_from.width +
+      rotation_ratio * (rot_to.width - rot_from.width));
+    rot_current_size.height = floor(rot_from.height +
+      rotation_ratio * (rot_to.height - rot_from.height));
 
     // Tick animation.  Complete rotation in 1/6th sec.
     double now = double_time();
@@ -768,6 +986,180 @@ double current_device_rotation (void)
 
 #ifdef USE_BACKBUFFER
 
+# ifndef USE_IPHONE
+
+struct gl_version
+{
+  // iOS always uses OpenGL ES 1.1.
+  unsigned major;
+  unsigned minor;
+};
+
+static GLboolean
+gl_check_ver (const struct gl_version *caps,
+              unsigned gl_major,
+              unsigned gl_minor)
+{
+  return caps->major > gl_major ||
+           (caps->major == gl_major && caps->minor >= gl_minor);
+}
+
+# else
+
+static GLboolean
+gluCheckExtension (const GLubyte *ext_name, const GLubyte *ext_string)
+{
+  size_t ext_len = strlen ((const char *)ext_name);
+
+  for (;;) {
+    const GLubyte *found = (const GLubyte *)strstr ((const char *)ext_string,
+                                                    (const char *)ext_name);
+    if (!found)
+      break;
+
+    char last_ch = found[ext_len];
+    if ((found == ext_string || found[-1] == ' ') &&
+        (last_ch == ' ' || !last_ch)) {
+      return GL_TRUE;
+    }
+
+    ext_string = found + ext_len;
+  }
+
+  return GL_FALSE;
+}
+
+# endif
+
+/* Called during startAnimation before the first call to createBackbuffer. */
+- (void) enableBackbuffer:(CGSize)new_backbuffer_size
+{
+# ifndef USE_IPHONE
+  struct gl_version version;
+
+  {
+    const char *version_str = (const char *)glGetString (GL_VERSION);
+
+    /* iPhone is always OpenGL ES 1.1. */
+    if (sscanf ((const char *)version_str, "%u.%u",
+                &version.major, &version.minor) < 2)
+    {
+      version.major = 1;
+      version.minor = 1;
+    }
+  }
+# endif
+
+  // The OpenGL extensions in use in here are pretty are pretty much ubiquitous
+  // on OS X, but it's still good form to check.
+  const GLubyte *extensions = glGetString (GL_EXTENSIONS);
+
+  glGenTextures (1, &backbuffer_texture);
+
+  // On really old systems, it would make sense to split the texture
+  // into subsections
+# ifndef USE_IPHONE
+  gl_texture_target = (gluCheckExtension ((const GLubyte *)
+                                         "GL_ARB_texture_rectangle",
+                                         extensions)
+                       ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D);
+# else
+  // OES_texture_npot also provides this, but iOS never provides it.
+  gl_limited_npot_p = gluCheckExtension ((const GLubyte *)
+                                         "GL_APPLE_texture_2D_limited_npot",
+                                         extensions);
+  gl_texture_target = GL_TEXTURE_2D;
+# endif
+
+  glBindTexture (gl_texture_target, &backbuffer_texture);
+  glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+  // GL_LINEAR might make sense on Retina iPads.
+  glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+  glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+  glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+# ifndef USE_IPHONE
+  // There isn't much sense in supporting one of these if the other
+  // isn't present.
+  gl_apple_client_storage_p =
+    gluCheckExtension ((const GLubyte *)"GL_APPLE_client_storage",
+                       extensions) &&
+    gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_range", extensions);
+
+  if (gl_apple_client_storage_p) {
+    glTexParameteri (gl_texture_target, GL_TEXTURE_STORAGE_HINT_APPLE,
+                     GL_STORAGE_SHARED_APPLE);
+    glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
+  }
+# endif
+
+  // If a video adapter suports BGRA textures, then that's probably as fast as
+  // you're gonna get for getting a texture onto the screen.
+# ifdef USE_IPHONE
+  gl_pixel_format =
+    gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_format_BGRA8888",
+                       extensions) ? GL_BGRA : GL_RGBA;
+  gl_pixel_type = GL_UNSIGNED_BYTE;
+  // See also OES_read_format.
+# else
+  if (gl_check_ver (&version, 1, 2) ||
+      (gluCheckExtension ((const GLubyte *)"GL_EXT_bgra", extensions) &&
+       gluCheckExtension ((const GLubyte *)"GL_APPLE_packed_pixels",
+                          extensions))) {
+    gl_pixel_format = GL_BGRA;
+    // Both Intel and PowerPC-era docs say to use GL_UNSIGNED_INT_8_8_8_8_REV.
+    gl_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
+  } else {
+    gl_pixel_format = GL_RGBA;
+    gl_pixel_type = GL_UNSIGNED_BYTE;
+  }
+  // GL_ABGR_EXT/GL_UNSIGNED_BYTE is another possibilty that may have made more
+  // sense on PowerPC.
+# endif
+
+  glEnable (gl_texture_target);
+  glEnableClientState (GL_VERTEX_ARRAY);
+  glEnableClientState (GL_TEXTURE_COORD_ARRAY);
+
+# ifdef USE_IPHONE
+  glMatrixMode (GL_PROJECTION);
+  glLoadIdentity();
+  NSAssert (new_backbuffer_size.width != 0 && new_backbuffer_size.height != 0,
+            @"initial_bounds never got set");
+  // This is pretty similar to the glOrtho in createBackbuffer for OS X.
+  glOrthof (-new_backbuffer_size.width, new_backbuffer_size.width,
+            -new_backbuffer_size.height, new_backbuffer_size.height, -1, 1);
+# endif // USE_IPHONE
+
+  check_gl_error ("enableBackbuffer");
+}
+
+
+static GLsizei
+to_pow2 (size_t x)
+{
+  if (x <= 1)
+    return 1;
+
+  size_t mask = (size_t)-1;
+  unsigned bits = sizeof(x) * CHAR_BIT;
+  unsigned log2 = bits;
+
+  --x;
+  while (bits) {
+    if (!(x & mask)) {
+      log2 -= bits;
+      x <<= bits;
+    }
+
+    bits >>= 1;
+    mask <<= bits;
+  }
+
+  return 1 << log2;
+}
+
+
 /* Create a bitmap context into which we render everything.
    If the desired size has changed, re-created it.
    new_size is in rotated pixels, not points: the same size
@@ -778,68 +1170,35 @@ double current_device_rotation (void)
   // Colorspaces and CGContexts only happen with non-GL hacks.
   if (colorspace)
     CGColorSpaceRelease (colorspace);
-# ifdef BACKBUFFER_CGCONTEXT
-  if (window_ctx)
-    CGContextRelease (window_ctx);
-# endif
+
+# ifdef BACKBUFFER_OPENGL
+  NSAssert ([NSOpenGLContext currentContext] ==
+            ogl_ctx, @"invalid GL context");
+
+  // This almost isn't necessary, except for the ugly aliasing artifacts.
+#  ifndef USE_IPHONE
+  glViewport (0, 0, new_size.width, new_size.height);
+
+  glMatrixMode (GL_PROJECTION);
+  glLoadIdentity();
+  // This is pretty similar to the glOrthof in enableBackbuffer for iPhone.
+  glOrtho (-new_size.width, new_size.width, -new_size.height, new_size.height,
+           -1, 1);
+#  endif // !USE_IPHONE
+# endif // BACKBUFFER_OPENGL
        
   NSWindow *window = [self window];
 
   if (window && xdpy) {
     [self lockFocus];
 
-# if defined(BACKBUFFER_CGCONTEXT)
-    // TODO: This was borrowed from jwxyz_window_resized, and should
-    // probably be refactored.
-         
-    // Figure out which screen the window is currently on.
-    CGDirectDisplayID cgdpy = 0;
-
-    {
-//    int wx, wy;
-//    TODO: XTranslateCoordinates is returning (0,1200) on my system.
-//    Is this right?
-//    In any case, those weren't valid coordinates for CGGetDisplaysWithPoint.
-//    XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL);
-//    p.x = wx;
-//    p.y = wy;
-
-      NSPoint p0 = {0, 0};
-      p0 = [window convertBaseToScreen:p0];
-      CGPoint p = {p0.x, p0.y};
-      CGDisplayCount n;
-      CGGetDisplaysWithPoint (p, 1, &cgdpy, &n);
-      NSAssert (cgdpy, @"unable to find CGDisplay");
-    }
-
-    {
-      // Figure out this screen's colorspace, and use that for every CGImage.
-      //
-      CMProfileRef profile = 0;
-
-      // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no
-      // documented replacement as of OS X 10.9.
-      // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html
-      CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile);
-      NSAssert (profile, @"unable to find colorspace profile");
-      colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile);
-      NSAssert (colorspace, @"unable to find colorspace");
-    }
-# elif defined(BACKBUFFER_CALAYER)
+# ifdef BACKBUFFER_OPENGL
     // Was apparently faster until 10.9.
     colorspace = CGColorSpaceCreateDeviceRGB ();
-# endif // BACKBUFFER_CALAYER
+# endif // BACKBUFFER_OPENGL
 
-# ifdef BACKBUFFER_CGCONTEXT
-    window_ctx = [[window graphicsContext] graphicsPort];
-    CGContextRetain (window_ctx);
-# endif // BACKBUFFER_CGCONTEXT
-         
     [self unlockFocus];
   } else {
-# ifdef BACKBUFFER_CGCONTEXT
-    window_ctx = NULL;
-# endif // BACKBUFFER_CGCONTEXT
     colorspace = CGColorSpaceCreateDeviceRGB();
   }
 
@@ -849,6 +1208,8 @@ double current_device_rotation (void)
     return;
 
   CGContextRef ob = backbuffer;
+  void *odata = backbuffer_data;
+  size_t olen = backbuffer_len;
 
   CGSize osize = backbuffer_size;      // pixels, not points.
   backbuffer_size = new_size;          // pixels, not points.
@@ -858,16 +1219,112 @@ double current_device_rotation (void)
         backbuffer_size.width, backbuffer_size.height);
 # endif
 
-  backbuffer = CGBitmapContextCreate (NULL,
+  /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
+     <https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html>.
+
+     iOS uses bog-standard glTexImage2D (for now).
+
+     glMapBuffer is the standard way to get data from system RAM to video
+     memory asynchronously and without a memcpy, but support for
+     APPLE_client_storage is ubiquitous on OS X (not so for glMapBuffer),
+     and on iOS GL_PIXEL_UNPACK_BUFFER is only available on OpenGL ES 3
+     (iPhone 5S or newer). Plus, glMapBuffer doesn't work well with
+     CGBitmapContext: glMapBuffer can return a different pointer on each
+     call, but a CGBitmapContext doesn't allow its data pointer to be
+     changed -- and recreating the context for a new pointer can be
+     expensive (glyph caches get dumped, for instance).
+
+     glMapBufferRange has MAP_FLUSH_EXPLICIT_BIT and MAP_UNSYNCHRONIZED_BIT,
+     and these seem to allow mapping the buffer and leaving it where it is
+     in client address space while OpenGL works with the buffer, but it
+     requires OpenGL 3 Core profile on OS X (and ES 3 on iOS for
+     GL_PIXEL_UNPACK_BUFFER), so point goes to APPLE_client_storage.
+
+     AMD_pinned_buffer provides the same advantage as glMapBufferRange, but
+     Apple never implemented that one for OS X.
+   */
+
+  backbuffer_data = NULL;
+  gl_texture_w = (int)backbuffer_size.width;
+  gl_texture_h = (int)backbuffer_size.height;
+
+  NSAssert (gl_texture_target == GL_TEXTURE_2D
+# ifndef USE_IPHONE
+            || gl_texture_target == GL_TEXTURE_RECTANGLE_EXT
+# endif
+                 , @"unexpected GL texture target");
+
+# ifndef USE_IPHONE
+  if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
+# else
+  if (!gl_limited_npot_p)
+# endif
+  {
+    gl_texture_w = to_pow2 (gl_texture_w);
+    gl_texture_h = to_pow2 (gl_texture_h);
+  }
+
+  size_t bytes_per_row = gl_texture_w * 4;
+
+# if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
+  // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
+  // it will fall back to a memcpy.
+  // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW24
+  bytes_per_row = (bytes_per_row + 31) & ~31;
+# endif // BACKBUFFER_OPENGL && !USE_IPHONE
+
+  backbuffer_len = bytes_per_row * gl_texture_h;
+  if (backbuffer_len) // mmap requires this to be non-zero.
+    backbuffer_data = mmap (NULL, backbuffer_len,
+                            PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED,
+                            -1, 0);
+
+  BOOL alpha_first_p, order_little_p;
+
+  if (gl_pixel_format == GL_BGRA) {
+    alpha_first_p = YES;
+    order_little_p = YES;
+/*
+  } else if (gl_pixel_format == GL_ABGR_EXT) {
+    alpha_first_p = NO;
+    order_little_p = YES; */
+  } else {
+    NSAssert (gl_pixel_format == GL_RGBA, @"unknown GL pixel format");
+    alpha_first_p = NO;
+    order_little_p = NO;
+  }
+
+#ifdef USE_IPHONE
+  NSAssert (gl_pixel_type == GL_UNSIGNED_BYTE, @"unknown GL pixel type");
+#else
+  NSAssert (gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8 ||
+            gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8_REV ||
+            gl_pixel_type == GL_UNSIGNED_BYTE,
+            @"unknown GL pixel type");
+
+#if defined __LITTLE_ENDIAN__
+  const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8;
+#elif defined __BIG_ENDIAN__
+  const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
+#else
+# error Unknown byte order.
+#endif
+
+  if (gl_pixel_type == backwards_pixel_type)
+    order_little_p ^= YES;
+#endif
+
+  CGBitmapInfo bitmap_info =
+    (alpha_first_p ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaNoneSkipLast) |
+    (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
+
+  backbuffer = CGBitmapContextCreate (backbuffer_data,
                                       (int)backbuffer_size.width,
                                       (int)backbuffer_size.height,
-                                      8, 
-                                      (int)backbuffer_size.width * 4,
+                                      8,
+                                      bytes_per_row,
                                       colorspace,
-                                      // kCGImageAlphaPremultipliedLast
-                                      (kCGImageAlphaNoneSkipFirst |
-                                       kCGBitmapByteOrder32Host)
-                                      );
+                                      bitmap_info);
   NSAssert (backbuffer, @"unable to allocate back buffer");
 
   // Clear it.
@@ -877,6 +1334,11 @@ double current_device_rotation (void)
   CGContextSetGrayFillColor (backbuffer, 0, 1);
   CGContextFillRect (backbuffer, r);
 
+# if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
+  if (gl_apple_client_storage_p)
+    glTextureRangeAPPLE (gl_texture_target, backbuffer_len, backbuffer_data);
+# endif // BACKBUFFER_OPENGL && !USE_IPHONE
+
   if (ob) {
     // Restore old bits, as much as possible, to the X11 upper left origin.
 
@@ -889,9 +1351,124 @@ double current_device_rotation (void)
     CGContextDrawImage (backbuffer, rect, img);
     CGImageRelease (img);
     CGContextRelease (ob);
+
+    if (olen)
+      // munmap should round len up to the nearest page.
+      munmap (odata, olen);
   }
+
+  check_gl_error ("createBackbuffer");
 }
 
+
+- (void) drawBackbuffer
+{
+# ifdef BACKBUFFER_OPENGL
+
+  NSAssert ([ogl_ctx isKindOfClass:[NSOpenGLContext class]],
+            @"ogl_ctx is not an NSOpenGLContext");
+
+  NSAssert (! (CGBitmapContextGetBytesPerRow (backbuffer) % 4),
+            @"improperly-aligned backbuffer");
+
+  // This gets width and height from the backbuffer in case
+  // APPLE_client_storage is in use. See the note in createBackbuffer.
+  // This still has to happen every frame even when APPLE_client_storage has
+  // the video adapter pulling texture data straight from
+  // XScreenSaverView-owned memory.
+  glTexImage2D (gl_texture_target, 0, GL_RGBA,
+                (GLsizei)(CGBitmapContextGetBytesPerRow (backbuffer) / 4),
+                gl_texture_h, 0, gl_pixel_format, gl_pixel_type,
+                backbuffer_data);
+
+  GLfloat vertices[4][2] =
+  {
+    {-backbuffer_size.width,  backbuffer_size.height},
+    { backbuffer_size.width,  backbuffer_size.height},
+    { backbuffer_size.width, -backbuffer_size.height},
+    {-backbuffer_size.width, -backbuffer_size.height}
+  };
+
+  GLfloat tex_coords[4][2];
+
+#  ifndef USE_IPHONE
+  if (gl_texture_target == GL_TEXTURE_RECTANGLE_EXT) {
+    tex_coords[0][0] = 0;
+    tex_coords[0][1] = 0;
+    tex_coords[1][0] = backbuffer_size.width;
+    tex_coords[1][1] = 0;
+    tex_coords[2][0] = backbuffer_size.width;
+    tex_coords[2][1] = backbuffer_size.height;
+    tex_coords[3][0] = 0;
+    tex_coords[3][1] = backbuffer_size.height;
+  } else
+#  endif // USE_IPHONE
+  {
+    GLfloat x = backbuffer_size.width / gl_texture_w;
+    GLfloat y = backbuffer_size.height / gl_texture_h;
+    tex_coords[0][0] = 0;
+    tex_coords[0][1] = 0;
+    tex_coords[1][0] = x;
+    tex_coords[1][1] = 0;
+    tex_coords[2][0] = x;
+    tex_coords[2][1] = y;
+    tex_coords[3][0] = 0;
+    tex_coords[3][1] = y;
+  }
+
+#  ifdef USE_IPHONE
+  if (!ignore_rotation_p) {
+    glMatrixMode (GL_MODELVIEW);
+    glLoadIdentity();
+    glRotatef (rot_current_angle, 0, 0, -1);
+
+    if (rotation_ratio >= 0)
+      glClear (GL_COLOR_BUFFER_BIT);
+  }
+#  endif  // USE_IPHONE
+
+  glVertexPointer (2, GL_FLOAT, 0, vertices);
+  glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
+  glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
+
+#  if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
+  check_gl_error ("drawBackbuffer");
+#  endif
+
+  // This can also happen near the beginning of render_x11.
+  [self flushBackbuffer];
+
+# endif // BACKBUFFER_OPENGL
+}
+
+
+- (void)flushBackbuffer
+{
+# ifndef USE_IPHONE
+  // The OpenGL pipeline is not automatically synchronized with the contents
+  // of the backbuffer, so without glFinish, OpenGL can start rendering from
+  // the backbuffer texture at the same time that JWXYZ is clearing and
+  // drawing the next frame in the backing store for the backbuffer texture.
+  glFinish();
+
+  if (double_buffered_p)
+    [ogl_ctx flushBuffer]; // despite name, this actually swaps
+# else
+  glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
+  [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
+# endif
+
+# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
+  // glGetError waits for the OpenGL command pipe to flush, so skip it in
+  // release builds.
+  // OpenGL Programming Guide for Mac -> OpenGL Application Design
+  // Strategies -> Allow OpenGL to Manage Your Resources
+  // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_designstrategies/opengl_designstrategies.html#//apple_ref/doc/uid/TP40001987-CH2-SW7
+  check_gl_error ("flushBackbuffer");
+# endif
+}
+
+
 #endif // USE_BACKBUFFER
 
 
@@ -904,6 +1481,13 @@ double current_device_rotation (void)
   CGSize new_size;     // pixels, not points
 
 # ifdef USE_BACKBUFFER
+
+  [self prepareContext];
+
+#  if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
+  [ogl_ctx update];
+#  endif // BACKBUFFER_OPENGL && !USE_IPHONE
+
 #  ifdef USE_IPHONE
   CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow])
                     ? initial_bounds
@@ -1056,7 +1640,7 @@ double current_device_rotation (void)
      an animation interval that is faster than animationTimeInterval.
 
      HOWEVER!  On modern systems where setAnimationTimeInterval is *not*
-     ignored, it's important that it be faster than 30 FPS.  120 FPS is good.
+     ignored, it's important that it be faster than 30 FPS.  240 FPS is good.
 
      An NSTimer won't fire if the timer is already running the invocation
      function from a previous firing.  So, if we use a 30 FPS
@@ -1069,7 +1653,7 @@ double current_device_rotation (void)
      integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
      is rounded down from what it would normally be.
 
-     So if we set animationTimeInterval to 1/120 instead of 1/30, frame rates
+     So if we set animationTimeInterval to 1/240 instead of 1/30, frame rates
      become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
      steps for higher or lower animation time intervals respectively.
    */
@@ -1077,13 +1661,19 @@ double current_device_rotation (void)
   gettimeofday (&tv, 0);
   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
   if (now < next_frame_time) return;
-  
-  [self prepareContext];
+
+  [self prepareContext]; // resize_x11 also calls this.
+  // [self flushBackbuffer];
 
   if (resized_p) {
     // We do this here instead of in setFrame so that all the
     // Xlib drawing takes place under the animation timer.
-    [self resizeContext];
+
+# ifndef USE_IPHONE
+    if (ogl_ctx)
+      [ogl_ctx setView:self];
+# endif // !USE_IPHONE
+
     NSRect r;
 # ifndef USE_BACKBUFFER
     r = [self bounds];
@@ -1107,21 +1697,17 @@ double current_device_rotation (void)
 
   // And finally:
   //
-# ifndef USE_IPHONE
-  NSDisableScreenUpdates();
-# endif
   // NSAssert(xdata, @"no xdata when drawing");
   if (! xdata) abort();
   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
   if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata);
-# ifndef USE_IPHONE
-  NSEnableScreenUpdates();
-# endif
 
   gettimeofday (&tv, 0);
   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
   next_frame_time = now + (delay / 1000000.0);
 
+  [self drawBackbuffer];
+
 # ifdef USE_IPHONE     // Allow savers on the iPhone to run full-tilt.
   if (delay < [self animationTimeInterval])
     [self setAnimationTimeInterval:(delay / 1000000.0)];
@@ -1166,19 +1752,6 @@ double current_device_rotation (void)
 }
 
 
-/* drawRect always does nothing, and animateOneFrame renders bits to the
-   screen.  This is (now) true of both X11 and GL on both MacOS and iOS.
- */
-
-- (void)drawRect:(NSRect)rect
-{
-  if (xwindow)    // clear to the X window's bg color, not necessarily black.
-    XClearWindow (xdpy, xwindow);
-  else
-    [super drawRect:rect];    // early: black.
-}
-
-
 #ifndef USE_BACKBUFFER
 
 - (void) animateOneFrame
@@ -1201,128 +1774,10 @@ double current_device_rotation (void)
 
   [self render_x11];
 
-# ifdef USE_IPHONE
+# if defined USE_IPHONE && defined USE_BACKBUFFER
   UIGraphicsPopContext();
 # endif
-
-# ifdef USE_IPHONE
-  // The rotation origin for layer.affineTransform is in the center already.
-  CGAffineTransform t = ignore_rotation_p ?
-    CGAffineTransformIdentity :
-    CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI));
-
-  // Ratio of backbuffer size in pixels to layer size in points.
-  CGSize ssize = backbuffer_size;
-  CGSize bsize = [self bounds].size;
-  GLfloat s = ((ssize.width > ssize.height ? ssize.width : ssize.height) /
-               (bsize.width > bsize.height ? bsize.width : bsize.height));
-
-  self.layer.contentsScale = s;
-  self.layer.affineTransform = t;
-
-  /* Setting the layer's bounds also sets the view's bounds.
-     The view's bounds must be in points, not pixels, and it
-     must be rotated to the current orientation.
-   */
-  CGRect bounds;
-  bounds.origin.x = 0;
-  bounds.origin.y = 0;
-  bounds.size.width  = ssize.width  / s;
-  bounds.size.height = ssize.height / s;
-  self.layer.bounds = bounds;
-
-# endif // USE_IPHONE
-# if defined(BACKBUFFER_CALAYER)
-  [self.layer setNeedsDisplay];
-# elif defined(BACKBUFFER_CGCONTEXT)
-  size_t
-    w = CGBitmapContextGetWidth (backbuffer),
-    h = CGBitmapContextGetHeight (backbuffer);
-  
-  size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer);
-  CGDataProviderRef prov = CGDataProviderCreateWithData (NULL,
-                                            CGBitmapContextGetData(backbuffer),
-                                                         bpl * h,
-                                                         NULL);
-
-
-  CGImageRef img = CGImageCreate (w, h,
-                                  8, 32,
-                                  CGBitmapContextGetBytesPerRow(backbuffer),
-                                  colorspace,
-                                  CGBitmapContextGetBitmapInfo(backbuffer),
-                                  prov, NULL, NO,
-                                  kCGRenderingIntentDefault);
-
-  CGDataProviderRelease (prov);
-  
-  CGRect rect;
-  rect.origin.x = 0;
-  rect.origin.y = 0;
-  rect.size = backbuffer_size;
-  CGContextDrawImage (window_ctx, rect, img);
-  
-  CGImageRelease (img);
-
-  CGContextFlush (window_ctx);
-# endif // BACKBUFFER_CGCONTEXT
-}
-
-# ifdef BACKBUFFER_CALAYER
-
-- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
-{
-  // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4.
-  char *dest_data = (char *)CGBitmapContextGetData (ctx);
-
-  // The CGContext here is normally upside-down on iOS.
-  if (dest_data &&
-      CGBitmapContextGetBitmapInfo (ctx) ==
-        (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host)
-#  ifdef USE_IPHONE
-      && CGContextGetCTM (ctx).d < 0
-#  endif // USE_IPHONE
-      )
-  {
-    size_t dest_height = CGBitmapContextGetHeight (ctx);
-    size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx);
-    size_t src_height = CGBitmapContextGetHeight (backbuffer);
-    size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer);
-    char *src_data = (char *)CGBitmapContextGetData (backbuffer);
-
-    size_t height = src_height < dest_height ? src_height : dest_height;
-    
-    if (src_bpr == dest_bpr) {
-      // iPad 1: 4.0 ms, iPad 2: 6.7 ms
-      memcpy (dest_data, src_data, src_bpr * height);
-    } else {
-      // iPad 1: 4.6 ms, iPad 2: 7.2 ms
-      size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr;
-      while (height) {
-        memcpy (dest_data, src_data, bpr);
-        --height;
-        src_data += src_bpr;
-        dest_data += dest_bpr;
-      }
-    }
-  } else {
-
-    // iPad 1: 9.6 ms, iPad 2: 12.1 ms
-
-#  ifdef USE_IPHONE
-    CGContextScaleCTM (ctx, 1, -1);
-    CGFloat s = [self contentScaleFactor];
-    CGFloat hs = [self hackedContentScaleFactor];
-    CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s);
-#  endif // USE_IPHONE
-    
-    CGImageRef img = CGBitmapContextCreateImage (backbuffer);
-    CGContextDrawImage (ctx, self.layer.bounds, img);
-    CGImageRelease (img);
-  }
 }
-# endif  // BACKBUFFER_CALAYER
 
 #endif // USE_BACKBUFFER
 
@@ -1571,6 +2026,18 @@ double current_device_rotation (void)
             case NSNextFunctionKey:       k = XK_Next;      break;
             case NSBeginFunctionKey:      k = XK_Begin;     break;
             case NSEndFunctionKey:        k = XK_End;       break;
+            case NSF1FunctionKey:        k = XK_F1;        break;
+            case NSF2FunctionKey:        k = XK_F2;        break;
+            case NSF3FunctionKey:        k = XK_F3;        break;
+            case NSF4FunctionKey:        k = XK_F4;        break;
+            case NSF5FunctionKey:        k = XK_F5;        break;
+            case NSF6FunctionKey:        k = XK_F6;        break;
+            case NSF7FunctionKey:        k = XK_F7;        break;
+            case NSF8FunctionKey:        k = XK_F8;        break;
+            case NSF9FunctionKey:        k = XK_F9;        break;
+            case NSF10FunctionKey:       k = XK_F10;       break;
+            case NSF11FunctionKey:       k = XK_F11;       break;
+            case NSF12FunctionKey:       k = XK_F12;       break;
             default:
               {
                 const char *s =
@@ -1662,6 +2129,23 @@ double current_device_rotation (void)
     [super flagsChanged:e];
 }
 
+
+- (NSOpenGLPixelFormat *) getGLPixelFormat
+{
+  NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat");
+
+  NSOpenGLPixelFormatAttribute attrs[40];
+  int i = 0;
+  attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
+
+  if (double_buffered_p)
+    attrs[i++] = NSOpenGLPFADoubleBuffer;
+
+  attrs[i] = 0;
+
+  return [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
+}
+
 #else  // USE_IPHONE
 
 
@@ -1682,42 +2166,13 @@ double current_device_rotation (void)
 //  }
   [self resignFirstResponder];
 
-  // Find SaverRunner.window (as opposed to SaverRunner.saverWindow)
-  UIWindow *listWindow = 0;
-  for (UIWindow *w in [[UIApplication sharedApplication] windows]) {
-    if (w != [self window]) {
-      listWindow = w;
-      break;
-    }
-  }
-
-  UIView *fader = [self superview];  // the "backgroundView" view is our parent
-
   if (relaunch_p) {   // Fake a shake on the SaverListController.
-    UIViewController *v = [listWindow rootViewController];
-    if ([v isKindOfClass: [UINavigationController class]]) {
-# if TARGET_IPHONE_SIMULATOR
-      NSLog (@"simulating shake on saver list");
-# endif
-      UINavigationController *n = (UINavigationController *) v;
-      [[n topViewController] motionEnded: UIEventSubtypeMotionShake
-                               withEvent: nil];
-    }
+    [_delegate didShake:self];
   } else {     // Not launching another, animate our return to the list.
 # if TARGET_IPHONE_SIMULATOR
     NSLog (@"fading back to saver list");
 # endif
-    UIWindow *saverWindow = [self window]; // not SaverRunner.window
-    [listWindow setHidden:NO];
-    [UIView animateWithDuration: 0.5
-            animations:^{ fader.alpha = 0.0; }
-            completion:^(BOOL finished) {
-               [fader removeFromSuperview];
-               fader.alpha = 1.0;
-               [saverWindow setHidden:YES];
-               [listWindow makeKeyAndVisible];
-               [[[listWindow rootViewController] view] becomeFirstResponder];
-            }];
+    [_delegate wantsFadeOut:self];
   }
 }
 
@@ -1743,19 +2198,15 @@ double current_device_rotation (void)
    The savers are under a different UIWindow and a UINavigationController
    that does not do automatic rotation.
 
-   We have to do it this way for OpenGL savers because using Core Animation
-   on an EAGLContext causes the OpenGL pipeline to fall back on software
-   rendering and performance goes to hell.
-
-   For X11-only savers, we could just use Core Animation and let the system
-   handle it, but (maybe) it's simpler to do it the same way for X11 and GL.
+   We have to do it this way because using Core Animation on an EAGLContext
+   causes the OpenGL pipeline used on both X11 and GL savers to fall back on
+   software rendering and performance goes to hell.
 
    During and after rotation, the size/shape of the X11 window changes,
    and ConfigureNotify events are generated.
 
-   X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which
-   rotated at the last minute via a CGAffineTransformMakeRotation when it is
-   copied to the display hardware.
+   X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which is
+   rendered onto a rotating OpenGL quad.
 
    GL code always recieves a portrait-oriented X11 Window whose size never
    changes.  The GL COLOR_BUFFER is displayed on the hardware directly and
@@ -1770,30 +2221,25 @@ double current_device_rotation (void)
 {
   UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
 
-  /* If the simulator starts up in the rotated position, sometimes
-     the UIDevice says we're in Portrait when we're not -- but it
-     turns out that the UINavigationController knows what's up!
-     So get it from there.
+  /* Sometimes UIDevice doesn't know the proper orientation, or the device is
+     face up/face down, so in those cases fall back to the status bar
+     orientation. The SaverViewController tries to set the status bar to the
+     proper orientation before it creates the XScreenSaverView; see
+     _storedOrientation in SaverViewController.
    */
-  if (current == UIDeviceOrientationUnknown) {
-    switch ([[[self window] rootViewController] interfaceOrientation]) {
-    case UIInterfaceOrientationPortrait:
-      current = UIDeviceOrientationPortrait;
-      break;
-    case UIInterfaceOrientationPortraitUpsideDown:
-      current = UIDeviceOrientationPortraitUpsideDown;
-      break;
-    /* It's opposite day, "because rotating the device to the left requires
-       rotating the content to the right" */
-    case UIInterfaceOrientationLandscapeLeft:
-      current = UIDeviceOrientationLandscapeRight;
-      break;
-    case UIInterfaceOrientationLandscapeRight:
-      current = UIDeviceOrientationLandscapeLeft;
-      break;
-    default:
-      break;
-    }
+  if (current == UIDeviceOrientationUnknown ||
+    current == UIDeviceOrientationFaceUp ||
+    current == UIDeviceOrientationFaceDown) {
+    /* Mind the differences between UIInterfaceOrientation and
+       UIDeviceOrientaiton:
+       1. UIInterfaceOrientation does not include FaceUp and FaceDown.
+       2. LandscapeLeft and LandscapeRight are swapped between the two. But
+          converting between device and interface orientation doesn't need to
+          take this into account, because (from the UIInterfaceOrientation
+          description): "rotating the device requires rotating the content in
+          the opposite direction."
+        */
+    current = [UIApplication sharedApplication].statusBarOrientation;
   }
 
   /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get
@@ -1808,11 +2254,6 @@ double current_device_rotation (void)
   if (rotation_ratio >= 0) return;     // in the midst of rotation animation
   if (orientation == current) return;  // no change
 
-  // When transitioning to FaceUp or FaceDown, pretend there was no change.
-  if (current == UIDeviceOrientationFaceUp ||
-      current == UIDeviceOrientationFaceDown)
-    return;
-
   new_orientation = current;           // current animation target
   rotation_ratio = 0;                  // start animating
   rot_start_time = double_time();
@@ -1856,12 +2297,20 @@ double current_device_rotation (void)
   }
 
 # if TARGET_IPHONE_SIMULATOR
-  NSLog (@"rotation begun: %s %d -> %s %d; %d x %d",
+  NSLog (@"%srotation begun: %s %d -> %s %d; %d x %d",
+         initted_p ? "" : "initial ",
          orientname(orientation), (int) rot_current_angle,
          orientname(new_orientation), (int) angle_to,
          (int) rot_current_size.width, (int) rot_current_size.height);
 # endif
 
+  // Even though the status bar isn't on the screen, this still does two things:
+  // 1. It fixes the orientation of the iOS simulator.
+  // 2. It places the iOS notification center on the expected edge.
+  // 3. It prevents the notification center from causing rotation events.
+  [[UIApplication sharedApplication] setStatusBarOrientation:new_orientation
+                                                    animated:NO];
+
  if (! initted_p) {
    // If we've done a rotation but the saver hasn't been initialized yet,
    // don't bother going through an X11 resize, but just do it now.
@@ -1942,16 +2391,63 @@ double current_device_rotation (void)
 
 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
    convert it to what X11 and OpenGL expect.
+
+   Getting this crap right is tricky, given the confusion of the various
+   scale factors, so here's a checklist that I think covers all of the X11
+   and OpenGL cases. For each of these: rotate to all 4 orientations;
+   ensure the mouse tracks properly to all 4 corners.
+
+   Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator.
+
+   Test hacks must cover:
+     X11 ignoreRotation = true
+     X11 ignoreRotation = false
+     OpenGL (rotation is handled manually, so they never ignoreRotation)
+
+   Test devices must cover:
+     contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2)
+     contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air)
+     contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+)
+
+     iPad 2:    768x1024 / 1 = 768x1024
+     iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical)
+     iPhone 4s:  640x960 / 2 = 320x480
+     iPhone 5:  640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical)
+     iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3)
+   
+   Tests:
+                     iPad2 iPadAir iPhone4s iPhone5 iPhone6+
+     Attraction        X  yes  Y       Y       Y       Y       Y
+     Fireworkx X  no   Y       Y       Y       Y       Y
+     Carousel  GL yes  Y       Y       Y       Y       Y
+     Voronoi   GL no   Y       Y       Y       Y       Y
  */
 - (void) convertMouse:(int)rot x:(int*)x y:(int *)y
 {
-  int w = [self frame].size.width;
-  int h = [self frame].size.height;
   int xx = *x, yy = *y;
-  int swap;
 
-  if (ignore_rotation_p) {
-    // We need to rotate the coordinates to match the unrotated X11 window.
+# if TARGET_IPHONE_SIMULATOR
+  {
+    XWindowAttributes xgwa;
+    XGetWindowAttributes (xdpy, xwindow, &xgwa);
+    NSLog (@"TOUCH %4d, %-4d in %4d x %-4d  ig=%d rr=%d cs=%.0f hcs=%.0f\n",
+           *x, *y, 
+           xgwa.width, xgwa.height,
+           ignore_rotation_p, [self reshapeRotatedWindow],
+           [self contentScaleFactor],
+           [self hackedContentScaleFactor]);
+  }
+# endif // TARGET_IPHONE_SIMULATOR
+
+  if (!ignore_rotation_p && [self reshapeRotatedWindow]) {
+    //
+    // For X11 hacks with ignoreRotation == false, we need to rotate the
+    // coordinates to match the unrotated X11 window.  We do not do this
+    // for GL hacks, or for X11 hacks with ignoreRotation == true.
+    //
+    int w = [self frame].size.width;
+    int h = [self frame].size.height;
+    int swap;
     switch (orientation) {
     case UIDeviceOrientationLandscapeRight:
       swap = xx; xx = h-yy; yy = swap;
@@ -1966,15 +2462,24 @@ double current_device_rotation (void)
     }
   }
 
-  double s = [self contentScaleFactor];
+  double s = [self hackedContentScaleFactor];
   *x = xx * s;
   *y = yy * s;
 
-# if TARGET_IPHONE_SIMULATOR
-  NSLog (@"touch %4d, %-4d in %4d x %-4d  %d %d\n",
-         *x, *y, (int)(w*s), (int)(h*s),
-         ignore_rotation_p, [self reshapeRotatedWindow]);
-# endif
+# if TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
+  {
+    XWindowAttributes xgwa;
+    XGetWindowAttributes (xdpy, xwindow, &xgwa);
+    NSLog (@"touch %4d, %-4d in %4d x %-4d  ig=%d rr=%d cs=%.0f hcs=%.0f\n",
+           *x, *y, 
+           xgwa.width, xgwa.height,
+           ignore_rotation_p, [self reshapeRotatedWindow],
+           [self contentScaleFactor],
+           [self hackedContentScaleFactor]);
+    if (*x < 0 || *y < 0 || *x > xgwa.width || *y > xgwa.height)
+      abort();
+  }
+# endif // TARGET_IPHONE_SIMULATOR
 }
 
 
@@ -2127,6 +2632,18 @@ double current_device_rotation (void)
   }
 }
 
+- (NSDictionary *)getGLProperties
+{
+  return [NSDictionary dictionaryWithObjectsAndKeys:
+          kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
+          nil];
+}
+
+- (void)addExtraRenderbuffers:(CGSize)size
+{
+  // No extra renderbuffers are needed for 2D screenhacks.
+}
+
 #endif // USE_IPHONE