From http://www.jwz.org/xscreensaver/xscreensaver-5.34.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
index a122f7f2b50466695cc2053b005bea0ba9141419..657835b9c9b1e5ebe9eeeb4e045cd616b2c75f6a 100644 (file)
@@ -1,13 +1,13 @@
-/* xscreensaver, Copyright (c) 2006, 2007 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
-* the above copyright notice appear in all copies and that both that
-* copyright notice and this permission notice appear in supporting
-* documentation.  No representations are made about the suitability of this
-* software for any purpose.  It is provided "as is" without express or 
-* implied warranty.
-*/
+/* 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
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation.  No representations are made about the suitability of this
+ * software for any purpose.  It is provided "as is" without express or 
+ * implied warranty.
+ */
 
 /* This is a subclass of Apple's ScreenSaverView that knows how to run
    xscreensaver programs without X11 via the dark magic of the "jwxyz"
    the "screenhack.c" module.
  */
 
+#import <QuartzCore/QuartzCore.h>
+#import <sys/mman.h>
+#import <zlib.h>
 #import "XScreenSaverView.h"
 #import "XScreenSaverConfigSheet.h"
+#import "Updater.h"
 #import "screenhackI.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.
+ */
+#ifndef  MAC_OS_X_VERSION_10_6
+# define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
+#endif
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6  /* 10.6 SDK */
+# import <objc/objc-auto.h>
+# 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
@@ -30,19 +53,74 @@ const char *progclass;
 int mono_p = 0;
 
 
+# ifdef USE_IPHONE
+
+#  define NSSizeToCGSize(x) (x)
+
+extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
+
+/* Stub definition of the superclass, for iPhone.
+ */
+@implementation ScreenSaverView
+{
+  NSTimeInterval anim_interval;
+  Bool animating_p;
+  NSTimer *anim_timer;
+}
+
+- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
+  self = [super initWithFrame:frame];
+  if (! self) return 0;
+  anim_interval = 1.0/30;
+  return self;
+}
+- (NSTimeInterval)animationTimeInterval { return anim_interval; }
+- (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
+- (BOOL)hasConfigureSheet { return NO; }
+- (NSWindow *)configureSheet { return nil; }
+- (NSView *)configureView { return nil; }
+- (BOOL)isPreview { return NO; }
+- (BOOL)isAnimating { return animating_p; }
+- (void)animateOneFrame { }
+
+- (void)startAnimation {
+  if (animating_p) return;
+  animating_p = YES;
+  anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
+                        target:self
+                        selector:@selector(animateOneFrame)
+                        userInfo:nil
+                        repeats:YES];
+}
+
+- (void)stopAnimation {
+  if (anim_timer) {
+    [anim_timer invalidate];
+    anim_timer = 0;
+  }
+  animating_p = NO;
+}
+@end
+
+# endif // !USE_IPHONE
+
+
+
+@interface XScreenSaverView (Private)
+- (void) stopAndClose:(Bool)relaunch;
+@end
+
 @implementation XScreenSaverView
 
-- (struct xscreensaver_function_table *) findFunctionTable
+// Given a lower-cased saver name, returns the function table for it.
+// If no name, guess the name from the class's bundle name.
+//
+- (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
 {
   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
-  
+
   NSString *path = [nsb bundlePath];
-  NSString *name = [[[path lastPathComponent] stringByDeletingPathExtension]
-                    lowercaseString];
-  NSString *suffix = @"_xscreensaver_function_table";
-  NSString *table_name = [name stringByAppendingString:suffix];
-  
   CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
                                                (CFStringRef) path,
                                                kCFURLPOSIXPathStyle,
@@ -50,11 +128,34 @@ int mono_p = 0;
   CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
   CFRelease (url);
   NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
+  // #### Analyze says "Potential leak of an object stored into cfb"
   
+  if (! name)
+    name = [[path lastPathComponent] stringByDeletingPathExtension];
+
+  name = [[name lowercaseString]
+           stringByReplacingOccurrencesOfString:@" "
+           withString:@""];
+
+# ifndef USE_IPHONE
+  // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
+  // I'm guessing that symbol-stripping is mandatory.  Fuck.
+  NSString *table_name = [name stringByAppendingString:
+                                 @"_xscreensaver_function_table"];
   void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
-  NSAssert2 (addr, @"no symbol \"%@\" in bundle %@", table_name, path);
+  CFRelease (cfb);
+
+  if (! addr)
+    NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
+
+# else  // USE_IPHONE
+  // Depends on the auto-generated "ios-function-table.m" being up to date.
+  if (! function_tables)
+    function_tables = [make_function_table_dict() retain];
+  NSValue *v = [function_tables objectForKey: name];
+  void *addr = v ? [v pointerValue] : 0;
+# endif // USE_IPHONE
 
-//  NSLog (@"%@ = 0x%08X", table_name, (unsigned long) addr);
   return (struct xscreensaver_function_table *) addr;
 }
 
@@ -79,9 +180,10 @@ int mono_p = 0;
   strcat (npath, opath);
   if (putenv (npath)) {
     perror ("putenv");
-    abort();
+    NSAssert1 (0, @"putenv \"%s\" failed", npath);
   }
-  free (npath);
+
+  /* Don't free (npath) -- MacOS's putenv() does not copy it. */
 }
 
 
@@ -99,9 +201,9 @@ int mono_p = 0;
   strcat (env, s);
   if (putenv (env)) {
     perror ("putenv");
-    abort();
+    NSAssert1 (0, @"putenv \"%s\" failed", env);
   }
-  free (env);
+  /* Don't free (env) -- MacOS's putenv() does not copy it. */
 }
 
 
@@ -114,28 +216,83 @@ add_default_options (const XrmOptionDescRec *opts,
   /* These aren't "real" command-line options (there are no actual command-line
      options in the Cocoa version); but this is the somewhat kludgey way that
      the <xscreensaver-text /> and <xscreensaver-image /> tags in the
-     ../hacks/config/*.xml files communicate with the preferences database.
+     ../hacks/config/\*.xml files communicate with the preferences database.
   */
   static const XrmOptionDescRec default_options [] = {
     { "-text-mode",              ".textMode",          XrmoptionSepArg, 0 },
     { "-text-literal",           ".textLiteral",       XrmoptionSepArg, 0 },
     { "-text-file",              ".textFile",          XrmoptionSepArg, 0 },
     { "-text-url",               ".textURL",           XrmoptionSepArg, 0 },
+    { "-text-program",           ".textProgram",       XrmoptionSepArg, 0 },
     { "-grab-desktop",           ".grabDesktopImages", XrmoptionNoArg, "True" },
     { "-no-grab-desktop",        ".grabDesktopImages", XrmoptionNoArg, "False"},
     { "-choose-random-images",   ".chooseRandomImages",XrmoptionNoArg, "True" },
     { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
     { "-image-directory",        ".imageDirectory",    XrmoptionSepArg, 0 },
+    { "-fps",                    ".doFPS",             XrmoptionNoArg, "True" },
+    { "-no-fps",                 ".doFPS",             XrmoptionNoArg, "False"},
+    { "-foreground",             ".foreground",        XrmoptionSepArg, 0 },
+    { "-fg",                     ".foreground",        XrmoptionSepArg, 0 },
+    { "-background",             ".background",        XrmoptionSepArg, 0 },
+    { "-bg",                     ".background",        XrmoptionSepArg, 0 },
+
+# ifndef USE_IPHONE
+    // <xscreensaver-updater />
+    {    "-" SUSUEnableAutomaticChecksKey,
+         "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True"  },
+    { "-no-" SUSUEnableAutomaticChecksKey,
+         "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
+    {    "-" SUAutomaticallyUpdateKey,
+         "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True"  },
+    { "-no-" SUAutomaticallyUpdateKey,
+         "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
+    {    "-" SUSendProfileInfoKey,
+         "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
+    { "-no-" SUSendProfileInfoKey,
+         "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
+    {    "-" SUScheduledCheckIntervalKey,
+         "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
+# endif // !USE_IPHONE
+
     { 0, 0, 0, 0 }
   };
   static const char *default_defaults [] = {
+    ".doFPS:              False",
+    ".doubleBuffer:       True",
+    ".multiSample:        False",
+# ifndef USE_IPHONE
     ".textMode:           date",
+# else
+    ".textMode:           url",
+# endif
  // ".textLiteral:        ",
  // ".textFile:           ",
- // ".textURL:            ",
+    ".textURL:            https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
+ // ".textProgram:        ",
     ".grabDesktopImages:  yes",
+# ifndef USE_IPHONE
     ".chooseRandomImages: no",
+# else
+    ".chooseRandomImages: yes",
+# endif
     ".imageDirectory:     ~/Pictures",
+    ".relaunchDelay:      2",
+    ".texFontCacheSize:   30",
+
+# ifndef USE_IPHONE
+#  define STR1(S) #S
+#  define STR(S) STR1(S)
+#  define __objc_yes Yes
+#  define __objc_no  No
+    "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
+    "." SUAutomaticallyUpdateKey ":  "    STR(SUAutomaticallyUpdateDef),
+    "." SUSendProfileInfoKey ": "         STR(SUSendProfileInfoDef),
+    "." SUScheduledCheckIntervalKey ": "  STR(SUScheduledCheckIntervalDef),
+#  undef __objc_yes
+#  undef __objc_no
+#  undef STR1
+#  undef STR
+# endif // USE_IPHONE
     0
   };
 
@@ -189,12 +346,55 @@ add_default_options (const XrmOptionDescRec *opts,
 }
 
 
-- (id) initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
+#ifdef USE_IPHONE
+/* Returns the current time in seconds as a double.
+ */
+static double
+double_time (void)
+{
+  struct timeval now;
+# ifdef GETTIMEOFDAY_TWO_ARGS
+  struct timezone tzp;
+  gettimeofday(&now, &tzp);
+# else
+  gettimeofday(&now);
+# endif
+
+  return (now.tv_sec + ((double) now.tv_usec * 0.000001));
+}
+#endif // USE_IPHONE
+
+#if TARGET_IPHONE_SIMULATOR
+static const char *
+orientname(unsigned long o)
+{
+  switch (o) {
+  case UIDeviceOrientationUnknown:             return "Unknown";
+  case UIDeviceOrientationPortrait:            return "Portrait";
+  case UIDeviceOrientationPortraitUpsideDown:  return "PortraitUpsideDown";
+  case UIDeviceOrientationLandscapeLeft:       return "LandscapeLeft";
+  case UIDeviceOrientationLandscapeRight:      return "LandscapeRight";
+  case UIDeviceOrientationFaceUp:              return "FaceUp";
+  case UIDeviceOrientationFaceDown:            return "FaceDown";
+  default:                                     return "ERROR";
+  }
+}
+#endif // TARGET_IPHONE_SIMULATOR
+
+
+- (id) initWithFrame:(NSRect)frame
+           saverName:(NSString *)saverName
+           isPreview:(BOOL)isPreview
 {
   if (! (self = [super initWithFrame:frame isPreview:isPreview]))
     return 0;
   
-  xsft = [self findFunctionTable];
+  xsft = [self findFunctionTable: saverName];
+  if (! xsft) {
+    [self release];
+    return 0;
+  }
+
   [self setShellPath];
 
   setup_p = YES;
@@ -206,7 +406,7 @@ add_default_options (const XrmOptionDescRec *opts,
      "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
    */
   NSString *name = [NSString stringWithCString:xsft->progclass
-                                      encoding:NSUTF8StringEncoding];
+                             encoding:NSISOLatin1StringEncoding];
   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
   [self setResourcesEnv:name];
 
@@ -223,17 +423,90 @@ add_default_options (const XrmOptionDescRec *opts,
   progname = progclass = xsft->progclass;
 
   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
+  double s = 1;
+# endif
+
+  CGSize bb_size;      // pixels, not points
+  bb_size.width  = s * frame.size.width;
+  bb_size.height = s * frame.size.height;
+
+# ifdef USE_IPHONE
+  initial_bounds = rot_current_size = rot_from = rot_to = bb_size;
+  rotation_ratio = -1;
+
+  orientation = UIDeviceOrientationUnknown;
+  [self didRotate:nil];
+  [self initGestures];
+
+  // So we can tell when we're docked.
+  [UIDevice currentDevice].batteryMonitoringEnabled = YES;
+
+  [self setBackgroundColor:[NSColor blackColor]];
+
+  [[NSNotificationCenter defaultCenter]
+   addObserver:self
+   selector:@selector(didRotate:)
+   name:UIDeviceOrientationDidChangeNotification object:nil];
+# endif // USE_IPHONE
+
   return self;
 }
 
+
+#ifdef USE_IPHONE
++ (Class) layerClass
+{
+  return [CAEAGLLayer class];
+}
+#endif
+
+
+- (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
+{
+  return [self initWithFrame:frame saverName:0 isPreview:p];
+}
+
+
 - (void) dealloc
 {
-  NSAssert(![self isAnimating], @"still animating");
+  if ([self isAnimating])
+    [self stopAnimation];
   NSAssert(!xdata, @"xdata not yet freed");
-  if (xdpy)
-    jwxyz_free_display (xdpy);
+  NSAssert(!xdpy, @"xdpy not yet freed");
+
+# ifdef USE_IPHONE
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+# endif
+
+# ifdef USE_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);
+
+# endif // USE_BACKBUFFER
+
   [prefsReader release];
+
+  // xsft
+  // fpst
+
   [super dealloc];
 }
 
@@ -243,15 +516,237 @@ add_default_options (const XrmOptionDescRec *opts,
 }
 
 
+#ifdef USE_IPHONE
+- (void) lockFocus { }
+- (void) unlockFocus { }
+#endif // USE_IPHONE
+
+
+
+# ifdef USE_IPHONE
+/* A few seconds after the saver launches, we store the "wasRunning"
+   preference.  This is so that if the saver is crashing at startup,
+   we don't launch it again next time, getting stuck in a crash loop.
+ */
+- (void) allSystemsGo: (NSTimer *) timer
+{
+  NSAssert (timer == crash_timer, @"crash timer screwed up");
+  crash_timer = 0;
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setBool:YES forKey:@"wasRunning"];
+  [prefs synchronize];
+}
+#endif // USE_IPHONE
+
+
 - (void) startAnimation
 {
   NSAssert(![self isAnimating], @"already animating");
   NSAssert(!initted_p && !xdata, @"already initialized");
+
+  // See comment in render_x11() for why this value is important:
+  [self setAnimationTimeInterval: 1.0 / 240.0];
+
   [super startAnimation];
   /* We can't draw on the window from this method, so we actually do the
      initialization of the screen saver (xsft->init_cb) in the first call
      to animateOneFrame() instead.
    */
+
+# 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];
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs removeObjectForKey:@"wasRunning"];
+  [prefs synchronize];
+
+  crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
+                         target:self
+                         selector:@selector(allSystemsGo:)
+                         userInfo:nil
+                         repeats:NO];
+
+# endif // USE_IPHONE
+
+  // Never automatically turn the screen off if we are docked,
+  // and an animation is running.
+  //
+# ifdef USE_IPHONE
+  [UIApplication sharedApplication].idleTimerDisabled =
+    ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
+  [[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
@@ -262,163 +757,1052 @@ add_default_options (const XrmOptionDescRec *opts,
 
     [self lockFocus];       // in case something tries to draw from here
     [self prepareContext];
+
+    /* I considered just not even calling the free callback at all...
+       But webcollage-cocoa needs it, to kill the inferior webcollage
+       processes (since the screen saver framework never generates a
+       SIGPIPE for them...)  Instead, I turned off the free call in
+       xlockmore.c, which is where all of the bogus calls are anyway.
+     */
     xsft->free_cb (xdpy, xwindow, xdata);
     [self unlockFocus];
 
+    // xdpy must be freed before dealloc is called, because xdpy owns a
+    // circular reference to the parent XScreenSaverView.
+    jwxyz_free_display (xdpy);
+    xdpy = NULL;
+    xwindow = NULL;
+
 //  setup_p = NO; // #### wait, do we need this?
     initted_p = NO;
     xdata = 0;
   }
 
+# ifdef USE_IPHONE
+  if (crash_timer)
+    [crash_timer invalidate];
+  crash_timer = 0;
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs removeObjectForKey:@"wasRunning"];
+  [prefs synchronize];
+# endif // USE_IPHONE
+
   [super stopAnimation];
+
+  // When an animation is no longer running (e.g., looking at the list)
+  // then it's ok to power off the screen when docked.
+  //
+# ifdef USE_IPHONE
+  [UIApplication sharedApplication].idleTimerDisabled = NO;
+  [[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
+
+  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
- */
+// #### 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
+  }
 }
 
-/* Hook for the XScreenSaverGLView subclass
- */
-- (void) resizeContext
-{
-}
 
-- (void) animateOneFrame
+static void
+screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
 {
-  if (!initted_p) {
-
-    if (! xdpy) {
-      xdpy = jwxyz_make_display (self);
-      xwindow = XRootWindow (xdpy, 0);
-    }
+  fps_compute (fpst, 0, -1);
+  fps_draw (fpst);
+}
 
-    if (!setup_p) {
-      setup_p = YES;
-      if (xsft->setup_cb)
-        xsft->setup_cb (xsft, xsft->setup_arg);
-    }
-    initted_p = YES;
-    resized_p = NO;
-    NSAssert(!xdata, @"xdata already initialized");
-    
-# undef ya_rand_init
-    ya_rand_init (0);
-    
-    XSetWindowBackground (xdpy, xwindow,
-                          get_pixel_resource (xdpy, 0,
-                                              "background", "Background"));
-    XClearWindow (xdpy, xwindow);
-    
-    [[self window] setAcceptsMouseMovedEvents:YES];
 
-    /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
-       drawing primitives will run on the GPU instead of the CPU.
-       It seems like it might make things worse rather than better,
-       though...  Plus it makes us binary-incompatible with 10.4.
+#ifdef USE_IPHONE
 
-# if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
-    [[self window] setPreferredBackingLocation:
-                     NSWindowBackingLocationVideoMemory];
-# endif
-     */
+/* On iPhones with Retina displays, we can draw the savers in "real"
+   pixels, and that works great.  The 320x480 "point" screen is really
+   a 640x960 *pixel* screen.  However, Retina iPads have 768x1024
+   point screens which are 1536x2048 pixels, and apparently that's
+   enough pixels that copying those bits to the screen is slow.  Like,
+   drops us from 15fps to 7fps.  So, on Retina iPads, we don't draw in
+   real pixels.  This will probably make the savers look better
+   anyway, since that's a higher resolution than most desktop monitors
+   have even today.  (This is only true for X11 programs, not GL 
+   programs.  Those are fine at full rez.)
 
-    /* Kludge: even though the init_cb functions are declared to take 2 args,
-      actually call them with 3, for the benefit of xlockmore_init() and
-      xlockmore_setup().
-      */
-    void *(*init_cb) (Display *, Window, void *) = 
-      (void *(*) (Display *, Window, void *)) xsft->init_cb;
-    
-    xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
-  }
+   This method is overridden in XScreenSaverGLView, since this kludge
+   isn't necessary for GL programs, being resolution independent by
+   nature.
+ */
+- (CGFloat) hackedContentScaleFactor
+{
+  NSSize ssize = [[[UIScreen mainScreen] currentMode] size];
+  NSSize bsize = [self bounds].size;
 
-  /* I don't understand why we have to do this *every frame*, but we do,
-     or else the cursor comes back on.
-   */
-  if (![self isPreview])
-    [NSCursor setHiddenUntilMouseMoves:YES];
+  CGFloat
+    max_ssize = ssize.width > ssize.height ? ssize.width : ssize.height,
+    max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height;
 
-  /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing.
-     This is bad, because some of the screen hacks want to delay for long 
-     periods (like 5 seconds or a minute!) between frames, and running them
-     all at 60 FPS is no good.
-  
-     So, we don't use setAnimationTimeInterval, and just let the framework call
-     us whenever.  But, we only invoke the screen hack's "draw frame" method
-     when enough time has expired.
-  
-     This means two extra calls to gettimeofday() per frame.  For fast-cycling
-     screen savers, that might actually slow them down.  Oh well.
+  // Ratio of screen size in pixels to view size in points.
+  CGFloat s = max_ssize / max_bsize;
 
-     #### Also, we do not run the draw callback faster than the system's
-          animationTimeInterval, so if any savers are pickier about timing
-          than that, this may slow them down too much.  If that's a problem,
-          then we could call draw_cb in a loop here (with usleep) until the
-          next call would put us past animationTimeInterval...  But a better
-          approach would probably be to just change the saver to not do that.
-   */
-  struct timeval tv;
-  gettimeofday (&tv, 0);
-  double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
-  if (now < next_frame_time) return;
-  
-  [self prepareContext];
+  // Two constraints:
 
-  if (resized_p) {
-    // We do this here instead of in setFrameSize so that all the
-    // Xlib drawing takes place under the animation timer.
-    [self resizeContext];
-    NSRect r = [self frame];
-    xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
-    resized_p = NO;
-  }
+  // 1. Don't exceed -- let's say 1280 pixels in either direction.
+  //    (Otherwise the frame rate gets bad.)
+  CGFloat mag0 = ceil(max_ssize / 1280);
 
-  // Run any XtAppAddInput callbacks now.
-  // (Note that XtAppAddTimeOut callbacks have already been run by
-  // the Cocoa event loop.)
-  //
-  jwxyz_sources_run (display_sources_data (xdpy));
+  // 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);
 
-  // And finally:
-  //
-  unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
-  
-  XSync (xdpy, 0);
-  
-  gettimeofday (&tv, 0);
-  now = tv.tv_sec + (tv.tv_usec / 1000000.0);
-  next_frame_time = now + (delay / 1000000.0);
+  // 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);
 }
 
 
-- (void)drawRect:(NSRect)rect
+static GLfloat _global_rot_current_angle_kludge;
+
+double current_device_rotation (void)
 {
-  if (xwindow)    // clear to the X window's bg color, not necessarily black.
-    XClearWindow (xdpy, xwindow);
-  else
-    [super drawRect:rect];    // early: black.
+  return -_global_rot_current_angle_kludge;
 }
 
 
-- (void) setFrameSize:(NSSize) newSize
+- (void) hackRotation
 {
-  [super setFrameSize:newSize];
-  if ([self isAnimating]) {
-    resized_p = YES;
+  if (rotation_ratio >= 0) {   // in the midst of a rotation animation
+
+#   define CLAMP180(N) while (N < 0) N += 360; while (N > 180) N -= 360
+    GLfloat f = angle_from;
+    GLfloat t = angle_to;
+    CLAMP180(f);
+    CLAMP180(t);
+    GLfloat dist = -(t-f);
+    CLAMP180(dist);
+
+    // Intermediate angle.
+    rot_current_angle = f - rotation_ratio * dist;
+
+    // Intermediate frame size.
+    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();
+    double duration = 1/6.0;
+    rotation_ratio = 1 - ((rot_start_time + duration - now) / duration);
+
+    if (rotation_ratio > 1 || ignore_rotation_p) {     // Done animating.
+      orientation = new_orientation;
+      rot_current_angle = angle_to;
+      rot_current_size = rot_to;
+      rotation_ratio = -1;
+
+# if TARGET_IPHONE_SIMULATOR
+      NSLog (@"rotation ended: %s %d, %d x %d",
+             orientname(orientation), (int) rot_current_angle,
+             (int) rot_current_size.width, (int) rot_current_size.height);
+# endif
+
+      // Check orientation again in case we rotated again while rotating:
+      // this is a no-op if nothing has changed.
+      [self didRotate:nil];
+    }
+  } else {                     // Not animating a rotation.
+    rot_current_angle = angle_to;
+    rot_current_size = rot_to;
   }
-}
 
-- (void) setFrame:(NSRect) newRect
+  CLAMP180(rot_current_angle);
+  _global_rot_current_angle_kludge = rot_current_angle;
+
+#   undef CLAMP180
+
+  CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow])
+                    ? initial_bounds
+                    : rot_current_size);
+  if ((int) backbuffer_size.width  != (int) rotsize.width ||
+      (int) backbuffer_size.height != (int) rotsize.height)
+    [self resize_x11];
+}
+
+
+- (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
+{
+  if (i == 0) exit (-1);       // Cancel
+  [self stopAndClose:NO];      // Keep going
+}
+
+- (void) handleException: (NSException *)e
+{
+  NSLog (@"Caught exception: %@", e);
+  [[[UIAlertView alloc] initWithTitle:
+                          [NSString stringWithFormat: @"%s crashed!",
+                                    xsft->progclass]
+                        message:
+                          [NSString stringWithFormat:
+                                      @"The error message was:"
+                                    "\n\n%@\n\n"
+                                    "If it keeps crashing, try "
+                                    "resetting its options.",
+                                    e]
+                        delegate: self
+                        cancelButtonTitle: @"Exit"
+                        otherButtonTitles: @"Keep going", nil]
+    show];
+  [self stopAnimation];
+}
+
+#endif // USE_IPHONE
+
+
+#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
+   and shape as the X11 window as seen by the hacks.
+ */
+- (void) createBackbuffer:(CGSize)new_size
+{
+  // Colorspaces and CGContexts only happen with non-GL hacks.
+  if (colorspace)
+    CGColorSpaceRelease (colorspace);
+
+# 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];
+
+# ifdef BACKBUFFER_OPENGL
+    // Was apparently faster until 10.9.
+    colorspace = CGColorSpaceCreateDeviceRGB ();
+# endif // BACKBUFFER_OPENGL
+
+    [self unlockFocus];
+  } else {
+    colorspace = CGColorSpaceCreateDeviceRGB();
+  }
+
+  if (backbuffer &&
+      (int)backbuffer_size.width  == (int)new_size.width &&
+      (int)backbuffer_size.height == (int)new_size.height)
+    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.
+
+# if TARGET_IPHONE_SIMULATOR
+  NSLog(@"backbuffer %.0fx%.0f",
+        backbuffer_size.width, backbuffer_size.height);
+# endif
+
+  /* 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,
+                                      bytes_per_row,
+                                      colorspace,
+                                      bitmap_info);
+  NSAssert (backbuffer, @"unable to allocate back buffer");
+
+  // Clear it.
+  CGRect r;
+  r.origin.x = r.origin.y = 0;
+  r.size = backbuffer_size;
+  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.
+
+    CGRect rect;   // pixels, not points
+    rect.origin.x = 0;
+    rect.origin.y = (backbuffer_size.height - osize.height);
+    rect.size = osize;
+
+    CGImageRef img = CGBitmapContextCreateImage (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
+
+
+/* Inform X11 that the size of our window has changed.
+ */
+- (void) resize_x11
+{
+  if (!xwindow) return;  // early
+
+  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
+                    : rot_current_size);
+  new_size.width  = rotsize.width;
+  new_size.height = rotsize.height;
+#  else  // !USE_IPHONE
+  new_size = NSSizeToCGSize([self bounds].size);
+#  endif // !USE_IPHONE
+
+  [self createBackbuffer:new_size];
+  jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height,
+                        backbuffer);
+# else   // !USE_BACKBUFFER
+  new_size = [self bounds].size;
+  jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height,
+                        0);
+# endif  // !USE_BACKBUFFER
+
+# if TARGET_IPHONE_SIMULATOR
+  NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
+# endif
+
+  // Next time render_x11 is called, run the saver's reshape_cb.
+  resized_p = YES;
+}
+
+
+- (void) render_x11
+{
+# ifdef USE_IPHONE
+  @try {
+
+  if (orientation == UIDeviceOrientationUnknown)
+    [self didRotate:nil];
+  [self hackRotation];
+# endif
+
+  if (!initted_p) {
+
+    if (! xdpy) {
+# ifdef USE_BACKBUFFER
+      NSAssert (backbuffer, @"no back buffer");
+      xdpy = jwxyz_make_display (self, backbuffer);
+# else
+      xdpy = jwxyz_make_display (self, 0);
+# endif
+      xwindow = XRootWindow (xdpy, 0);
+
+# ifdef USE_IPHONE
+      /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
+      ignore_rotation_p =
+        get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
+# endif // USE_IPHONE
+
+      [self resize_x11];
+    }
+
+    if (!setup_p) {
+      setup_p = YES;
+      if (xsft->setup_cb)
+        xsft->setup_cb (xsft, xsft->setup_arg);
+    }
+    initted_p = YES;
+    resized_p = NO;
+    NSAssert(!xdata, @"xdata already initialized");
+
+
+# undef ya_rand_init
+    ya_rand_init (0);
+    
+    XSetWindowBackground (xdpy, xwindow,
+                          get_pixel_resource (xdpy, 0,
+                                              "background", "Background"));
+    XClearWindow (xdpy, xwindow);
+    
+# ifndef USE_IPHONE
+    [[self window] setAcceptsMouseMovedEvents:YES];
+# endif
+
+    /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
+       drawing primitives will run on the GPU instead of the CPU.
+       It seems like it might make things worse rather than better,
+       though...  Plus it makes us binary-incompatible with 10.4.
+
+# if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
+    [[self window] setPreferredBackingLocation:
+                     NSWindowBackingLocationVideoMemory];
+# endif
+     */
+
+    /* Kludge: even though the init_cb functions are declared to take 2 args,
+      actually call them with 3, for the benefit of xlockmore_init() and
+      xlockmore_setup().
+      */
+    void *(*init_cb) (Display *, Window, void *) = 
+      (void *(*) (Display *, Window, void *)) xsft->init_cb;
+    
+    xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
+    // NSAssert(xdata, @"no xdata from init");
+    if (! xdata) abort();
+
+    if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
+      fpst = fps_init (xdpy, xwindow);
+      if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
+    } else {
+      fpst = NULL;
+      xsft->fps_cb = 0;
+    }
+
+    [self checkForUpdates];
+  }
+
+
+  /* I don't understand why we have to do this *every frame*, but we do,
+     or else the cursor comes back on.
+   */
+# ifndef USE_IPHONE
+  if (![self isPreview])
+    [NSCursor setHiddenUntilMouseMoves:YES];
+# endif
+
+
+  if (fpst)
+    {
+      /* This is just a guess, but the -fps code wants to know how long
+         we were sleeping between frames.
+       */
+      long usecs = 1000000 * [self animationTimeInterval];
+      usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
+      if (usecs < 0) usecs = 0;
+      fps_slept (fpst, usecs);
+    }
+
+
+  /* It turns out that on some systems (possibly only 10.5 and older?)
+     [ScreenSaverView setAnimationTimeInterval] does nothing.  This means
+     that we cannot rely on it.
+
+     Some of the screen hacks want to delay for long periods, and letting the
+     framework run the update function at 30 FPS when it really wanted half a
+     minute between frames would be bad.  So instead, we assume that the
+     framework's animation timer might fire whenever, but we only invoke the
+     screen hack's "draw frame" method when enough time has expired.
+  
+     This means two extra calls to gettimeofday() per frame.  For fast-cycling
+     screen savers, that might actually slow them down.  Oh well.
+
+     A side-effect of this is that it's not possible for a saver to request
+     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.  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
+     animationTimeInterval (33333 Âµs) and a screenhack takes 40000 Âµs for a
+     frame, there will be a 26666 Âµs delay until the next frame, 66666 Âµs
+     after the beginning of the current frame.  In other words, 25 FPS
+     becomes 15 FPS.
+
+     Frame rates tend to snap to values of 30/N, where N is a positive
+     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/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.
+   */
+  struct timeval tv;
+  gettimeofday (&tv, 0);
+  double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
+  if (now < next_frame_time) return;
+
+  [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.
+
+# ifndef USE_IPHONE
+    if (ogl_ctx)
+      [ogl_ctx setView:self];
+# endif // !USE_IPHONE
+
+    NSRect r;
+# ifndef USE_BACKBUFFER
+    r = [self bounds];
+# else  // USE_BACKBUFFER
+    r.origin.x = 0;
+    r.origin.y = 0;
+    r.size.width  = backbuffer_size.width;
+    r.size.height = backbuffer_size.height;
+# endif // USE_BACKBUFFER
+
+    xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height);
+    resized_p = NO;
+  }
+
+  // Run any XtAppAddInput callbacks now.
+  // (Note that XtAppAddTimeOut callbacks have already been run by
+  // the Cocoa event loop.)
+  //
+  jwxyz_sources_run (display_sources_data (xdpy));
+
+
+  // And finally:
+  //
+  // NSAssert(xdata, @"no xdata when drawing");
+  if (! xdata) abort();
+  unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
+  if (fpst && xsft->fps_cb)
+    xsft->fps_cb (xdpy, xwindow, fpst, xdata);
+
+  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)];
+# endif
+
+# ifdef DO_GC_HACKERY
+  /* Current theory is that the 10.6 garbage collector sucks in the
+     following way:
+
+     It only does a collection when a threshold of outstanding
+     collectable allocations has been surpassed.  However, CoreGraphics
+     creates lots of small collectable allocations that contain pointers
+     to very large non-collectable allocations: a small CG object that's
+     collectable referencing large malloc'd allocations (non-collectable)
+     containing bitmap data.  So the large allocation doesn't get freed
+     until GC collects the small allocation, which triggers its finalizer
+     to run which frees the large allocation.  So GC is deciding that it
+     doesn't really need to run, even though the process has gotten
+     enormous.  GC eventually runs once pageouts have happened, but by
+     then it's too late, and the machine's resident set has been
+     sodomized.
+
+     So, we force an exhaustive garbage collection in this process
+     approximately every 5 seconds whether the system thinks it needs 
+     one or not.
+  */
+  {
+    static int tick = 0;
+    if (++tick > 5*30) {
+      tick = 0;
+      objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
+    }
+  }
+# endif // DO_GC_HACKERY
+
+# ifdef USE_IPHONE
+  }
+  @catch (NSException *e) {
+    [self handleException: e];
+  }
+# endif // USE_IPHONE
+}
+
+
+#ifndef USE_BACKBUFFER
+
+- (void) animateOneFrame
+{
+  [self render_x11];
+  jwxyz_flush_context(xdpy);
+}
+
+#else  // USE_BACKBUFFER
+
+- (void) animateOneFrame
+{
+  // Render X11 into the backing store bitmap...
+
+  NSAssert (backbuffer, @"no back buffer");
+
+# ifdef USE_IPHONE
+  UIGraphicsPushContext (backbuffer);
+# endif
+
+  [self render_x11];
+
+# if defined USE_IPHONE && defined USE_BACKBUFFER
+  UIGraphicsPopContext();
+# endif
+}
+
+#endif // USE_BACKBUFFER
+
+
+
+- (void) setFrame:(NSRect) newRect
 {
   [super setFrame:newRect];
-  if (xwindow)   // inform Xlib that the window has changed.
-    jwxyz_window_resized (xdpy, xwindow);
+
+  if (xwindow)     // inform Xlib that the window has changed now.
+    [self resize_x11];
 }
 
 
+# ifndef USE_IPHONE  // Doesn't exist on iOS
+- (void) setFrameSize:(NSSize) newSize
+{
+  [super setFrameSize:newSize];
+  if (xwindow)
+    [self resize_x11];
+}
+# endif // !USE_IPHONE
+
+
 +(BOOL) performGammaFade
 {
   return YES;
@@ -429,11 +1813,49 @@ add_default_options (const XrmOptionDescRec *opts,
   return YES;
 }
 
++ (NSString *) decompressXML: (NSData *)data
+{
+  if (! data) return 0;
+  BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
+
+  // If it's not already XML, decompress it.
+  NSAssert (compressed_p, @"xml isn't compressed");
+  if (compressed_p) {
+    NSMutableData *data2 = 0;
+    int ret = -1;
+    z_stream zs;
+    memset (&zs, 0, sizeof(zs));
+    ret = inflateInit2 (&zs, 16 + MAX_WBITS);
+    if (ret == Z_OK) {
+      UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
+      data2 = [NSMutableData dataWithLength: usize];
+      zs.next_in   = (Bytef *) data.bytes;
+      zs.avail_in  = (uint) data.length;
+      zs.next_out  = (Bytef *) data2.bytes;
+      zs.avail_out = (uint) data2.length;
+      ret = inflate (&zs, Z_FINISH);
+      inflateEnd (&zs);
+    }
+    if (ret == Z_OK || ret == Z_STREAM_END)
+      data = data2;
+    else
+      NSAssert2 (0, @"gunzip error: %d: %s",
+                 ret, (zs.msg ? zs.msg : "<null>"));
+  }
+
+  return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+}
+
+
+#ifndef USE_IPHONE
 - (NSWindow *) configureSheet
+#else
+- (UIViewController *) configureView
+#endif
 {
   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
   NSString *file = [NSString stringWithCString:xsft->progclass
-                                      encoding:NSUTF8StringEncoding];
+                                      encoding:NSISOLatin1StringEncoding];
   file = [file lowercaseString];
   NSString *path = [bundle pathForResource:file ofType:@"xml"];
   if (!path) {
@@ -442,38 +1864,93 @@ add_default_options (const XrmOptionDescRec *opts,
     return nil;
   }
   
-  NSWindow *sheet = [[XScreenSaverConfigSheet alloc]
-                     initWithXMLFile:path
-                             options:xsft->options
-                          controller:[prefsReader userDefaultsController]];
-  
+# ifdef USE_IPHONE
+  UIViewController *sheet;
+# else  // !USE_IPHONE
+  NSWindow *sheet;
+# endif // !USE_IPHONE
+
+  NSData *xmld = [NSData dataWithContentsOfFile:path];
+  NSString *xml = [[self class] decompressXML: xmld];
+  sheet = [[XScreenSaverConfigSheet alloc]
+            initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
+                options:xsft->options
+             controller:[prefsReader userDefaultsController]
+       globalController:[prefsReader globalDefaultsController]
+               defaults:[prefsReader defaultOptions]];
+
   // #### am I expected to retain this, or not? wtf.
   //      I thought not, but if I don't do this, we (sometimes) crash.
-  [sheet retain];
+  // #### Analyze says "potential leak of an object stored into sheet"
+  // [sheet retain];
 
   return sheet;
 }
 
 
+- (NSUserDefaultsController *) userDefaultsController
+{
+  return [prefsReader userDefaultsController];
+}
+
+
 /* Announce our willingness to accept keyboard input.
-*/
+ */
 - (BOOL)acceptsFirstResponder
 {
   return YES;
 }
 
 
+- (void) beep
+{
+# ifndef USE_IPHONE
+  NSBeep();
+# else // USE_IPHONE 
+
+  // There's no way to play a standard system alert sound!
+  // We'd have to include our own WAV for that.
+  //
+  // Or we could vibrate:
+  // #import <AudioToolbox/AudioToolbox.h>
+  // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
+  //
+  // Instead, just flash the screen white, then fade.
+  //
+  UIView *v = [[UIView alloc] initWithFrame: [self frame]]; 
+  [v setBackgroundColor: [UIColor whiteColor]];
+  [[self window] addSubview:v];
+  [UIView animateWithDuration: 0.1
+          animations:^{ [v setAlpha: 0.0]; }
+          completion:^(BOOL finished) { [v removeFromSuperview]; } ];
+
+# endif  // USE_IPHONE
+}
+
+
+/* Send an XEvent to the hack.  Returns YES if it was handled.
+ */
+- (BOOL) sendEvent: (XEvent *) e
+{
+  if (!initted_p || ![self isAnimating]) // no event handling unless running.
+    return NO;
+
+  [self lockFocus];
+  [self prepareContext];
+  BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
+  [self unlockFocus];
+  return result;
+}
+
+
+#ifndef USE_IPHONE
+
 /* Convert an NSEvent into an XEvent, and pass it along.
    Returns YES if it was handled.
  */
-- (BOOL) doEvent: (NSEvent *) e
+- (BOOL) convertEvent: (NSEvent *) e
             type: (int) type
 {
-  if (![self isPreview] ||     // no event handling if actually screen-saving!
-      ![self isAnimating] ||
-      !initted_p)
-    return NO;
-  
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
   
@@ -488,9 +1965,14 @@ add_default_options (const XrmOptionDescRec *opts,
   
   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
                                             toView:self];
-  int x = p.x;
-  int y = [self frame].size.height - p.y;
-  
+# ifdef USE_IPHONE
+  double s = [self hackedContentScaleFactor];
+# else
+  int s = 1;
+# endif
+  int x = s * p.x;
+  int y = s * ([self bounds].size.height - p.y);
+
   xe.xany.type = type;
   switch (type) {
     case ButtonPress:
@@ -515,84 +1997,723 @@ add_default_options (const XrmOptionDescRec *opts,
     case KeyPress:
     case KeyRelease:
       {
-        NSString *nss = [e characters];
-        const char *s = [nss cStringUsingEncoding:NSUTF8StringEncoding];
-        xe.xkey.keycode = (s && *s ? *s : 0);
+        NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
+                        [e charactersIgnoringModifiers]);
+        KeySym k = 0;
+
+        if (!ns || [ns length] == 0)                   // dead key
+          {
+            // Cocoa hides the difference between left and right keys.
+            // Also we only get KeyPress events for these, no KeyRelease
+            // (unless we hack the mod state manually.  Bleh.)
+            //
+            if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
+            else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
+            else if (flags & NSControlKeyMask)      k = XK_Control_L;
+            else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
+            else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
+          }
+        else if ([ns length] == 1)                     // real key
+          {
+            switch ([ns characterAtIndex:0]) {
+            case NSLeftArrowFunctionKey:  k = XK_Left;      break;
+            case NSRightArrowFunctionKey: k = XK_Right;     break;
+            case NSUpArrowFunctionKey:    k = XK_Up;        break;
+            case NSDownArrowFunctionKey:  k = XK_Down;      break;
+            case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
+            case NSPageDownFunctionKey:   k = XK_Page_Down; break;
+            case NSHomeFunctionKey:       k = XK_Home;      break;
+            case NSPrevFunctionKey:       k = XK_Prior;     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 =
+                  [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
+                k = (s && *s ? *s : 0);
+              }
+              break;
+            }
+          }
+
+        if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
+
+        xe.xkey.keycode = k;
         xe.xkey.state = state;
         break;
       }
     default:
-      abort();
+      NSAssert1 (0, @"unknown X11 event type: %d", type);
+      break;
   }
 
-  [self lockFocus];
-  [self prepareContext];
-  BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe);
-  [self unlockFocus];
-  return result;
+  return [self sendEvent: &xe];
 }
 
 
 - (void) mouseDown: (NSEvent *) e
 {
-  if (! [self doEvent:e type:ButtonPress])
+  if (! [self convertEvent:e type:ButtonPress])
     [super mouseDown:e];
 }
 
 - (void) mouseUp: (NSEvent *) e
 {
-  if (! [self doEvent:e type:ButtonRelease])
+  if (! [self convertEvent:e type:ButtonRelease])
     [super mouseUp:e];
 }
 
 - (void) otherMouseDown: (NSEvent *) e
 {
-  if (! [self doEvent:e type:ButtonPress])
+  if (! [self convertEvent:e type:ButtonPress])
     [super otherMouseDown:e];
 }
 
 - (void) otherMouseUp: (NSEvent *) e
 {
-  if (! [self doEvent:e type:ButtonRelease])
+  if (! [self convertEvent:e type:ButtonRelease])
     [super otherMouseUp:e];
 }
 
 - (void) mouseMoved: (NSEvent *) e
 {
-  if (! [self doEvent:e type:MotionNotify])
+  if (! [self convertEvent:e type:MotionNotify])
     [super mouseMoved:e];
 }
 
 - (void) mouseDragged: (NSEvent *) e
 {
-  if (! [self doEvent:e type:MotionNotify])
+  if (! [self convertEvent:e type:MotionNotify])
     [super mouseDragged:e];
 }
 
 - (void) otherMouseDragged: (NSEvent *) e
 {
-  if (! [self doEvent:e type:MotionNotify])
+  if (! [self convertEvent:e type:MotionNotify])
     [super otherMouseDragged:e];
 }
 
 - (void) scrollWheel: (NSEvent *) e
 {
-  if (! [self doEvent:e type:ButtonPress])
+  if (! [self convertEvent:e type:ButtonPress])
     [super scrollWheel:e];
 }
 
 - (void) keyDown: (NSEvent *) e
 {
-  if (! [self doEvent:e type:KeyPress])
+  if (! [self convertEvent:e type:KeyPress])
     [super keyDown:e];
 }
 
 - (void) keyUp: (NSEvent *) e
 {
-  if (! [self doEvent:e type:KeyRelease])
+  if (! [self convertEvent:e type:KeyRelease])
     [super keyUp:e];
 }
 
+- (void) flagsChanged: (NSEvent *) e
+{
+  if (! [self convertEvent:e type:KeyPress])
+    [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
+
+
+- (void) stopAndClose:(Bool)relaunch_p
+{
+  if ([self isAnimating])
+    [self stopAnimation];
+
+  /* Need to make the SaverListController be the firstResponder again
+     so that it can continue to receive its own shake events.  I
+     suppose that this abstraction-breakage means that I'm adding
+     XScreenSaverView to the UINavigationController wrong...
+   */
+//  UIViewController *v = [[self window] rootViewController];
+//  if ([v isKindOfClass: [UINavigationController class]]) {
+//    UINavigationController *n = (UINavigationController *) v;
+//    [[n topViewController] becomeFirstResponder];
+//  }
+  [self resignFirstResponder];
+
+  if (relaunch_p) {   // Fake a shake on the SaverListController.
+    [_delegate didShake:self];
+  } else {     // Not launching another, animate our return to the list.
+# if TARGET_IPHONE_SIMULATOR
+    NSLog (@"fading back to saver list");
+# endif
+    [_delegate wantsFadeOut:self];
+  }
+}
+
+
+/* Whether the shape of the X11 Window should be changed to HxW when the
+   device is in a landscape orientation.  X11 hacks want this, but OpenGL
+   hacks do not.
+ */
+- (BOOL)reshapeRotatedWindow
+{
+  return YES;
+}
+
+
+/* Called after the device's orientation has changed.
+   
+   Rotation is complicated: the UI, X11 and OpenGL work in 3 different ways.
+
+   The UI (list of savers, preferences panels) is rotated by the system,
+   because its UIWindow is under a UINavigationController that does
+   automatic rotation, using Core Animation.
+
+   The savers are under a different UIWindow and a UINavigationController
+   that does not do automatic rotation.
+
+   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 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
+   unrotated, so the GL hacks themselves are responsible for rotating the
+   GL scene to match current_device_rotation().
+
+   Touch events are converted to mouse clicks, and those XEvent coordinates
+   are reported in the coordinate system currently in use by the X11 window.
+   Again, GL must convert those.
+ */
+- (void)didRotate:(NSNotification *)notification
+{
+  UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
+
+  /* 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 ||
+    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
+     an orientation change event with an unknown orientation.  Those seem
+     to always be immediately followed by another orientation change with
+     a *real* orientation change, so let's try just ignoring those bogus
+     ones and hoping that the real one comes in shortly...
+   */
+  if (current == UIDeviceOrientationUnknown)
+    return;
+
+  if (rotation_ratio >= 0) return;     // in the midst of rotation animation
+  if (orientation == current) return;  // no change
+
+  new_orientation = current;           // current animation target
+  rotation_ratio = 0;                  // start animating
+  rot_start_time = double_time();
+
+  switch (orientation) {
+  case UIDeviceOrientationLandscapeLeft:      angle_from = 90;  break;
+  case UIDeviceOrientationLandscapeRight:     angle_from = 270; break;
+  case UIDeviceOrientationPortraitUpsideDown: angle_from = 180; break;
+  default:                                    angle_from = 0;   break;
+  }
+
+  switch (new_orientation) {
+  case UIDeviceOrientationLandscapeLeft:      angle_to = 90;  break;
+  case UIDeviceOrientationLandscapeRight:     angle_to = 270; break;
+  case UIDeviceOrientationPortraitUpsideDown: angle_to = 180; break;
+  default:                                    angle_to = 0;   break;
+  }
+
+  switch (orientation) {
+  case UIDeviceOrientationLandscapeRight:      // from landscape
+  case UIDeviceOrientationLandscapeLeft:
+    rot_from.width  = initial_bounds.height;
+    rot_from.height = initial_bounds.width;
+    break;
+  default:                                     // from portrait
+    rot_from.width  = initial_bounds.width;
+    rot_from.height = initial_bounds.height;
+    break;
+  }
+
+  switch (new_orientation) {
+  case UIDeviceOrientationLandscapeRight:      // to landscape
+  case UIDeviceOrientationLandscapeLeft:
+    rot_to.width  = initial_bounds.height;
+    rot_to.height = initial_bounds.width;
+    break;
+  default:                                     // to portrait
+    rot_to.width  = initial_bounds.width;
+    rot_to.height = initial_bounds.height;
+    break;
+  }
+
+# if TARGET_IPHONE_SIMULATOR
+  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.
+   rot_start_time = 0;  // dawn of time
+   [self hackRotation];
+ }
+}
+
+
+/* We distinguish between taps and drags.
+
+   - Drags/pans (down, motion, up) are sent to the saver to handle.
+   - Single-taps exit the saver.
+   - Double-taps are sent to the saver as a "Space" keypress.
+   - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
+
+   This means a saver cannot respond to a single-tap.  Only a few try to.
+ */
+
+- (void)initGestures
+{
+  UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
+                                   initWithTarget:self
+                                   action:@selector(handleDoubleTap)];
+  dtap.numberOfTapsRequired = 2;
+  dtap.numberOfTouchesRequired = 1;
+
+  UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
+                                   initWithTarget:self
+                                   action:@selector(handleTap)];
+  stap.numberOfTapsRequired = 1;
+  stap.numberOfTouchesRequired = 1;
+  UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
+                                  initWithTarget:self
+                                  action:@selector(handlePan:)];
+  pan.maximumNumberOfTouches = 1;
+  pan.minimumNumberOfTouches = 1;
+  // I couldn't get Swipe to work, but using a second Pan recognizer works.
+  UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
+                                   initWithTarget:self
+                                   action:@selector(handlePan2:)];
+  pan2.maximumNumberOfTouches = 2;
+  pan2.minimumNumberOfTouches = 2;
+
+  // Also handle long-touch, and treat that the same as Pan.
+  // Without this, panning doesn't start until there's motion, so the trick
+  // of holding down your finger to freeze the scene doesn't work.
+  //
+  UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
+                                         initWithTarget:self
+                                         action:@selector(handleLongPress:)];
+  hold.numberOfTapsRequired = 0;
+  hold.numberOfTouchesRequired = 1;
+  hold.minimumPressDuration = 0.25;   /* 1/4th second */
+
+  [stap requireGestureRecognizerToFail: dtap];
+  [stap requireGestureRecognizerToFail: hold];
+  [dtap requireGestureRecognizerToFail: hold];
+  [pan  requireGestureRecognizerToFail: hold];
+
+  [self setMultipleTouchEnabled:YES];
+
+  [self addGestureRecognizer: dtap];
+  [self addGestureRecognizer: stap];
+  [self addGestureRecognizer: pan];
+  [self addGestureRecognizer: pan2];
+  [self addGestureRecognizer: hold];
+
+  [dtap release];
+  [stap release];
+  [pan  release];
+  [pan2 release];
+  [hold release];
+}
+
+
+/* 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 xx = *x, yy = *y;
+
+# 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;
+      break;
+    case UIDeviceOrientationLandscapeLeft:
+      swap = xx; xx = yy; yy = w-swap;
+      break;
+    case UIDeviceOrientationPortraitUpsideDown: 
+      xx = w-xx; yy = h-yy;
+    default:
+      break;
+    }
+  }
+
+  double s = [self hackedContentScaleFactor];
+  *x = xx * s;
+  *y = yy * s;
+
+# 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
+}
+
+
+/* Single click exits saver.
+ */
+- (void) handleTap
+{
+  [self stopAndClose:NO];
+}
+
+
+/* Double click sends Space KeyPress.
+ */
+- (void) handleDoubleTap
+{
+  if (!xsft->event_cb || !xwindow) return;
+
+  XEvent xe;
+  memset (&xe, 0, sizeof(xe));
+  xe.xkey.keycode = ' ';
+  xe.xany.type = KeyPress;
+  BOOL ok1 = [self sendEvent: &xe];
+  xe.xany.type = KeyRelease;
+  BOOL ok2 = [self sendEvent: &xe];
+  if (!(ok1 || ok2))
+    [self beep];
+}
+
+
+/* Drag with one finger down: send MotionNotify.
+ */
+- (void) handlePan:(UIGestureRecognizer *)sender
+{
+  if (!xsft->event_cb || !xwindow) return;
+
+  XEvent xe;
+  memset (&xe, 0, sizeof(xe));
+
+  CGPoint p = [sender locationInView:self];  // this is in points, not pixels
+  int x = p.x;
+  int y = p.y;
+  [self convertMouse: rot_current_angle x:&x y:&y];
+  jwxyz_mouse_moved (xdpy, xwindow, x, y);
+
+  switch (sender.state) {
+  case UIGestureRecognizerStateBegan:
+    xe.xany.type = ButtonPress;
+    xe.xbutton.button = 1;
+    xe.xbutton.x = x;
+    xe.xbutton.y = y;
+    break;
+
+  case UIGestureRecognizerStateEnded:
+    xe.xany.type = ButtonRelease;
+    xe.xbutton.button = 1;
+    xe.xbutton.x = x;
+    xe.xbutton.y = y;
+    break;
+
+  case UIGestureRecognizerStateChanged:
+    xe.xany.type = MotionNotify;
+    xe.xmotion.x = x;
+    xe.xmotion.y = y;
+    break;
+
+  default:
+    break;
+  }
+
+  BOOL ok = [self sendEvent: &xe];
+  if (!ok && xe.xany.type == ButtonRelease)
+    [self beep];
+}
+
+
+/* Hold one finger down: assume we're about to start dragging.
+   Treat the same as Pan.
+ */
+- (void) handleLongPress:(UIGestureRecognizer *)sender
+{
+  [self handlePan:sender];
+}
+
+
+
+/* Drag with 2 fingers down: send arrow keys.
+ */
+- (void) handlePan2:(UIPanGestureRecognizer *)sender
+{
+  if (!xsft->event_cb || !xwindow) return;
+
+  if (sender.state != UIGestureRecognizerStateEnded)
+    return;
+
+  XEvent xe;
+  memset (&xe, 0, sizeof(xe));
+
+  CGPoint p = [sender locationInView:self];  // this is in points, not pixels
+  int x = p.x;
+  int y = p.y;
+  [self convertMouse: rot_current_angle x:&x y:&y];
+
+  if (abs(x) > abs(y))
+    xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
+  else
+    xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
+
+  BOOL ok1 = [self sendEvent: &xe];
+  xe.xany.type = KeyRelease;
+  BOOL ok2 = [self sendEvent: &xe];
+  if (!(ok1 || ok2))
+    [self beep];
+}
+
+
+/* We need this to respond to "shake" gestures
+ */
+- (BOOL)canBecomeFirstResponder
+{
+  return YES;
+}
+
+- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+}
+
+
+- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+}
+
+/* Shake means exit and launch a new saver.
+ */
+- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+  [self stopAndClose:YES];
+}
+
+
+- (void)setScreenLocked:(BOOL)locked
+{
+  if (screenLocked == locked) return;
+  screenLocked = locked;
+  if (locked) {
+    if ([self isAnimating])
+      [self stopAnimation];
+  } else {
+    if (! [self isAnimating])
+      [self startAnimation];
+  }
+}
+
+- (NSDictionary *)getGLProperties
+{
+  return [NSDictionary dictionaryWithObjectsAndKeys:
+          kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
+          nil];
+}
+
+- (void)addExtraRenderbuffers:(CGSize)size
+{
+  // No extra renderbuffers are needed for 2D screenhacks.
+}
+
+#endif // USE_IPHONE
+
+
+- (void) checkForUpdates
+{
+# ifndef USE_IPHONE
+  // We only check once at startup, even if there are multiple screens,
+  // and even if this saver is running for many days.
+  // (Uh, except this doesn't work because this static isn't shared,
+  // even if we make it an exported global. Not sure why. Oh well.)
+  static BOOL checked_p = NO;
+  if (checked_p) return;
+  checked_p = YES;
+
+  // If it's off, don't bother running the updater.  Otherwise, the
+  // updater will decide if it's time to hit the network.
+  if (! get_boolean_resource (xdpy,
+                              SUSUEnableAutomaticChecksKey,
+                              SUSUEnableAutomaticChecksKey))
+    return;
+
+  NSString *updater = @"XScreenSaverUpdater.app";
+
+  // There may be multiple copies of the updater: e.g., one in /Applications
+  // and one in the mounted installer DMG!  It's important that we run the
+  // one from the disk and not the DMG, so search for the right one.
+  //
+  NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+  NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+  NSArray *search =
+    @[[[bundle bundlePath] stringByDeletingLastPathComponent],
+      [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
+      @"/Library/Screen Savers",
+      @"/System/Library/Screen Savers",
+      @"/Applications",
+      @"/Applications/Utilities"];
+  NSString *app_path = nil;
+  for (NSString *dir in search) {
+    NSString *p = [dir stringByAppendingPathComponent:updater];
+    if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
+      app_path = p;
+      break;
+    }
+  }
+
+  if (! app_path)
+    app_path = [workspace fullPathForApplication:updater];
+
+  if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
+    app_path = 0;  // The DMG version will not do.
+
+  if (!app_path) {
+    NSLog(@"Unable to find %@", updater);
+    return;
+  }
+
+  NSError *err = nil;
+  if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
+                   options:(NSWorkspaceLaunchWithoutAddingToRecents |
+                            NSWorkspaceLaunchWithoutActivation |
+                            NSWorkspaceLaunchAndHide)
+                   configuration:nil
+                   error:&err]) {
+    NSLog(@"Unable to launch %@: %@", app_path, err);
+  }
+
+# endif // !USE_IPHONE
+}
+
 
 @end
 
@@ -603,7 +2724,7 @@ static PrefsReader *
 get_prefsReader (Display *dpy)
 {
   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
-  if (!view) abort();
+  if (!view) return 0;
   return [view prefsReader];
 }