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
  *
  * 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 <QuartzCore/QuartzCore.h>
+#import <sys/mman.h>
 #import <zlib.h>
 #import "XScreenSaverView.h"
 #import "XScreenSaverConfigSheet.h"
 #import <zlib.h>
 #import "XScreenSaverView.h"
 #import "XScreenSaverConfigSheet.h"
@@ -24,6 +25,9 @@
 #import "xlockmoreI.h"
 #import "jwxyz-timers.h"
 
 #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.
 
 /* 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
 
 # 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
 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:           ",
 # 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
  // ".textProgram:        ",
     ".grabDesktopImages:  yes",
 # ifndef USE_IPHONE
@@ -416,6 +424,13 @@ orientname(unsigned long o)
 
   next_frame_time = 0;
 
 
   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
 # 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;
 
   // 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;
 }
 
 
   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
 
 
 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
@@ -465,22 +480,26 @@ orientname(unsigned long o)
 
 - (void) dealloc
 {
 
 - (void) dealloc
 {
-  NSAssert(![self isAnimating], @"still animating");
+  if ([self isAnimating])
+    [self stopAnimation];
   NSAssert(!xdata, @"xdata not yet freed");
   NSAssert(!xdpy, @"xdpy not yet freed");
 
   NSAssert(!xdata, @"xdata not yet freed");
   NSAssert(!xdpy, @"xdpy not yet freed");
 
+# ifdef USE_IPHONE
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+# endif
+
 # ifdef USE_BACKBUFFER
 # 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);
 
 
   if (colorspace)
     CGColorSpaceRelease (colorspace);
 
-#  ifdef BACKBUFFER_CGCONTEXT
-  if (window_ctx)
-    CGContextRelease (window_ctx);
-#  endif // BACKBUFFER_CGCONTEXT
-
 # endif // USE_BACKBUFFER
 
   [prefsReader release];
 # 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:
   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
 
   [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
    */
 
 # 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];
 
   if (crash_timer)
     [crash_timer invalidate];
 
@@ -560,8 +589,165 @@ orientname(unsigned long o)
   [[UIApplication sharedApplication]
     setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
 # endif
   [[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
 {
 
 - (void)stopAnimation
 {
@@ -611,19 +797,38 @@ orientname(unsigned long o)
   [[UIApplication sharedApplication]
     setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
 # endif
   [[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;
 
   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.
   // 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_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();
 
     // Tick animation.  Complete rotation in 1/6th sec.
     double now = double_time();
@@ -768,6 +986,180 @@ double current_device_rotation (void)
 
 #ifdef USE_BACKBUFFER
 
 
 #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
 /* 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);
   // 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];
 
        
   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 ();
     // 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 {
     [self unlockFocus];
   } else {
-# ifdef BACKBUFFER_CGCONTEXT
-    window_ctx = NULL;
-# endif // BACKBUFFER_CGCONTEXT
     colorspace = CGColorSpaceCreateDeviceRGB();
   }
 
     colorspace = CGColorSpaceCreateDeviceRGB();
   }
 
@@ -849,6 +1208,8 @@ double current_device_rotation (void)
     return;
 
   CGContextRef ob = backbuffer;
     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.
 
   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_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,
                                       (int)backbuffer_size.width,
                                       (int)backbuffer_size.height,
-                                      8, 
-                                      (int)backbuffer_size.width * 4,
+                                      8,
+                                      bytes_per_row,
                                       colorspace,
                                       colorspace,
-                                      // kCGImageAlphaPremultipliedLast
-                                      (kCGImageAlphaNoneSkipFirst |
-                                       kCGBitmapByteOrder32Host)
-                                      );
+                                      bitmap_info);
   NSAssert (backbuffer, @"unable to allocate back buffer");
 
   // Clear it.
   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);
 
   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.
 
   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);
     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
 
 
 #endif // USE_BACKBUFFER
 
 
@@ -904,6 +1481,13 @@ double current_device_rotation (void)
   CGSize new_size;     // pixels, not points
 
 # ifdef USE_BACKBUFFER
   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
 #  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*
      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
 
      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.
 
      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.
    */
      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;
   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.
 
   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];
     NSRect r;
 # ifndef USE_BACKBUFFER
     r = [self bounds];
@@ -1107,21 +1697,17 @@ double current_device_rotation (void)
 
   // And finally:
   //
 
   // 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);
   // 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);
 
 
   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)];
 # 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
 #ifndef USE_BACKBUFFER
 
 - (void) animateOneFrame
@@ -1201,128 +1774,10 @@ double current_device_rotation (void)
 
   [self render_x11];
 
 
   [self render_x11];
 
-# ifdef USE_IPHONE
+# if defined USE_IPHONE && defined USE_BACKBUFFER
   UIGraphicsPopContext();
 # endif
   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
 
 
 #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 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 =
             default:
               {
                 const char *s =
@@ -1662,6 +2129,23 @@ double current_device_rotation (void)
     [super flagsChanged:e];
 }
 
     [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
 
 
 #else  // USE_IPHONE
 
 
@@ -1682,42 +2166,13 @@ double current_device_rotation (void)
 //  }
   [self resignFirstResponder];
 
 //  }
   [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.
   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
   } 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.
 
    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.
 
 
    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
 
    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];
 
 {
   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
   }
 
   /* 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
 
   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();
   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
   }
 
 # 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
 
          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.
  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.
 
 /* 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
 {
  */
 - (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 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;
     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;
 
   *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
 
 
 #endif // USE_IPHONE