From http://www.jwz.org/xscreensaver/xscreensaver-5.38.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
index 9ab94bc48e26fd2b3b53557a783ce43afe4fb63e..1ec05a7df80254fd1fa92b42ab892dcb5a2075a4 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2015 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2017 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
 #import "XScreenSaverConfigSheet.h"
 #import "Updater.h"
 #import "screenhackI.h"
 #import "XScreenSaverConfigSheet.h"
 #import "Updater.h"
 #import "screenhackI.h"
-#import "xlockmoreI.h"
+#import "pow2.h"
+#import "jwxyzI.h"
+#import "jwxyz-cocoa.h"
 #import "jwxyz-timers.h"
 
 #import "jwxyz-timers.h"
 
-#ifndef USE_IPHONE
+#ifdef USE_IPHONE
+// XScreenSaverView.m speaks OpenGL ES just fine, but enableBackbuffer does
+// need (jwzgles_)gluCheckExtension.
+# import "jwzglesI.h"
+#else
 # import <OpenGL/glu.h>
 #endif
 
 # import <OpenGL/glu.h>
 #endif
 
 #ifndef  MAC_OS_X_VERSION_10_6
 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
 #endif
 #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 */
+#ifndef  MAC_OS_X_VERSION_10_12
+# define MAC_OS_X_VERSION_10_12 101200
+#endif
+#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 && \
+     MAC_OS_X_VERSION_MAX_ALLOWED <  MAC_OS_X_VERSION_10_12)
+  /* 10.6 SDK or later, and earlier than 10.12 SDK */
 # import <objc/objc-auto.h>
 # define DO_GC_HACKERY
 #endif
 # import <objc/objc-auto.h>
 # define DO_GC_HACKERY
 #endif
@@ -107,6 +118,7 @@ extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
 
 
 @interface XScreenSaverView (Private)
 
 
 @interface XScreenSaverView (Private)
+- (void) stopAndClose;
 - (void) stopAndClose:(Bool)relaunch;
 @end
 
 - (void) stopAndClose:(Bool)relaunch;
 @end
 
@@ -173,17 +185,16 @@ extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
   const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
   const char *opath = getenv ("PATH");
   if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
   const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
   const char *opath = getenv ("PATH");
   if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
-  char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 30);
-  strcpy (npath, "PATH=");
-  strcat (npath, dir);
+  char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 2);
+  strcpy (npath, dir);
   strcat (npath, ":");
   strcat (npath, opath);
   strcat (npath, ":");
   strcat (npath, opath);
-  if (putenv (npath)) {
-    perror ("putenv");
-    NSAssert1 (0, @"putenv \"%s\" failed", npath);
+  if (setenv ("PATH", npath, 1)) {
+    perror ("setenv");
+    NSAssert1 (0, @"setenv \"PATH=%s\" failed", npath);
   }
 
   }
 
-  /* Don't free (npath) -- MacOS's putenv() does not copy it. */
+  free (npath);
 }
 
 
 }
 
 
@@ -196,14 +207,33 @@ extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
   
   const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
   
   const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
-  char *env = (char *) malloc (strlen (s) + 40);
-  strcpy (env, "XSCREENSAVER_CLASSPATH=");
-  strcat (env, s);
-  if (putenv (env)) {
-    perror ("putenv");
-    NSAssert1 (0, @"putenv \"%s\" failed", env);
+  if (setenv ("XSCREENSAVER_CLASSPATH", s, 1)) {
+    perror ("setenv");
+    NSAssert1 (0, @"setenv \"XSCREENSAVER_CLASSPATH=%s\" failed", s);
+  }
+}
+
+
+- (void) loadCustomFonts
+{
+# ifndef USE_IPHONE
+  NSBundle *nsb = [NSBundle bundleForClass:[self class]];
+  NSMutableArray *fonts = [NSMutableArray arrayWithCapacity:20];
+  for (NSString *ext in @[@"ttf", @"otf"]) {
+    [fonts addObjectsFromArray: [nsb pathsForResourcesOfType:ext
+                                     inDirectory:NULL]];
+  }
+  for (NSString *font in fonts) {
+    CFURLRef url = (CFURLRef) [NSURL fileURLWithPath: font];
+    CFErrorRef err = 0;
+    if (! CTFontManagerRegisterFontsForURL (url, kCTFontManagerScopeProcess,
+                                            &err)) {
+      // Just ignore errors:
+      // "The file has already been registered in the specified scope."
+      // NSLog (@"loading font: %@ %@", url, err);
+    }
   }
   }
-  /* Don't free (env) -- MacOS's putenv() does not copy it. */
+# endif // !USE_IPHONE
 }
 
 
 }
 
 
@@ -257,7 +287,12 @@ add_default_options (const XrmOptionDescRec *opts,
     { 0, 0, 0, 0 }
   };
   static const char *default_defaults [] = {
     { 0, 0, 0, 0 }
   };
   static const char *default_defaults [] = {
+
+# if defined(USE_IPHONE) && !defined(__OPTIMIZE__)
+    ".doFPS:              True",
+# else
     ".doFPS:              False",
     ".doFPS:              False",
+# endif
     ".doubleBuffer:       True",
     ".multiSample:        False",
 # ifndef USE_IPHONE
     ".doubleBuffer:       True",
     ".multiSample:        False",
 # ifndef USE_IPHONE
@@ -346,42 +381,6 @@ add_default_options (const XrmOptionDescRec *opts,
 }
 
 
 }
 
 
-#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
 - (id) initWithFrame:(NSRect)frame
            saverName:(NSString *)saverName
            isPreview:(BOOL)isPreview
@@ -409,7 +408,7 @@ orientname(unsigned long o)
                              encoding:NSISOLatin1StringEncoding];
   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
   [self setResourcesEnv:name];
                              encoding:NSISOLatin1StringEncoding];
   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
   [self setResourcesEnv:name];
-
+  [self loadCustomFonts];
   
   XrmOptionDescRec *opts = 0;
   const char **defs = 0;
   
   XrmOptionDescRec *opts = 0;
   const char **defs = 0;
@@ -424,7 +423,7 @@ orientname(unsigned long o)
 
   next_frame_time = 0;
 
 
   next_frame_time = 0;
 
-# ifndef USE_IPHONE
+# if !defined USE_IPHONE && defined JWXYZ_QUARTZ
   // 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.
   // 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.
@@ -432,34 +431,19 @@ orientname(unsigned long o)
 # endif
 
 # ifdef USE_IPHONE
 # 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]];
   [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
 
 # endif // USE_IPHONE
 
+# ifdef JWXYZ_QUARTZ
+  // Colorspaces and CGContexts only happen with non-GL hacks.
+  colorspace = CGColorSpaceCreateDeviceRGB ();
+# endif
+
   return self;
 }
 
   return self;
 }
 
@@ -489,18 +473,23 @@ orientname(unsigned long o)
   [[NSNotificationCenter defaultCenter] removeObserver:self];
 # endif
 
   [[NSNotificationCenter defaultCenter] removeObserver:self];
 # endif
 
-# ifdef USE_BACKBUFFER
-
 #  ifdef BACKBUFFER_OPENGL
 #  ifdef BACKBUFFER_OPENGL
+# ifndef USE_IPHONE
+  [pixfmt release];
+# endif // !USE_IPHONE
   [ogl_ctx release];
   // Releasing the OpenGL context should also free any OpenGL objects,
   // including the backbuffer texture and frame/render/depthbuffers.
 #  endif // 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 defined JWXYZ_GL && defined USE_IPHONE
+  [ogl_ctx_pixmap release];
+# endif // JWXYZ_GL
+
+# ifdef JWXYZ_QUARTZ
   if (colorspace)
     CGColorSpaceRelease (colorspace);
   if (colorspace)
     CGColorSpaceRelease (colorspace);
-
-# endif // USE_BACKBUFFER
+# endif // JWXYZ_QUARTZ
 
   [prefsReader release];
 
 
   [prefsReader release];
 
@@ -537,6 +526,45 @@ orientname(unsigned long o)
   [prefs setBool:YES forKey:@"wasRunning"];
   [prefs synchronize];
 }
   [prefs setBool:YES forKey:@"wasRunning"];
   [prefs synchronize];
 }
+
+
+- (void) resizeGL
+{
+  if (!ogl_ctx)
+    return;
+
+  CGSize screen_size = self.bounds.size;
+  double s = self.contentScaleFactor;
+  screen_size.width *= s;
+  screen_size.height *= s;
+
+#if defined JWXYZ_GL
+  GLuint *framebuffer = &xwindow->gl_framebuffer;
+  GLuint *renderbuffer = &xwindow->gl_renderbuffer;
+  xwindow->window.current_drawable = xwindow;
+#elif defined JWXYZ_QUARTZ
+  GLuint *framebuffer = &gl_framebuffer;
+  GLuint *renderbuffer = &gl_renderbuffer;
+#endif // JWXYZ_QUARTZ
+
+  if (*framebuffer)  glDeleteFramebuffersOES  (1, framebuffer);
+  if (*renderbuffer) glDeleteRenderbuffersOES (1, renderbuffer);
+
+  create_framebuffer (framebuffer, 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, *renderbuffer);
+
+  [self addExtraRenderbuffers:screen_size];
+
+  check_framebuffer_status();
+}
 #endif // USE_IPHONE
 
 
 #endif // USE_IPHONE
 
 
@@ -555,16 +583,6 @@ 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];
 
@@ -586,10 +604,13 @@ orientname(unsigned long o)
 # ifdef USE_IPHONE
   [UIApplication sharedApplication].idleTimerDisabled =
     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
 # ifdef USE_IPHONE
   [UIApplication sharedApplication].idleTimerDisabled =
     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
-  [[UIApplication sharedApplication]
-    setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
 # endif
 
 # endif
 
+  xwindow = (Window) calloc (1, sizeof(*xwindow));
+  xwindow->type = WINDOW;
+  xwindow->window.view = self;
+  CFRetain (xwindow->window.view);   // needed for garbage collection?
+
 #ifdef BACKBUFFER_OPENGL
   CGSize new_backbuffer_size;
 
 #ifdef BACKBUFFER_OPENGL
   CGSize new_backbuffer_size;
 
@@ -597,16 +618,16 @@ orientname(unsigned long o)
 # ifndef USE_IPHONE
     if (!ogl_ctx) {
 
 # ifndef USE_IPHONE
     if (!ogl_ctx) {
 
-      NSOpenGLPixelFormat *pixfmt = [self getGLPixelFormat];
+      pixfmt = [self getGLPixelFormat];
+      [pixfmt retain];
 
       NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");
 
 
       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.
       // 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.
+      // "Analyze" says that both pixfmt and ogl_ctx are leaked.
       ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
                                          shareContext:nil];
 
       ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
                                          shareContext:nil];
 
@@ -623,6 +644,18 @@ orientname(unsigned long o)
     // from initWithFrame.
     [ogl_ctx setView:self];
 
     // from initWithFrame.
     [ogl_ctx setView:self];
 
+    // Get device pixels instead of points.
+    self.wantsBestResolutionOpenGLSurface = YES;
+
+    // This may not be necessary if there's FBO support.
+#  ifdef JWXYZ_GL
+    xwindow->window.pixfmt = pixfmt;
+    CFRetain (xwindow->window.pixfmt);
+    xwindow->window.virtual_screen = [ogl_ctx currentVirtualScreen];
+    xwindow->window.current_drawable = xwindow;
+    NSAssert (ogl_ctx, @"no CGContext");
+#  endif
+
     // 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);
     // 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);
@@ -645,6 +678,11 @@ orientname(unsigned long o)
 
     new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
 
 
     new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
 
+    // Scale factor for desktop retina displays
+    double s = [self hackedContentScaleFactor];
+    new_backbuffer_size.width *= s;
+    new_backbuffer_size.height *= s;
+
 # else  // USE_IPHONE
     if (!ogl_ctx) {
       CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
 # else  // USE_IPHONE
     if (!ogl_ctx) {
       CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
@@ -655,86 +693,37 @@ orientname(unsigned long o)
       eagl_layer.contentsScale = [UIScreen mainScreen].scale;
 
       ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
       eagl_layer.contentsScale = [UIScreen mainScreen].scale;
 
       ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
-    }
-
-    [EAGLContext setCurrentContext: ogl_ctx];
+# ifdef JWXYZ_GL
+      ogl_ctx_pixmap = [[EAGLContext alloc]
+                        initWithAPI:kEAGLRenderingAPIOpenGLES1
+                        sharegroup:ogl_ctx.sharegroup];
+# endif // JWXYZ_GL
 
 
-    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;
+      eagl_layer.contentsGravity = [self getCAGravity];
     }
 
     }
 
-    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];
+# ifdef JWXYZ_GL
+    xwindow->window.ogl_ctx_pixmap = ogl_ctx_pixmap;
+# endif // JWXYZ_GL
 
 
-    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;
-    }
+    [EAGLContext setCurrentContext: ogl_ctx];
 
 
-    glViewport (0, 0, screen_size.width, screen_size.height);
+    [self resizeGL];
 
 
-    new_backbuffer_size = initial_bounds;
+    double s = [self hackedContentScaleFactor];
+    new_backbuffer_size = self.bounds.size;
+    new_backbuffer_size.width *= s;
+    new_backbuffer_size.height *= s;
 
 # endif // USE_IPHONE
 
 
 # endif // USE_IPHONE
 
+# ifdef JWXYZ_GL
+    xwindow->ogl_ctx = ogl_ctx;
+#  ifndef USE_IPHONE
+    CFRetain (xwindow->ogl_ctx);
+#  endif // USE_IPHONE
+# endif // JWXYZ_GL
+
     check_gl_error ("startAnimation");
 
 //  NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
     check_gl_error ("startAnimation");
 
 //  NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
@@ -744,9 +733,52 @@ orientname(unsigned long o)
   }
 #endif // BACKBUFFER_OPENGL
 
   }
 #endif // BACKBUFFER_OPENGL
 
-#ifdef USE_BACKBUFFER
+  [self setViewport];
   [self createBackbuffer:new_backbuffer_size];
   [self createBackbuffer:new_backbuffer_size];
-#endif
+
+# ifdef USE_TOUCHBAR
+  static BOOL created_touchbar = NO;
+
+  if (!touchbar_view &&
+      //#### !self.isPreview &&
+      self.window.screen == [[NSScreen screens] objectAtIndex: 0] &&
+      !created_touchbar) {
+
+    // Figure out which NSScreen has the touchbar on it;
+    // find its bounds; create a saver there.
+
+    created_touchbar = YES;
+    NSScreen *tbs = [[NSScreen screens] lastObject]; // #### write me
+    NSRect rect = [tbs visibleFrame];
+
+    // #### debugging
+    rect.origin.x += 40;
+    rect.origin.x += 40;
+    rect.size.width /= 4;
+    rect.size.height /= 4;
+    NSLog(@"## TB %.0f, %.0f  %.0f x %.0f",
+          rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
+
+    touchbar_view = [[[self class] alloc]
+                      initWithFrame:rect
+                      saverName:[NSString stringWithCString:xsft->progclass
+                                          encoding:NSISOLatin1StringEncoding]
+                      isPreview:self.isPreview];
+    [touchbar_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
+
+    touchbar_window = [[NSWindow alloc]
+                        initWithContentRect:rect
+                        styleMask: (NSTitledWindowMask|NSResizableWindowMask)
+                        backing:NSBackingStoreBuffered
+                        defer:YES
+                        screen:tbs];
+    [touchbar_window setTitle: @"XScreenSaver Touchbar"];
+    [[touchbar_window contentView] addSubview: touchbar_view];
+    [touchbar_window makeKeyAndOrderFront:touchbar_window];
+  }
+
+  if (touchbar_view) [touchbar_view startAnimation];
+# endif // USE_TOUCHBAR
 }
 
 - (void)stopAnimation
 }
 
 - (void)stopAnimation
@@ -758,19 +790,23 @@ orientname(unsigned long o)
     [self lockFocus];       // in case something tries to draw from here
     [self prepareContext];
 
     [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
+    /* All of the xlockmore hacks need to have their release functions
+       called, or launching the same saver twice does not work.  Also
+       webcollage-cocoa needs it in order to kill the inferior webcollage
        processes (since the screen saver framework never generates a
        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.
+       SIGPIPE for them).
      */
      */
-    xsft->free_cb (xdpy, xwindow, xdata);
+     if (xdata)
+       xsft->free_cb (xdpy, xwindow, xdata);
     [self unlockFocus];
 
     [self unlockFocus];
 
-    // xdpy must be freed before dealloc is called, because xdpy owns a
-    // circular reference to the parent XScreenSaverView.
-    jwxyz_free_display (xdpy);
+    jwxyz_quartz_free_display (xdpy);
     xdpy = NULL;
     xdpy = NULL;
+# if defined JWXYZ_GL && !defined USE_IPHONE
+    CFRelease (xwindow->ogl_ctx);
+# endif
+    CFRelease (xwindow->window.view);
+    free (xwindow);
     xwindow = NULL;
 
 //  setup_p = NO; // #### wait, do we need this?
     xwindow = NULL;
 
 //  setup_p = NO; // #### wait, do we need this?
@@ -794,8 +830,6 @@ orientname(unsigned long o)
   //
 # ifdef USE_IPHONE
   [UIApplication sharedApplication].idleTimerDisabled = NO;
   //
 # 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
 # endif
 
   // Without this, the GL frame stays on screen when switching tabs
@@ -808,6 +842,7 @@ orientname(unsigned long o)
 
   clear_gl_error();    // This hack is defunct, don't let this linger.
 
 
   clear_gl_error();    // This hack is defunct, don't let this linger.
 
+# ifdef JWXYZ_QUARTZ
   CGContextRelease (backbuffer);
   backbuffer = nil;
 
   CGContextRelease (backbuffer);
   backbuffer = nil;
 
@@ -815,19 +850,43 @@ orientname(unsigned long o)
     munmap (backbuffer_data, backbuffer_len);
   backbuffer_data = NULL;
   backbuffer_len = 0;
     munmap (backbuffer_data, backbuffer_len);
   backbuffer_data = NULL;
   backbuffer_len = 0;
+# endif
+
+# ifdef USE_TOUCHBAR
+  if (touchbar_view) {
+    [touchbar_view stopAnimation];
+    [touchbar_window close];
+
+    [touchbar_view release];
+    [touchbar_window release];
+
+    touchbar_view = nil;
+    touchbar_window = nil;
+  }
+# endif
+}
+
+
+- (NSOpenGLContext *) oglContext
+{
+  return ogl_ctx;
 }
 
 
 // #### maybe this could/should just be on 'lockFocus' instead?
 - (void) prepareContext
 {
 }
 
 
 // #### maybe this could/should just be on 'lockFocus' instead?
 - (void) prepareContext
 {
-  if (ogl_ctx) {
+  if (xwindow) {
 #ifdef USE_IPHONE
     [EAGLContext setCurrentContext:ogl_ctx];
 #else  // !USE_IPHONE
     [ogl_ctx makeCurrentContext];
 //    check_gl_error ("makeCurrentContext");
 #endif // !USE_IPHONE
 #ifdef USE_IPHONE
     [EAGLContext setCurrentContext:ogl_ctx];
 #else  // !USE_IPHONE
     [ogl_ctx makeCurrentContext];
 //    check_gl_error ("makeCurrentContext");
 #endif // !USE_IPHONE
+
+#ifdef JWXYZ_GL
+    xwindow->window.current_drawable = xwindow;
+#endif
   }
 }
 
   }
 }
 
@@ -840,18 +899,12 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
 }
 
 
 }
 
 
-#ifdef USE_IPHONE
+/* Some of the older X11 savers look bad if a "pixel" is not a thing you can
+   see.  They expect big, chunky, luxurious 1990s pixels, and if they use
+   "device" pixels on a Retina screen, everything just disappears.
 
 
-/* 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.)
+   Retina iPads have 768x1024 point screens which are 1536x2048 pixels,
+   2017 iMac screens are 5120x2880 in device pixels.
 
    This method is overridden in XScreenSaverGLView, since this kludge
    isn't necessary for GL programs, being resolution independent by
 
    This method is overridden in XScreenSaverGLView, since this kludge
    isn't necessary for GL programs, being resolution independent by
@@ -859,132 +912,102 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
  */
 - (CGFloat) hackedContentScaleFactor
 {
  */
 - (CGFloat) hackedContentScaleFactor
 {
-  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.
-  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);
+# ifdef USE_IPHONE
+  CGFloat s = self.contentScaleFactor;
+# else
+  CGFloat s = self.window.backingScaleFactor;
+# endif
 
 
-  // 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);
+  if (_lowrez_p) {
+    NSSize b = [self bounds].size;
+    CGFloat wh = b.width > b.height ? b.width : b.height;
+    const int max = 800; // maybe 1024?
+    wh *= s;
+    if (wh > max)
+      s *= max / wh;
+  }
 
 
-  // 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);
+  return s;
 }
 
 
 }
 
 
-static GLfloat _global_rot_current_angle_kludge;
-
-double current_device_rotation (void)
-{
-  return -_global_rot_current_angle_kludge;
-}
-
+#ifdef USE_IPHONE
 
 
-- (void) hackRotation
+double
+current_device_rotation (void)
 {
 {
-  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);
+  UIDeviceOrientation o = [[UIDevice currentDevice] orientation];
 
 
-    // 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;
+  /* 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 (o == UIDeviceOrientationUnknown ||
+      o == UIDeviceOrientationFaceUp  ||
+      o == UIDeviceOrientationFaceDown) {
+    /* Mind the differences between UIInterfaceOrientation and
+       UIDeviceOrientation:
+       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."
+        */
+    /* statusBarOrientation deprecated in iOS 9 */
+    o = (UIDeviceOrientation)  // from UIInterfaceOrientation
+      [UIApplication sharedApplication].statusBarOrientation;
   }
 
   }
 
-  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];
+  switch (o) {
+  case UIDeviceOrientationLandscapeLeft:      return -90; break;
+  case UIDeviceOrientationLandscapeRight:     return  90; break;
+  case UIDeviceOrientationPortraitUpsideDown: return 180; break;
+  default:                                    return 0;   break;
+  }
 }
 
 
 }
 
 
-- (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);
 - (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];
+  UIAlertController *c = [UIAlertController
+                           alertControllerWithTitle:
+                             [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]
+                           preferredStyle:UIAlertControllerStyleAlert];
+
+  [c addAction: [UIAlertAction actionWithTitle: @"Exit"
+                               style: UIAlertActionStyleDefault
+                               handler: ^(UIAlertAction *a) {
+    exit (-1);
+  }]];
+  [c addAction: [UIAlertAction actionWithTitle: @"Keep going"
+                               style: UIAlertActionStyleDefault
+                               handler: ^(UIAlertAction *a) {
+    [self stopAndClose:NO];
+  }]];
+
+  UIViewController *vc =
+    [UIApplication sharedApplication].keyWindow.rootViewController;
+  while (vc.presentedViewController)
+    vc = vc.presentedViewController;
+  [vc presentViewController:c animated:YES completion:nil];
   [self stopAnimation];
 }
 
 #endif // USE_IPHONE
 
 
   [self stopAnimation];
 }
 
 #endif // USE_IPHONE
 
 
-#ifdef USE_BACKBUFFER
+#ifdef JWXYZ_QUARTZ
 
 # ifndef USE_IPHONE
 
 
 # ifndef USE_IPHONE
 
@@ -1004,31 +1027,6 @@ gl_check_ver (const struct gl_version *caps,
            (caps->major == gl_major && caps->minor >= gl_minor);
 }
 
            (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. */
 # endif
 
 /* Called during startAnimation before the first call to createBackbuffer. */
@@ -1065,13 +1063,12 @@ gluCheckExtension (const GLubyte *ext_name, const GLubyte *ext_string)
                        ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D);
 # else
   // OES_texture_npot also provides this, but iOS never provides it.
                        ? 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_limited_npot_p = jwzgles_gluCheckExtension
+    ((const GLubyte *) "GL_APPLE_texture_2D_limited_npot", extensions);
   gl_texture_target = GL_TEXTURE_2D;
 # endif
 
   gl_texture_target = GL_TEXTURE_2D;
 # endif
 
-  glBindTexture (gl_texture_target, &backbuffer_texture);
+  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_MIN_FILTER, GL_NEAREST);
   // GL_LINEAR might make sense on Retina iPads.
   glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
@@ -1097,8 +1094,11 @@ gluCheckExtension (const GLubyte *ext_name, const GLubyte *ext_string)
   // you're gonna get for getting a texture onto the screen.
 # ifdef USE_IPHONE
   gl_pixel_format =
   // 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;
+    jwzgles_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
   gl_pixel_type = GL_UNSIGNED_BYTE;
   // See also OES_read_format.
 # else
@@ -1121,102 +1121,88 @@ gluCheckExtension (const GLubyte *ext_name, const GLubyte *ext_string)
   glEnableClientState (GL_VERTEX_ARRAY);
   glEnableClientState (GL_TEXTURE_COORD_ARRAY);
 
   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");
 }
 
 
   check_gl_error ("enableBackbuffer");
 }
 
 
-static GLsizei
-to_pow2 (size_t x)
+#ifdef USE_IPHONE
+- (BOOL) suppressRotationAnimation
 {
 {
-  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 [self ignoreRotation];        // Don't animate if we aren't rotating
+}
 
 
-  return 1 << log2;
+- (BOOL) rotateTouches
+{
+  return FALSE;                        // Adjust event coordinates only if rotating
 }
 }
+#endif
 
 
 
 
-/* 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
+- (void) setViewport
 {
 {
-  // Colorspaces and CGContexts only happen with non-GL hacks.
-  if (colorspace)
-    CGColorSpaceRelease (colorspace);
-
 # ifdef BACKBUFFER_OPENGL
   NSAssert ([NSOpenGLContext currentContext] ==
             ogl_ctx, @"invalid GL context");
 
 # 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);
+  NSSize new_size = self.bounds.size;
+
+#  ifdef USE_IPHONE
+  GLfloat s = self.contentScaleFactor;
+#  else // !USE_IPHONE
+  const GLfloat s = self.window.backingScaleFactor;
+#  endif
+  GLfloat hs = self.hackedContentScaleFactor;
+
+  // On OS X this almost isn't necessary, except for the ugly aliasing
+  // artifacts.
+  glViewport (0, 0, new_size.width * s, new_size.height * s);
 
   glMatrixMode (GL_PROJECTION);
   glLoadIdentity();
 
   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 USE_IPHONE
+  glOrthof
+#  else
+  glOrtho
+#  endif
+    (-new_size.width * hs, new_size.width * hs,
+     -new_size.height * hs, new_size.height * hs,
+     -1, 1);
 
 
-# ifdef BACKBUFFER_OPENGL
-    // Was apparently faster until 10.9.
-    colorspace = CGColorSpaceCreateDeviceRGB ();
+#  ifdef USE_IPHONE
+  if ([self ignoreRotation]) {
+    int o = (int) -current_device_rotation();
+    glRotatef (o, 0, 0, 1);
+  }
+#  endif // USE_IPHONE
 # endif // BACKBUFFER_OPENGL
 # endif // BACKBUFFER_OPENGL
+}
 
 
-    [self unlockFocus];
-  } else {
-    colorspace = CGColorSpaceCreateDeviceRGB();
+
+/* 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
+{
+  CGSize osize = CGSizeZero;
+  if (backbuffer) {
+    osize.width = CGBitmapContextGetWidth(backbuffer);
+    osize.height = CGBitmapContextGetHeight(backbuffer);
   }
 
   if (backbuffer &&
   }
 
   if (backbuffer &&
-      (int)backbuffer_size.width  == (int)new_size.width &&
-      (int)backbuffer_size.height == (int)new_size.height)
+      (int)osize.width  == (int)new_size.width &&
+      (int)osize.height == (int)new_size.height)
     return;
 
   CGContextRef ob = backbuffer;
   void *odata = backbuffer_data;
     return;
 
   CGContextRef ob = backbuffer;
   void *odata = backbuffer_data;
-  size_t olen = backbuffer_len;
+  GLsizei olen = backbuffer_len;
 
 
-  CGSize osize = backbuffer_size;      // pixels, not points.
-  backbuffer_size = new_size;          // pixels, not points.
-
-# if TARGET_IPHONE_SIMULATOR
+# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
   NSLog(@"backbuffer %.0fx%.0f",
   NSLog(@"backbuffer %.0fx%.0f",
-        backbuffer_size.width, backbuffer_size.height);
+        new_size.width, new_size.height);
 # endif
 
   /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
 # endif
 
   /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
@@ -1245,8 +1231,8 @@ to_pow2 (size_t x)
    */
 
   backbuffer_data = NULL;
    */
 
   backbuffer_data = NULL;
-  gl_texture_w = (int)backbuffer_size.width;
-  gl_texture_h = (int)backbuffer_size.height;
+  gl_texture_w = (int)new_size.width;
+  gl_texture_h = (int)new_size.height;
 
   NSAssert (gl_texture_target == GL_TEXTURE_2D
 # ifndef USE_IPHONE
 
   NSAssert (gl_texture_target == GL_TEXTURE_2D
 # ifndef USE_IPHONE
@@ -1260,11 +1246,11 @@ to_pow2 (size_t x)
   if (!gl_limited_npot_p)
 # endif
   {
   if (!gl_limited_npot_p)
 # endif
   {
-    gl_texture_w = to_pow2 (gl_texture_w);
-    gl_texture_h = to_pow2 (gl_texture_h);
+    gl_texture_w = (GLsizei) to_pow2 (gl_texture_w);
+    gl_texture_h = (GLsizei) to_pow2 (gl_texture_h);
   }
 
   }
 
-  size_t bytes_per_row = gl_texture_w * 4;
+  GLsizei 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
 
 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
   // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
@@ -1319,8 +1305,8 @@ to_pow2 (size_t x)
     (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
 
   backbuffer = CGBitmapContextCreate (backbuffer_data,
     (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
 
   backbuffer = CGBitmapContextCreate (backbuffer_data,
-                                      (int)backbuffer_size.width,
-                                      (int)backbuffer_size.height,
+                                      (int)new_size.width,
+                                      (int)new_size.height,
                                       8,
                                       bytes_per_row,
                                       colorspace,
                                       8,
                                       bytes_per_row,
                                       colorspace,
@@ -1330,7 +1316,7 @@ to_pow2 (size_t x)
   // Clear it.
   CGRect r;
   r.origin.x = r.origin.y = 0;
   // Clear it.
   CGRect r;
   r.origin.x = r.origin.y = 0;
-  r.size = backbuffer_size;
+  r.size = new_size;
   CGContextSetGrayFillColor (backbuffer, 0, 1);
   CGContextFillRect (backbuffer, r);
 
   CGContextSetGrayFillColor (backbuffer, 0, 1);
   CGContextFillRect (backbuffer, r);
 
@@ -1344,7 +1330,7 @@ to_pow2 (size_t x)
 
     CGRect rect;   // pixels, not points
     rect.origin.x = 0;
 
     CGRect rect;   // pixels, not points
     rect.origin.x = 0;
-    rect.origin.y = (backbuffer_size.height - osize.height);
+    rect.origin.y = (new_size.height - osize.height);
     rect.size = osize;
 
     CGImageRef img = CGBitmapContextCreateImage (ob);
     rect.size = osize;
 
     CGImageRef img = CGBitmapContextCreateImage (ob);
@@ -1381,51 +1367,28 @@ to_pow2 (size_t x)
                 gl_texture_h, 0, gl_pixel_format, gl_pixel_type,
                 backbuffer_data);
 
                 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 w = xwindow->frame.width, h = xwindow->frame.height;
+
+  GLfloat vertices[4][2] = {{-w,  h}, {w,  h}, {w, -h}, {-w, -h}};
 
   GLfloat tex_coords[4][2];
 
 #  ifndef USE_IPHONE
 
   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
+  if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
 #  endif // USE_IPHONE
   {
 #  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;
+    w /= gl_texture_w;
+    h /= gl_texture_h;
   }
 
   }
 
-#  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
+  tex_coords[0][0] = 0;
+  tex_coords[0][1] = 0;
+  tex_coords[1][0] = w;
+  tex_coords[1][1] = 0;
+  tex_coords[2][0] = w;
+  tex_coords[2][1] = h;
+  tex_coords[3][0] = 0;
+  tex_coords[3][1] = h;
 
   glVertexPointer (2, GL_FLOAT, 0, vertices);
   glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
 
   glVertexPointer (2, GL_FLOAT, 0, vertices);
   glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
@@ -1434,29 +1397,81 @@ to_pow2 (size_t x)
 #  if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
   check_gl_error ("drawBackbuffer");
 #  endif
 #  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
 }
 
 # endif // BACKBUFFER_OPENGL
 }
 
+#endif // JWXYZ_QUARTZ
 
 
-- (void)flushBackbuffer
+#ifdef JWXYZ_GL
+
+- (void)enableBackbuffer:(CGSize)new_backbuffer_size;
+{
+  jwxyz_set_matrices (new_backbuffer_size.width, new_backbuffer_size.height);
+  check_gl_error ("enableBackbuffer");
+}
+
+- (void)createBackbuffer:(CGSize)new_size
 {
 {
+  NSAssert ([NSOpenGLContext currentContext] ==
+            ogl_ctx, @"invalid GL context");
+  NSAssert (xwindow->window.current_drawable == xwindow,
+            @"current_drawable not set properly");
+
 # ifndef USE_IPHONE
 # 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();
+  /* On iOS, Retina means glViewport gets called with the screen size instead
+     of the backbuffer/xwindow size. This happens in startAnimation.
 
 
-  if (double_buffered_p)
+     The GL screenhacks call glViewport themselves.
+   */
+  glViewport (0, 0, new_size.width, new_size.height);
+# endif
+
+  // TODO: Preserve contents on resize.
+  glClear (GL_COLOR_BUFFER_BIT);
+  check_gl_error ("createBackbuffer");
+}
+
+#endif // JWXYZ_GL
+
+
+- (void)flushBackbuffer
+{
+# ifdef JWXYZ_GL
+  // Make sure the right context is active: there's two under JWXYZ_GL.
+  jwxyz_bind_drawable (xwindow, xwindow);
+# endif // JWXYZ_GL
+
+# ifndef USE_IPHONE
+
+#  ifdef JWXYZ_QUARTZ
+  // 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.
+  // This is only a concern under JWXYZ_QUARTZ because of
+  // APPLE_client_storage; JWXYZ_GL doesn't use that.
+  glFinish();
+#  endif // JWXYZ_QUARTZ
+
+  // If JWXYZ_GL was single-buffered, there would need to be a glFinish (or
+  // maybe just glFlush?) here, because single-buffered contexts don't always
+  // update what's on the screen after drawing finishes. (i.e., in safe mode)
+
+#  ifdef JWXYZ_QUARTZ
+  // JWXYZ_GL is always double-buffered.
+  if (double_buffered_p)
+#  endif // JWXYZ_QUARTZ
     [ogl_ctx flushBuffer]; // despite name, this actually swaps
     [ogl_ctx flushBuffer]; // despite name, this actually swaps
-# else
+# else // USE_IPHONE
+
+  // jwxyz_bind_drawable() only binds the framebuffer, not the renderbuffer.
+#  ifdef JWXYZ_GL
+  GLint gl_renderbuffer = xwindow->gl_renderbuffer;
+#  endif
+
   glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
   [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
   glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
   [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
-# endif
+# endif // USE_IPHONE
 
 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
   // glGetError waits for the OpenGL command pipe to flush, so skip it in
 
 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
   // glGetError waits for the OpenGL command pipe to flush, so skip it in
@@ -1469,45 +1484,67 @@ to_pow2 (size_t x)
 }
 
 
 }
 
 
-#endif // USE_BACKBUFFER
-
-
 /* Inform X11 that the size of our window has changed.
  */
 - (void) resize_x11
 {
 /* Inform X11 that the size of our window has changed.
  */
 - (void) resize_x11
 {
-  if (!xwindow) return;  // early
+  if (!xdpy) return;     // early
+
+  NSSize new_size;     // pixels, not points
 
 
-  CGSize new_size;     // pixels, not points
+  new_size = self.bounds.size;
 
 
-# ifdef USE_BACKBUFFER
+#  ifdef USE_IPHONE
+
+  // If this hack ignores rotation, then that means that it pretends to
+  // always be in portrait mode.  If the View has been resized to a 
+  // landscape shape, swap width and height to keep the backbuffer
+  // in portrait.
+  //
+  double rot = current_device_rotation();
+  if ([self ignoreRotation] && (rot == 90 || rot == -90)) {
+    CGFloat swap    = new_size.width;
+    new_size.width  = new_size.height;
+    new_size.height = swap;
+  }
+#  endif // USE_IPHONE
+
+  double s = self.hackedContentScaleFactor;
+  new_size.width *= s;
+  new_size.height *= s;
 
   [self prepareContext];
 
   [self prepareContext];
+  [self setViewport];
+
+  // On first resize, xwindow->frame is 0x0.
+  if (xwindow->frame.width == new_size.width &&
+      xwindow->frame.height == new_size.height)
+    return;
 
 #  if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
   [ogl_ctx update];
 #  endif // BACKBUFFER_OPENGL && !USE_IPHONE
 
 
 #  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
+  NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
+  xwindow->frame.x    = 0;
+  xwindow->frame.y    = 0;
+  xwindow->frame.width  = new_size.width;
+  xwindow->frame.height = new_size.height;
+
+  [self createBackbuffer:CGSizeMake(xwindow->frame.width,
+                                    xwindow->frame.height)];
+
+# if defined JWXYZ_QUARTZ
+  xwindow->cgc = backbuffer;
+  NSAssert (xwindow->cgc, @"no CGContext");
+# elif defined JWXYZ_GL && !defined USE_IPHONE
+  [ogl_ctx update];
+  [ogl_ctx setView:xwindow->window.view]; // (Is this necessary?)
+# endif // JWXYZ_GL && USE_IPHONE
+
+  jwxyz_window_resized (xdpy);
+
+# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
   NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
 # endif
 
   NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
 # endif
 
@@ -1516,33 +1553,82 @@ to_pow2 (size_t x)
 }
 
 
 }
 
 
+#ifdef USE_IPHONE
+
+/* Called by SaverRunner when the device has changed orientation.
+   That means we need to generate a resize event, even if the size
+   has not changed (e.g., from LandscapeLeft to LandscapeRight).
+ */
+- (void) orientationChanged
+{
+  [self setViewport];
+  resized_p = YES;
+  next_frame_time = 0;  // Get a new frame on screen quickly
+}
+
+/* A hook run after the 'reshape_' method has been called.  Used by
+  XScreenSaverGLView to adjust the in-scene GL viewport.
+ */
+- (void) postReshape
+{
+}
+#endif // USE_IPHONE
+
+
+// Only render_x11 should call this.  XScreenSaverGLView specializes it.
+- (void) reshape_x11
+{
+  xsft->reshape_cb (xdpy, xwindow, xdata,
+                    xwindow->frame.width, xwindow->frame.height);
+}
+
 - (void) render_x11
 {
 # ifdef USE_IPHONE
   @try {
 - (void) render_x11
 {
 # ifdef USE_IPHONE
   @try {
-
-  if (orientation == UIDeviceOrientationUnknown)
-    [self didRotate:nil];
-  [self hackRotation];
 # endif
 
 # endif
 
+  // jwxyz_make_display needs this.
+  [self prepareContext]; // resize_x11 also calls this.
+
   if (!initted_p) {
 
   if (!initted_p) {
 
+    resized_p = NO;
+
     if (! xdpy) {
     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 JWXYZ_QUARTZ
+      xwindow->cgc = backbuffer;
+# endif // JWXYZ_QUARTZ
+      xdpy = jwxyz_quartz_make_display (xwindow);
 
 
-# ifdef USE_IPHONE
+# if defined USE_IPHONE
       /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
       /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
-      ignore_rotation_p =
+      _ignoreRotation =
+#  ifdef JWXYZ_GL
+        TRUE; // Rotation doesn't work yet. TODO: Make rotation work.
+#  else  // !JWXYZ_GL
         get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
         get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
+#  endif // !JWXYZ_GL
 # endif // USE_IPHONE
 
 # endif // USE_IPHONE
 
+      _lowrez_p = get_boolean_resource (xdpy, "lowrez", "Lowrez");
+      if (_lowrez_p) {
+        resized_p = YES;
+
+# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
+        NSSize  b = [self bounds].size;
+        CGFloat s = self.hackedContentScaleFactor;
+#  ifdef USE_IPHONE
+        CGFloat o = self.contentScaleFactor;
+#  else
+        CGFloat o = self.window.backingScaleFactor;
+#  endif
+        NSLog(@"lowrez: scaling %.0fx%.0f -> %.0fx%.0f (%.02f)",
+              b.width * o, b.height * o,
+              b.width * s, b.height * s, s);
+# endif
+      }
+
       [self resize_x11];
     }
 
       [self resize_x11];
     }
 
@@ -1552,7 +1638,6 @@ to_pow2 (size_t x)
         xsft->setup_cb (xsft, xsft->setup_arg);
     }
     initted_p = YES;
         xsft->setup_cb (xsft, xsft->setup_arg);
     }
     initted_p = YES;
-    resized_p = NO;
     NSAssert(!xdata, @"xdata already initialized");
 
 
     NSAssert(!xdata, @"xdata already initialized");
 
 
@@ -1592,12 +1677,18 @@ to_pow2 (size_t x)
 
     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
       fpst = fps_init (xdpy, xwindow);
 
     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
       fpst = fps_init (xdpy, xwindow);
-      if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
+      fps_cb = xsft->fps_cb;
+      if (! fps_cb) fps_cb = screenhack_do_fps;
     } else {
       fpst = NULL;
     } else {
       fpst = NULL;
-      xsft->fps_cb = 0;
+      fps_cb = 0;
     }
 
     }
 
+# ifdef USE_IPHONE
+    if (current_device_rotation() != 0)   // launched while rotated
+      resized_p = YES;
+# endif
+
     [self checkForUpdates];
   }
 
     [self checkForUpdates];
   }
 
@@ -1623,6 +1714,14 @@ to_pow2 (size_t x)
     }
 
 
     }
 
 
+  /* Run any XtAppAddInput and XtAppAddTimeOut callbacks now.
+     Do this before delaying for next_frame_time to avoid throttling
+     timers to the hack's frame rate.
+   */
+  XtAppProcessEvent (XtDisplayToApplicationContext (xdpy),
+                     XtIMTimer | XtIMAlternateInput);
+
+
   /* 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.
   /* 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.
@@ -1662,7 +1761,6 @@ to_pow2 (size_t x)
   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
   if (now < next_frame_time) return;
 
   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) {
   // [self flushBackbuffer];
 
   if (resized_p) {
@@ -1674,39 +1772,28 @@ to_pow2 (size_t x)
       [ogl_ctx setView:self];
 # endif // !USE_IPHONE
 
       [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);
+    [self reshape_x11];
     resized_p = NO;
   }
 
     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);
 
   // 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 (xdpy, xwindow, fpst, xdata);
+  if (fpst && fps_cb)
+    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);
 
 
   gettimeofday (&tv, 0);
   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
   next_frame_time = now + (delay / 1000000.0);
 
+# ifdef JWXYZ_QUARTZ
   [self drawBackbuffer];
   [self drawBackbuffer];
+# endif
+  // This can also happen near the beginning of render_x11.
+  [self flushBackbuffer];
 
 # ifdef USE_IPHONE     // Allow savers on the iPhone to run full-tilt.
   if (delay < [self animationTimeInterval])
 
 # ifdef USE_IPHONE     // Allow savers on the iPhone to run full-tilt.
   if (delay < [self animationTimeInterval])
@@ -1752,36 +1839,31 @@ to_pow2 (size_t x)
 }
 
 
 }
 
 
-#ifndef USE_BACKBUFFER
-
-- (void) animateOneFrame
-{
-  [self render_x11];
-  jwxyz_flush_context(xdpy);
-}
-
-#else  // USE_BACKBUFFER
-
 - (void) animateOneFrame
 {
   // Render X11 into the backing store bitmap...
 
 - (void) animateOneFrame
 {
   // Render X11 into the backing store bitmap...
 
+# ifdef JWXYZ_QUARTZ
   NSAssert (backbuffer, @"no back buffer");
 
   NSAssert (backbuffer, @"no back buffer");
 
-# ifdef USE_IPHONE
+#  ifdef USE_IPHONE
   UIGraphicsPushContext (backbuffer);
   UIGraphicsPushContext (backbuffer);
-# endif
+#  endif
+# endif // JWXYZ_QUARTZ
 
   [self render_x11];
 
 
   [self render_x11];
 
-# if defined USE_IPHONE && defined USE_BACKBUFFER
+# if defined USE_IPHONE && defined JWXYZ_QUARTZ
   UIGraphicsPopContext();
 # endif
   UIGraphicsPopContext();
 # endif
-}
 
 
-#endif // USE_BACKBUFFER
+# ifdef USE_TOUCHBAR
+  if (touchbar_view) [touchbar_view animateOneFrame];
+# endif
+}
 
 
 
 
+# ifndef USE_IPHONE  // Doesn't exist on iOS
 
 - (void) setFrame:(NSRect) newRect
 {
 
 - (void) setFrame:(NSRect) newRect
 {
@@ -1791,15 +1873,24 @@ to_pow2 (size_t x)
     [self resize_x11];
 }
 
     [self resize_x11];
 }
 
-
-# ifndef USE_IPHONE  // Doesn't exist on iOS
 - (void) setFrameSize:(NSSize) newSize
 {
   [super setFrameSize:newSize];
   if (xwindow)
     [self resize_x11];
 }
 - (void) setFrameSize:(NSSize) newSize
 {
   [super setFrameSize:newSize];
   if (xwindow)
     [self resize_x11];
 }
-# endif // !USE_IPHONE
+
+# else // USE_IPHONE
+
+- (void) layoutSubviews
+{
+  [super layoutSubviews];
+  [self resizeGL];
+  if (xwindow)
+    [self resize_x11];
+}
+
+# endif
 
 
 +(BOOL) performGammaFade
 
 
 +(BOOL) performGammaFade
@@ -1842,7 +1933,10 @@ to_pow2 (size_t x)
                  ret, (zs.msg ? zs.msg : "<null>"));
   }
 
                  ret, (zs.msg ? zs.msg : "<null>"));
   }
 
-  return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+  NSString *s = [[NSString alloc]
+                  initWithData:data encoding:NSUTF8StringEncoding];
+  [s autorelease];
+  return s;
 }
 
 
 }
 
 
@@ -1964,11 +2058,7 @@ to_pow2 (size_t x)
   
   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
                                             toView:self];
   
   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
                                             toView:self];
-# ifdef USE_IPHONE
   double s = [self hackedContentScaleFactor];
   double s = [self hackedContentScaleFactor];
-# else
-  int s = 1;
-# endif
   int x = s * p.x;
   int y = s * ([self bounds].size.height - p.y);
 
   int x = s * p.x;
   int y = s * ([self bounds].size.height - p.y);
 
@@ -1986,7 +2076,7 @@ to_pow2 (size_t x)
                              [e deltaX] < 0 ? Button7 :
                              0);
       else
                              [e deltaX] < 0 ? Button7 :
                              0);
       else
-        xe.xbutton.button = [e buttonNumber] + 1;
+        xe.xbutton.button = (unsigned int) [e buttonNumber] + 1;
       break;
     case MotionNotify:
       xe.xmotion.x = x;
       break;
     case MotionNotify:
       xe.xmotion.x = x;
@@ -2040,9 +2130,9 @@ to_pow2 (size_t x)
             case NSF12FunctionKey:       k = XK_F12;       break;
             default:
               {
             case NSF12FunctionKey:       k = XK_F12;       break;
             default:
               {
-                const char *s =
+                const char *ss =
                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
-                k = (s && *s ? *s : 0);
+                k = (ss && *ss ? *ss : 0);
               }
               break;
             }
               }
               break;
             }
@@ -2138,17 +2228,68 @@ to_pow2 (size_t x)
   int i = 0;
   attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
 
   int i = 0;
   attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
 
+/* OpenGL's core profile removes a lot of the same stuff that was removed in
+   OpenGL ES (e.g. glBegin, glDrawPixels), so it might be a possibility.
+
+  opengl_core_p = True;
+  if (opengl_core_p) {
+    attrs[i++] = NSOpenGLPFAOpenGLProfile;
+    attrs[i++] = NSOpenGLProfileVersion3_2Core;
+  }
+ */
+
+/* Eventually: multisampled pixmaps. May not be supported everywhere.
+   if (multi_sample_p) {
+     attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 1;
+     attrs[i++] = NSOpenGLPFASamples;       attrs[i++] = 6;
+   }
+ */
+
+# ifdef JWXYZ_QUARTZ
+  // Under Quartz, we're just blitting a texture.
   if (double_buffered_p)
     attrs[i++] = NSOpenGLPFADoubleBuffer;
   if (double_buffered_p)
     attrs[i++] = NSOpenGLPFADoubleBuffer;
+# endif
+
+# ifdef JWXYZ_GL
+  /* Under OpenGL, all sorts of drawing commands are being issued, and it might
+     be a performance problem if this activity occurs on the front buffer.
+     Also, some screenhacks expect OS X/iOS to always double-buffer.
+     NSOpenGLPFABackingStore prevents flickering with screenhacks that
+     don't redraw the entire screen every frame.
+   */
+  attrs[i++] = NSOpenGLPFADoubleBuffer;
+  attrs[i++] = NSOpenGLPFABackingStore;
+# endif
+
+  attrs[i++] = NSOpenGLPFAWindow;
+# ifdef JWXYZ_GL
+  attrs[i++] = NSOpenGLPFAPixelBuffer;
+  /* ...But not NSOpenGLPFAFullScreen, because that would be for
+     [NSOpenGLContext setFullScreen].
+   */
+# endif
+
+  /* NSOpenGLPFAFullScreen would go here if initWithFrame's isPreview == NO.
+   */
 
   attrs[i] = 0;
 
 
   attrs[i] = 0;
 
-  return [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
+  NSOpenGLPixelFormat *p = [[NSOpenGLPixelFormat alloc]
+                             initWithAttributes:attrs];
+  [p autorelease];
+  return p;
 }
 
 #else  // USE_IPHONE
 
 
 }
 
 #else  // USE_IPHONE
 
 
+- (void) stopAndClose
+{
+  [self stopAndClose:NO];
+}
+
+
 - (void) stopAndClose:(Bool)relaunch_p
 {
   if ([self isAnimating])
 - (void) stopAndClose:(Bool)relaunch_p
 {
   if ([self isAnimating])
@@ -2169,7 +2310,7 @@ to_pow2 (size_t x)
   if (relaunch_p) {   // Fake a shake on the SaverListController.
     [_delegate didShake:self];
   } else {     // Not launching another, animate our return to the list.
   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
+# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
     NSLog (@"fading back to saver list");
 # endif
     [_delegate wantsFadeOut:self];
     NSLog (@"fading back to saver list");
 # endif
     [_delegate wantsFadeOut:self];
@@ -2177,157 +2318,13 @@ to_pow2 (size_t x)
 }
 
 
 }
 
 
-/* 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.
 /* We distinguish between taps and drags.
 
    - Drags/pans (down, motion, up) are sent to the saver to handle.
-   - Single-taps exit the saver.
+   - Single-taps are sent to the saver to handle.
    - Double-taps are sent to the saver as a "Space" keypress.
    - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
    - 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.
+   - All taps expose the momentary "Close" button.
  */
 
 - (void)initGestures
  */
 
 - (void)initGestures
@@ -2340,7 +2337,7 @@ to_pow2 (size_t x)
 
   UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
                                    initWithTarget:self
 
   UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
                                    initWithTarget:self
-                                   action:@selector(handleTap)];
+                                   action:@selector(handleTap:)];
   stap.numberOfTapsRequired = 1;
   stap.numberOfTouchesRequired = 1;
  
   stap.numberOfTapsRequired = 1;
   stap.numberOfTouchesRequired = 1;
  
@@ -2368,10 +2365,16 @@ to_pow2 (size_t x)
   hold.numberOfTouchesRequired = 1;
   hold.minimumPressDuration = 0.25;   /* 1/4th second */
 
   hold.numberOfTouchesRequired = 1;
   hold.minimumPressDuration = 0.25;   /* 1/4th second */
 
+  // Two finger pinch to zoom in on the view.
+  UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] 
+                                      initWithTarget:self 
+                                      action:@selector(handlePinch:)];
+
   [stap requireGestureRecognizerToFail: dtap];
   [stap requireGestureRecognizerToFail: hold];
   [dtap requireGestureRecognizerToFail: hold];
   [pan  requireGestureRecognizerToFail: hold];
   [stap requireGestureRecognizerToFail: dtap];
   [stap requireGestureRecognizerToFail: hold];
   [dtap requireGestureRecognizerToFail: hold];
   [pan  requireGestureRecognizerToFail: hold];
+  [pan2 requireGestureRecognizerToFail: pinch];
 
   [self setMultipleTouchEnabled:YES];
 
 
   [self setMultipleTouchEnabled:YES];
 
@@ -2380,12 +2383,14 @@ to_pow2 (size_t x)
   [self addGestureRecognizer: pan];
   [self addGestureRecognizer: pan2];
   [self addGestureRecognizer: hold];
   [self addGestureRecognizer: pan];
   [self addGestureRecognizer: pan2];
   [self addGestureRecognizer: hold];
+  [self addGestureRecognizer: pinch];
 
   [dtap release];
   [stap release];
   [pan  release];
   [pan2 release];
   [hold release];
 
   [dtap release];
   [stap release];
   [pan  release];
   [pan2 release];
   [hold release];
+  [pinch release];
 }
 
 
 }
 
 
@@ -2417,66 +2422,85 @@ to_pow2 (size_t x)
    
    Tests:
                      iPad2 iPadAir iPhone4s iPhone5 iPhone6+
    
    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
+     Attraction        X  yes  -       -       -       -       Y
+     Fireworkx X  no   -       -       -       -       Y
+     Carousel  GL yes  -       -       -       -       Y
+     Voronoi   GL no   -       -       -       -       -
  */
  */
-- (void) convertMouse:(int)rot x:(int*)x y:(int *)y
+- (void) convertMouse:(CGPoint *)p
 {
 {
-  int xx = *x, yy = *y;
+  CGFloat xx = p->x, yy = p->y;
 
 
-# if TARGET_IPHONE_SIMULATOR
+# if 0 // TARGET_IPHONE_SIMULATOR
   {
     XWindowAttributes xgwa;
     XGetWindowAttributes (xdpy, xwindow, &xgwa);
   {
     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, 
+    NSLog (@"TOUCH %4g, %-4g in %4d x %-4d  cs=%.0f hcs=%.0f r=%d ig=%d\n",
+           p->x, p->y,
            xgwa.width, xgwa.height,
            xgwa.width, xgwa.height,
-           ignore_rotation_p, [self reshapeRotatedWindow],
            [self contentScaleFactor],
            [self contentScaleFactor],
-           [self hackedContentScaleFactor]);
+           [self hackedContentScaleFactor],
+           [self rotateTouches], [self ignoreRotation]);
   }
 # endif // TARGET_IPHONE_SIMULATOR
 
   }
 # endif // TARGET_IPHONE_SIMULATOR
 
-  if (!ignore_rotation_p && [self reshapeRotatedWindow]) {
+  if ([self rotateTouches]) {
+
+    // The XScreenSaverGLView case:
+    // The X11 window is rotated, as is the framebuffer.
+    // The device coordinates match the framebuffer dimensions,
+    // but might have axes swapped... and we need to swap them
+    // by ratios.
     //
     //
-    // 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;
+    GLfloat xr = (GLfloat) xx / w;
+    GLfloat yr = (GLfloat) yy / h;
+    GLfloat swap;
+    int o = (int) current_device_rotation();
+    switch (o) {
+    case -90: case  270: swap = xr; xr = 1-yr; yr = swap;   break;
+    case  90: case -270: swap = xr; xr = yr;   yr = 1-swap; break;
+    case 180: case -180:            xr = 1-xr; yr = 1-yr;   break;
+    default: break;
+    }
+    xx = xr * w;
+    yy = yr * h;
+
+  } else if ([self ignoreRotation]) {
+
+    // The X11 case, where the hack has opted not to rotate:
+    // The X11 window is unrotated, but the framebuffer is rotated.
+    // The device coordinates match the framebuffer, so they need to
+    // be de-rotated to match the X11 window.
     //
     int w = [self frame].size.width;
     int h = [self frame].size.height;
     int swap;
     //
     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;
+    int o = (int) current_device_rotation();
+    switch (o) {
+    case -90: case  270: swap = xx; xx = h-yy; yy = swap;   break;
+    case  90: case -270: swap = xx; xx = yy;   yy = w-swap; break;
+    case 180: case -180:            xx = w-xx; yy = h-yy;   break;
+    default: break;
     }
   }
 
   double s = [self hackedContentScaleFactor];
     }
   }
 
   double s = [self hackedContentScaleFactor];
-  *x = xx * s;
-  *y = yy * s;
+  p->x = xx * s;
+  p->y = yy * s;
 
 
-# if TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
+# if 0 // TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
   {
     XWindowAttributes xgwa;
     XGetWindowAttributes (xdpy, xwindow, &xgwa);
   {
     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, 
+    NSLog (@"touch %4g, %-4g in %4d x %-4d  cs=%.0f hcs=%.0f r=%d ig=%d\n",
+           p->x, p->y,
            xgwa.width, xgwa.height,
            xgwa.width, xgwa.height,
-           ignore_rotation_p, [self reshapeRotatedWindow],
            [self contentScaleFactor],
            [self contentScaleFactor],
-           [self hackedContentScaleFactor]);
-    if (*x < 0 || *y < 0 || *x > xgwa.width || *y > xgwa.height)
+           [self hackedContentScaleFactor],
+           [self rotateTouches], [self ignoreRotation]);
+    if (p->x < 0 || p->y < 0 || p->x > xgwa.width || p->y > xgwa.height)
       abort();
   }
 # endif // TARGET_IPHONE_SIMULATOR
       abort();
   }
 # endif // TARGET_IPHONE_SIMULATOR
@@ -2485,9 +2509,36 @@ to_pow2 (size_t x)
 
 /* Single click exits saver.
  */
 
 /* Single click exits saver.
  */
-- (void) handleTap
+- (void) handleTap:(UIGestureRecognizer *)sender
 {
 {
-  [self stopAndClose:NO];
+  if (!xwindow)
+    return;
+
+  XEvent xe;
+  memset (&xe, 0, sizeof(xe));
+
+  [self showCloseButton];
+
+  CGPoint p = [sender locationInView:self];  // this is in points, not pixels
+  [self convertMouse:&p];
+  NSAssert (xwindow->type == WINDOW, @"not a window");
+  xwindow->window.last_mouse_x = p.x;
+  xwindow->window.last_mouse_y = p.y;
+
+  xe.xany.type = ButtonPress;
+  xe.xbutton.button = 1;
+  xe.xbutton.x = p.x;
+  xe.xbutton.y = p.y;
+
+  if (! [self sendEvent: &xe])
+    ; //[self beep];
+
+  xe.xany.type = ButtonRelease;
+  xe.xbutton.button = 1;
+  xe.xbutton.x = p.x;
+  xe.xbutton.y = p.y;
+
+  [self sendEvent: &xe];
 }
 
 
 }
 
 
@@ -2497,6 +2548,8 @@ to_pow2 (size_t x)
 {
   if (!xsft->event_cb || !xwindow) return;
 
 {
   if (!xsft->event_cb || !xwindow) return;
 
+  [self showCloseButton];
+
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
   xe.xkey.keycode = ' ';
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
   xe.xkey.keycode = ' ';
@@ -2515,34 +2568,36 @@ to_pow2 (size_t x)
 {
   if (!xsft->event_cb || !xwindow) return;
 
 {
   if (!xsft->event_cb || !xwindow) return;
 
+  [self showCloseButton];
+
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
 
   CGPoint p = [sender locationInView:self];  // this is in points, not pixels
   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);
+  [self convertMouse:&p];
+  NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
+  xwindow->window.last_mouse_x = p.x;
+  xwindow->window.last_mouse_y = p.y;
 
   switch (sender.state) {
   case UIGestureRecognizerStateBegan:
     xe.xany.type = ButtonPress;
     xe.xbutton.button = 1;
 
   switch (sender.state) {
   case UIGestureRecognizerStateBegan:
     xe.xany.type = ButtonPress;
     xe.xbutton.button = 1;
-    xe.xbutton.x = x;
-    xe.xbutton.y = y;
+    xe.xbutton.x = p.x;
+    xe.xbutton.y = p.y;
     break;
 
   case UIGestureRecognizerStateEnded:
     xe.xany.type = ButtonRelease;
     xe.xbutton.button = 1;
     break;
 
   case UIGestureRecognizerStateEnded:
     xe.xany.type = ButtonRelease;
     xe.xbutton.button = 1;
-    xe.xbutton.x = x;
-    xe.xbutton.y = y;
+    xe.xbutton.x = p.x;
+    xe.xbutton.y = p.y;
     break;
 
   case UIGestureRecognizerStateChanged:
     xe.xany.type = MotionNotify;
     break;
 
   case UIGestureRecognizerStateChanged:
     xe.xany.type = MotionNotify;
-    xe.xmotion.x = x;
-    xe.xmotion.y = y;
+    xe.xmotion.x = p.x;
+    xe.xmotion.y = p.y;
     break;
 
   default:
     break;
 
   default:
@@ -2571,6 +2626,8 @@ to_pow2 (size_t x)
 {
   if (!xsft->event_cb || !xwindow) return;
 
 {
   if (!xsft->event_cb || !xwindow) return;
 
+  [self showCloseButton];
+
   if (sender.state != UIGestureRecognizerStateEnded)
     return;
 
   if (sender.state != UIGestureRecognizerStateEnded)
     return;
 
@@ -2578,14 +2635,12 @@ to_pow2 (size_t x)
   memset (&xe, 0, sizeof(xe));
 
   CGPoint p = [sender locationInView:self];  // this is in points, not pixels
   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];
+  [self convertMouse:&p];
 
 
-  if (abs(x) > abs(y))
-    xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left);
+  if (fabs(p.x) > fabs(p.y))
+    xe.xkey.keycode = (p.x > 0 ? XK_Right : XK_Left);
   else
   else
-    xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up);
+    xe.xkey.keycode = (p.y > 0 ? XK_Down : XK_Up);
 
   BOOL ok1 = [self sendEvent: &xe];
   xe.xany.type = KeyRelease;
 
   BOOL ok1 = [self sendEvent: &xe];
   xe.xany.type = KeyRelease;
@@ -2595,6 +2650,102 @@ to_pow2 (size_t x)
 }
 
 
 }
 
 
+/* Pinch with 2 fingers: zoom in around the center of the fingers.
+ */
+- (void) handlePinch:(UIPinchGestureRecognizer *)sender
+{
+  if (!xsft->event_cb || !xwindow) return;
+
+  [self showCloseButton];
+
+  if (sender.state == UIGestureRecognizerStateBegan)
+    pinch_transform = self.transform;  // Save the base transform
+
+  switch (sender.state) {
+  case UIGestureRecognizerStateBegan:
+  case UIGestureRecognizerStateChanged:
+    {
+      double scale = sender.scale;
+
+      if (scale < 1)
+        return;
+
+      self.transform = CGAffineTransformScale (pinch_transform, scale, scale);
+
+      CGPoint p = [sender locationInView: self];
+      p.x /= self.layer.bounds.size.width;
+      p.y /= self.layer.bounds.size.height;
+
+      CGPoint np = CGPointMake (self.bounds.size.width * p.x,
+                                self.bounds.size.height * p.y);
+      CGPoint op = CGPointMake (self.bounds.size.width *
+                                self.layer.anchorPoint.x, 
+                                self.bounds.size.height *
+                                self.layer.anchorPoint.y);
+      np = CGPointApplyAffineTransform (np, self.transform);
+      op = CGPointApplyAffineTransform (op, self.transform);
+
+      CGPoint pos = self.layer.position;
+      pos.x -= op.x;
+      pos.x += np.x;
+      pos.y -= op.y;
+      pos.y += np.y;
+      self.layer.position = pos;
+      self.layer.anchorPoint = p;
+    }
+    break;
+
+  case UIGestureRecognizerStateEnded:
+    {
+      // When released, snap back to the default zoom (but animate it).
+
+      CABasicAnimation *a1 = [CABasicAnimation
+                               animationWithKeyPath:@"position.x"];
+      a1.fromValue = [NSNumber numberWithFloat: self.layer.position.x];
+      a1.toValue   = [NSNumber numberWithFloat: self.bounds.size.width / 2];
+
+      CABasicAnimation *a2 = [CABasicAnimation
+                               animationWithKeyPath:@"position.y"];
+      a2.fromValue = [NSNumber numberWithFloat: self.layer.position.y];
+      a2.toValue   = [NSNumber numberWithFloat: self.bounds.size.height / 2];
+
+      CABasicAnimation *a3 = [CABasicAnimation
+                               animationWithKeyPath:@"anchorPoint.x"];
+      a3.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.x];
+      a3.toValue   = [NSNumber numberWithFloat: 0.5];
+
+      CABasicAnimation *a4 = [CABasicAnimation
+                               animationWithKeyPath:@"anchorPoint.y"];
+      a4.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.y];
+      a4.toValue   = [NSNumber numberWithFloat: 0.5];
+
+      CABasicAnimation *a5 = [CABasicAnimation
+                               animationWithKeyPath:@"transform.scale"];
+      a5.fromValue = [NSNumber numberWithFloat: sender.scale];
+      a5.toValue   = [NSNumber numberWithFloat: 1.0];
+
+      CAAnimationGroup *group = [CAAnimationGroup animation];
+      group.duration     = 0.3;
+      group.repeatCount  = 1;
+      group.autoreverses = NO;
+      group.animations = @[ a1, a2, a3, a4, a5 ];
+      group.timingFunction = [CAMediaTimingFunction
+                               functionWithName:
+                                 kCAMediaTimingFunctionEaseIn];
+      [self.layer addAnimation:group forKey:@"unpinch"];
+
+      self.transform = pinch_transform;
+      self.layer.anchorPoint = CGPointMake (0.5, 0.5);
+      self.layer.position = CGPointMake (self.bounds.size.width / 2,
+                                         self.bounds.size.height / 2);
+    }
+    break;
+  default:
+    abort();
+  }
+}
+
+
 /* We need this to respond to "shake" gestures
  */
 - (BOOL)canBecomeFirstResponder
 /* We need this to respond to "shake" gestures
  */
 - (BOOL)canBecomeFirstResponder
@@ -2619,6 +2770,117 @@ to_pow2 (size_t x)
 }
 
 
 }
 
 
+- (void) showCloseButton
+{
+  double iw = 24;
+  double ih = iw;
+  double off = 4;
+
+  if (!closeBox) {
+    int width = self.bounds.size.width;
+    closeBox = [[UIView alloc]
+                initWithFrame:CGRectMake(0, 0, width, ih + off)];
+    closeBox.backgroundColor = [UIColor clearColor];
+    closeBox.autoresizingMask =
+      UIViewAutoresizingFlexibleBottomMargin |
+      UIViewAutoresizingFlexibleWidth;
+
+    // Add the buttons to the bar
+    UIImage *img1 = [UIImage imageNamed:@"stop"];
+    UIImage *img2 = [UIImage imageNamed:@"settings"];
+
+    UIButton *button = [[UIButton alloc] init];
+    [button setFrame: CGRectMake(off, off, iw, ih)];
+    [button setBackgroundImage:img1 forState:UIControlStateNormal];
+    [button addTarget:self
+            action:@selector(stopAndClose)
+            forControlEvents:UIControlEventTouchUpInside];
+    [closeBox addSubview:button];
+    [button release];
+
+    button = [[UIButton alloc] init];
+    [button setFrame: CGRectMake(width - iw - off, off, iw, ih)];
+    [button setBackgroundImage:img2 forState:UIControlStateNormal];
+    [button addTarget:self
+            action:@selector(stopAndOpenSettings)
+            forControlEvents:UIControlEventTouchUpInside];
+    button.autoresizingMask =
+      UIViewAutoresizingFlexibleBottomMargin |
+      UIViewAutoresizingFlexibleLeftMargin;
+    [closeBox addSubview:button];
+    [button release];
+
+    [self addSubview:closeBox];
+  }
+
+  // Don't hide the buttons under the iPhone X bezel.
+  UIEdgeInsets is = { 0, };
+  if ([self respondsToSelector:@selector(safeAreaInsets)]) {
+#   pragma clang diagnostic push   // "only available on iOS 11.0 or newer"
+#   pragma clang diagnostic ignored "-Wunguarded-availability-new"
+    is = [self safeAreaInsets];
+#   pragma clang diagnostic pop
+    [closeBox setFrame:CGRectMake(is.left, is.top,
+                                  self.bounds.size.width - is.right - is.left,
+                                  ih + off)];
+  }
+
+  if (closeBox.layer.opacity <= 0) {  // Fade in
+
+    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
+    anim.duration     = 0.2;
+    anim.repeatCount  = 1;
+    anim.autoreverses = NO;
+    anim.fromValue    = [NSNumber numberWithFloat:0.0];
+    anim.toValue      = [NSNumber numberWithFloat:1.0];
+    [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
+    closeBox.layer.opacity = 1;
+  }
+
+  // Fade out N seconds from now.
+  if (closeBoxTimer)
+    [closeBoxTimer invalidate];
+  closeBoxTimer = [NSTimer scheduledTimerWithTimeInterval: 3
+                           target:self
+                           selector:@selector(closeBoxOff)
+                           userInfo:nil
+                           repeats:NO];
+}
+
+
+- (void)closeBoxOff
+{
+  if (closeBoxTimer) {
+    [closeBoxTimer invalidate];
+    closeBoxTimer = 0;
+  }
+  if (!closeBox)
+    return;
+
+  CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
+  anim.duration     = 0.2;
+  anim.repeatCount  = 1;
+  anim.autoreverses = NO;
+  anim.fromValue    = [NSNumber numberWithFloat: 1];
+  anim.toValue      = [NSNumber numberWithFloat: 0];
+  [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
+  closeBox.layer.opacity = 0;
+}
+
+
+- (void) stopAndOpenSettings
+{
+  NSString *s = [NSString stringWithCString:xsft->progclass
+                          encoding:NSISOLatin1StringEncoding];
+  if ([self isAnimating])
+    [self stopAnimation];
+  [self resignFirstResponder];
+  [_delegate wantsFadeOut:self];
+  [_delegate openPreferences: s];
+
+}
+
+
 - (void)setScreenLocked:(BOOL)locked
 {
   if (screenLocked == locked) return;
 - (void)setScreenLocked:(BOOL)locked
 {
   if (screenLocked == locked) return;
@@ -2636,6 +2898,12 @@ to_pow2 (size_t x)
 {
   return [NSDictionary dictionaryWithObjectsAndKeys:
           kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
 {
   return [NSDictionary dictionaryWithObjectsAndKeys:
           kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
+# ifdef JWXYZ_GL
+          /* This could be disabled if we knew the screen would be redrawn
+             entirely for every frame.
+           */
+          [NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
+# endif // JWXYZ_GL
           nil];
 }
 
           nil];
 }
 
@@ -2643,6 +2911,13 @@ to_pow2 (size_t x)
 {
   // No extra renderbuffers are needed for 2D screenhacks.
 }
 {
   // No extra renderbuffers are needed for 2D screenhacks.
 }
+
+- (NSString *)getCAGravity
+{
+  return kCAGravityCenter;  // Looks better in e.g. Compass.
+//  return kCAGravityBottomLeft;
+}
 
 #endif // USE_IPHONE
 
 
 #endif // USE_IPHONE
 
@@ -2705,7 +2980,7 @@ to_pow2 (size_t x)
                    options:(NSWorkspaceLaunchWithoutAddingToRecents |
                             NSWorkspaceLaunchWithoutActivation |
                             NSWorkspaceLaunchAndHide)
                    options:(NSWorkspaceLaunchWithoutAddingToRecents |
                             NSWorkspaceLaunchWithoutActivation |
                             NSWorkspaceLaunchAndHide)
-                   configuration:nil
+                   configuration:[NSMutableDictionary dictionary]
                    error:&err]) {
     NSLog(@"Unable to launch %@: %@", app_path, err);
   }
                    error:&err]) {
     NSLog(@"Unable to launch %@: %@", app_path, err);
   }