From http://www.jwz.org/xscreensaver/xscreensaver-5.36.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
1 /* xscreensaver, Copyright (c) 2006-2016 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12 /* This is a subclass of Apple's ScreenSaverView that knows how to run
13    xscreensaver programs without X11 via the dark magic of the "jwxyz"
14    library.  In xscreensaver terminology, this is the replacement for
15    the "screenhack.c" module.
16  */
17
18 #import <QuartzCore/QuartzCore.h>
19 #import <sys/mman.h>
20 #import <zlib.h>
21 #import "XScreenSaverView.h"
22 #import "XScreenSaverConfigSheet.h"
23 #import "Updater.h"
24 #import "screenhackI.h"
25 #import "xlockmoreI.h"
26 #import "jwxyzI.h"
27 #import "jwxyz-cocoa.h"
28 #import "jwxyz-timers.h"
29
30 #ifdef USE_IPHONE
31 // XScreenSaverView.m speaks OpenGL ES just fine, but enableBackbuffer does
32 // need (jwzgles_)gluCheckExtension.
33 # import "jwzglesI.h"
34 #else
35 # import <OpenGL/glu.h>
36 #endif
37
38 /* Garbage collection only exists if we are being compiled against the 
39    10.6 SDK or newer, not if we are building against the 10.4 SDK.
40  */
41 #ifndef  MAC_OS_X_VERSION_10_6
42 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
43 #endif
44 #ifndef  MAC_OS_X_VERSION_10_12
45 # define MAC_OS_X_VERSION_10_12 101200
46 #endif
47 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 && \
48      MAC_OS_X_VERSION_MAX_ALLOWED <  MAC_OS_X_VERSION_10_12)
49   /* 10.6 SDK or later, and earlier than 10.12 SDK */
50 # import <objc/objc-auto.h>
51 # define DO_GC_HACKERY
52 #endif
53
54 /* Duplicated in xlockmoreI.h and XScreenSaverGLView.m. */
55 extern void clear_gl_error (void);
56 extern void check_gl_error (const char *type);
57
58 extern struct xscreensaver_function_table *xscreensaver_function_table;
59
60 /* Global variables used by the screen savers
61  */
62 const char *progname;
63 const char *progclass;
64 int mono_p = 0;
65
66
67 # ifdef USE_IPHONE
68
69 #  define NSSizeToCGSize(x) (x)
70
71 extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
72
73 /* Stub definition of the superclass, for iPhone.
74  */
75 @implementation ScreenSaverView
76 {
77   NSTimeInterval anim_interval;
78   Bool animating_p;
79   NSTimer *anim_timer;
80 }
81
82 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
83   self = [super initWithFrame:frame];
84   if (! self) return 0;
85   anim_interval = 1.0/30;
86   return self;
87 }
88 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
89 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
90 - (BOOL)hasConfigureSheet { return NO; }
91 - (NSWindow *)configureSheet { return nil; }
92 - (NSView *)configureView { return nil; }
93 - (BOOL)isPreview { return NO; }
94 - (BOOL)isAnimating { return animating_p; }
95 - (void)animateOneFrame { }
96
97 - (void)startAnimation {
98   if (animating_p) return;
99   animating_p = YES;
100   anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
101                         target:self
102                         selector:@selector(animateOneFrame)
103                         userInfo:nil
104                         repeats:YES];
105 }
106
107 - (void)stopAnimation {
108   if (anim_timer) {
109     [anim_timer invalidate];
110     anim_timer = 0;
111   }
112   animating_p = NO;
113 }
114 @end
115
116 # endif // !USE_IPHONE
117
118
119
120 @interface XScreenSaverView (Private)
121 - (void) stopAndClose:(Bool)relaunch;
122 @end
123
124 @implementation XScreenSaverView
125
126 // Given a lower-cased saver name, returns the function table for it.
127 // If no name, guess the name from the class's bundle name.
128 //
129 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
130 {
131   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
132   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
133
134   NSString *path = [nsb bundlePath];
135   CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
136                                                (CFStringRef) path,
137                                                kCFURLPOSIXPathStyle,
138                                                true);
139   CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
140   CFRelease (url);
141   NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
142   // #### Analyze says "Potential leak of an object stored into cfb"
143   
144   if (! name)
145     name = [[path lastPathComponent] stringByDeletingPathExtension];
146
147   name = [[name lowercaseString]
148            stringByReplacingOccurrencesOfString:@" "
149            withString:@""];
150
151 # ifndef USE_IPHONE
152   // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
153   // I'm guessing that symbol-stripping is mandatory.  Fuck.
154   NSString *table_name = [name stringByAppendingString:
155                                  @"_xscreensaver_function_table"];
156   void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
157   CFRelease (cfb);
158
159   if (! addr)
160     NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
161
162 # else  // USE_IPHONE
163   // Depends on the auto-generated "ios-function-table.m" being up to date.
164   if (! function_tables)
165     function_tables = [make_function_table_dict() retain];
166   NSValue *v = [function_tables objectForKey: name];
167   void *addr = v ? [v pointerValue] : 0;
168 # endif // USE_IPHONE
169
170   return (struct xscreensaver_function_table *) addr;
171 }
172
173
174 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
175 // to $PATH for the benefit of savers that include helper shell scripts.
176 //
177 - (void) setShellPath
178 {
179   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
180   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
181   
182   NSString *nsdir = [nsb resourcePath];
183   NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
184   const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
185   const char *opath = getenv ("PATH");
186   if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
187   char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 2);
188   strcpy (npath, dir);
189   strcat (npath, ":");
190   strcat (npath, opath);
191   if (setenv ("PATH", npath, 1)) {
192     perror ("setenv");
193     NSAssert1 (0, @"setenv \"PATH=%s\" failed", npath);
194   }
195
196   free (npath);
197 }
198
199
200 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
201 // (e.g., "xscreensaver-text") know how to look up resources.
202 //
203 - (void) setResourcesEnv:(NSString *) name
204 {
205   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
206   NSAssert1 (nsb, @"no bundle for class %@", [self class]);
207   
208   const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
209   if (setenv ("XSCREENSAVER_CLASSPATH", s, 1)) {
210     perror ("setenv");
211     NSAssert1 (0, @"setenv \"XSCREENSAVER_CLASSPATH=%s\" failed", s);
212   }
213 }
214
215
216 - (void) loadCustomFonts
217 {
218 # ifndef USE_IPHONE
219   NSBundle *nsb = [NSBundle bundleForClass:[self class]];
220   NSMutableArray *fonts = [NSMutableArray arrayWithCapacity:20];
221   for (NSString *ext in @[@"ttf", @"otf"]) {
222     [fonts addObjectsFromArray: [nsb pathsForResourcesOfType:ext
223                                      inDirectory:NULL]];
224   }
225   for (NSString *font in fonts) {
226     CFURLRef url = (CFURLRef) [NSURL fileURLWithPath: font];
227     CFErrorRef err = 0;
228     if (! CTFontManagerRegisterFontsForURL (url, kCTFontManagerScopeProcess,
229                                             &err)) {
230       // Just ignore errors:
231       // "The file has already been registered in the specified scope."
232       // NSLog (@"loading font: %@ %@", url, err);
233     }
234   }
235 # endif // !USE_IPHONE
236 }
237
238
239 static void
240 add_default_options (const XrmOptionDescRec *opts,
241                      const char * const *defs,
242                      XrmOptionDescRec **opts_ret,
243                      const char ***defs_ret)
244 {
245   /* These aren't "real" command-line options (there are no actual command-line
246      options in the Cocoa version); but this is the somewhat kludgey way that
247      the <xscreensaver-text /> and <xscreensaver-image /> tags in the
248      ../hacks/config/\*.xml files communicate with the preferences database.
249   */
250   static const XrmOptionDescRec default_options [] = {
251     { "-text-mode",              ".textMode",          XrmoptionSepArg, 0 },
252     { "-text-literal",           ".textLiteral",       XrmoptionSepArg, 0 },
253     { "-text-file",              ".textFile",          XrmoptionSepArg, 0 },
254     { "-text-url",               ".textURL",           XrmoptionSepArg, 0 },
255     { "-text-program",           ".textProgram",       XrmoptionSepArg, 0 },
256     { "-grab-desktop",           ".grabDesktopImages", XrmoptionNoArg, "True" },
257     { "-no-grab-desktop",        ".grabDesktopImages", XrmoptionNoArg, "False"},
258     { "-choose-random-images",   ".chooseRandomImages",XrmoptionNoArg, "True" },
259     { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
260     { "-image-directory",        ".imageDirectory",    XrmoptionSepArg, 0 },
261     { "-fps",                    ".doFPS",             XrmoptionNoArg, "True" },
262     { "-no-fps",                 ".doFPS",             XrmoptionNoArg, "False"},
263     { "-foreground",             ".foreground",        XrmoptionSepArg, 0 },
264     { "-fg",                     ".foreground",        XrmoptionSepArg, 0 },
265     { "-background",             ".background",        XrmoptionSepArg, 0 },
266     { "-bg",                     ".background",        XrmoptionSepArg, 0 },
267
268 # ifndef USE_IPHONE
269     // <xscreensaver-updater />
270     {    "-" SUSUEnableAutomaticChecksKey,
271          "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True"  },
272     { "-no-" SUSUEnableAutomaticChecksKey,
273          "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
274     {    "-" SUAutomaticallyUpdateKey,
275          "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True"  },
276     { "-no-" SUAutomaticallyUpdateKey,
277          "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
278     {    "-" SUSendProfileInfoKey,
279          "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
280     { "-no-" SUSendProfileInfoKey,
281          "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
282     {    "-" SUScheduledCheckIntervalKey,
283          "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
284 # endif // !USE_IPHONE
285
286     { 0, 0, 0, 0 }
287   };
288   static const char *default_defaults [] = {
289
290 # if defined(USE_IPHONE) && !defined(__OPTIMIZE__)
291     ".doFPS:              True",
292 # else
293     ".doFPS:              False",
294 # endif
295     ".doubleBuffer:       True",
296     ".multiSample:        False",
297 # ifndef USE_IPHONE
298     ".textMode:           date",
299 # else
300     ".textMode:           url",
301 # endif
302  // ".textLiteral:        ",
303  // ".textFile:           ",
304     ".textURL:            https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
305  // ".textProgram:        ",
306     ".grabDesktopImages:  yes",
307 # ifndef USE_IPHONE
308     ".chooseRandomImages: no",
309 # else
310     ".chooseRandomImages: yes",
311 # endif
312     ".imageDirectory:     ~/Pictures",
313     ".relaunchDelay:      2",
314     ".texFontCacheSize:   30",
315
316 # ifndef USE_IPHONE
317 #  define STR1(S) #S
318 #  define STR(S) STR1(S)
319 #  define __objc_yes Yes
320 #  define __objc_no  No
321     "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
322     "." SUAutomaticallyUpdateKey ":  "    STR(SUAutomaticallyUpdateDef),
323     "." SUSendProfileInfoKey ": "         STR(SUSendProfileInfoDef),
324     "." SUScheduledCheckIntervalKey ": "  STR(SUScheduledCheckIntervalDef),
325 #  undef __objc_yes
326 #  undef __objc_no
327 #  undef STR1
328 #  undef STR
329 # endif // USE_IPHONE
330     0
331   };
332
333   int count = 0, i, j;
334   for (i = 0; default_options[i].option; i++)
335     count++;
336   for (i = 0; opts[i].option; i++)
337     count++;
338
339   XrmOptionDescRec *opts2 = (XrmOptionDescRec *) 
340     calloc (count + 1, sizeof (*opts2));
341
342   i = 0;
343   j = 0;
344   while (default_options[j].option) {
345     opts2[i] = default_options[j];
346     i++, j++;
347   }
348   j = 0;
349   while (opts[j].option) {
350     opts2[i] = opts[j];
351     i++, j++;
352   }
353
354   *opts_ret = opts2;
355
356
357   /* now the defaults
358    */
359   count = 0;
360   for (i = 0; default_defaults[i]; i++)
361     count++;
362   for (i = 0; defs[i]; i++)
363     count++;
364
365   const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
366
367   i = 0;
368   j = 0;
369   while (default_defaults[j]) {
370     defs2[i] = default_defaults[j];
371     i++, j++;
372   }
373   j = 0;
374   while (defs[j]) {
375     defs2[i] = defs[j];
376     i++, j++;
377   }
378
379   *defs_ret = defs2;
380 }
381
382
383 - (id) initWithFrame:(NSRect)frame
384            saverName:(NSString *)saverName
385            isPreview:(BOOL)isPreview
386 {
387   if (! (self = [super initWithFrame:frame isPreview:isPreview]))
388     return 0;
389   
390   xsft = [self findFunctionTable: saverName];
391   if (! xsft) {
392     [self release];
393     return 0;
394   }
395
396   [self setShellPath];
397
398   setup_p = YES;
399   if (xsft->setup_cb)
400     xsft->setup_cb (xsft, xsft->setup_arg);
401
402   /* The plist files for these preferences show up in
403      $HOME/Library/Preferences/ByHost/ in a file named like
404      "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
405    */
406   NSString *name = [NSString stringWithCString:xsft->progclass
407                              encoding:NSISOLatin1StringEncoding];
408   name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
409   [self setResourcesEnv:name];
410   [self loadCustomFonts];
411   
412   XrmOptionDescRec *opts = 0;
413   const char **defs = 0;
414   add_default_options (xsft->options, xsft->defaults, &opts, &defs);
415   prefsReader = [[PrefsReader alloc]
416                   initWithName:name xrmKeys:opts defaults:defs];
417   free (defs);
418   // free (opts);  // bah, we need these! #### leak!
419   xsft->options = opts;
420   
421   progname = progclass = xsft->progclass;
422
423   next_frame_time = 0;
424
425 # if !defined USE_IPHONE && defined JWXYZ_QUARTZ
426   // When the view fills the screen and double buffering is enabled, OS X will
427   // use page flipping for a minor CPU/FPS boost. In windowed mode, double
428   // buffering reduces the frame rate to 1/2 the screen's refresh rate.
429   double_buffered_p = !isPreview;
430 # endif
431
432 # ifdef USE_IPHONE
433   [self initGestures];
434
435   // So we can tell when we're docked.
436   [UIDevice currentDevice].batteryMonitoringEnabled = YES;
437
438   [self setBackgroundColor:[NSColor blackColor]];
439 # endif // USE_IPHONE
440
441 # ifdef JWXYZ_QUARTZ
442   // Colorspaces and CGContexts only happen with non-GL hacks.
443   colorspace = CGColorSpaceCreateDeviceRGB ();
444 # endif
445
446   return self;
447 }
448
449
450 #ifdef USE_IPHONE
451 + (Class) layerClass
452 {
453   return [CAEAGLLayer class];
454 }
455 #endif
456
457
458 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
459 {
460   return [self initWithFrame:frame saverName:0 isPreview:p];
461 }
462
463
464 - (void) dealloc
465 {
466   if ([self isAnimating])
467     [self stopAnimation];
468   NSAssert(!xdata, @"xdata not yet freed");
469   NSAssert(!xdpy, @"xdpy not yet freed");
470
471 # ifdef USE_IPHONE
472   [[NSNotificationCenter defaultCenter] removeObserver:self];
473 # endif
474
475 #  ifdef BACKBUFFER_OPENGL
476 # ifndef USE_IPHONE
477   [pixfmt release];
478 # endif // !USE_IPHONE
479   [ogl_ctx release];
480   // Releasing the OpenGL context should also free any OpenGL objects,
481   // including the backbuffer texture and frame/render/depthbuffers.
482 #  endif // BACKBUFFER_OPENGL
483
484 # if defined JWXYZ_GL && defined USE_IPHONE
485   [ogl_ctx_pixmap release];
486 # endif // JWXYZ_GL
487
488 # ifdef JWXYZ_QUARTZ
489   if (colorspace)
490     CGColorSpaceRelease (colorspace);
491 # endif // JWXYZ_QUARTZ
492
493   [prefsReader release];
494
495   // xsft
496   // fpst
497
498   [super dealloc];
499 }
500
501 - (PrefsReader *) prefsReader
502 {
503   return prefsReader;
504 }
505
506
507 #ifdef USE_IPHONE
508 - (void) lockFocus { }
509 - (void) unlockFocus { }
510 #endif // USE_IPHONE
511
512
513
514 # ifdef USE_IPHONE
515 /* A few seconds after the saver launches, we store the "wasRunning"
516    preference.  This is so that if the saver is crashing at startup,
517    we don't launch it again next time, getting stuck in a crash loop.
518  */
519 - (void) allSystemsGo: (NSTimer *) timer
520 {
521   NSAssert (timer == crash_timer, @"crash timer screwed up");
522   crash_timer = 0;
523
524   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
525   [prefs setBool:YES forKey:@"wasRunning"];
526   [prefs synchronize];
527 }
528
529
530 - (void) resizeGL
531 {
532   if (!ogl_ctx)
533     return;
534
535   CGSize screen_size = self.bounds.size;
536   double s = self.contentScaleFactor;
537   screen_size.width *= s;
538   screen_size.height *= s;
539
540 #if defined JWXYZ_GL
541   GLuint *framebuffer = &xwindow->gl_framebuffer;
542   GLuint *renderbuffer = &xwindow->gl_renderbuffer;
543   xwindow->window.current_drawable = xwindow;
544 #elif defined JWXYZ_QUARTZ
545   GLuint *framebuffer = &gl_framebuffer;
546   GLuint *renderbuffer = &gl_renderbuffer;
547 #endif // JWXYZ_QUARTZ
548
549   if (*framebuffer)  glDeleteFramebuffersOES  (1, framebuffer);
550   if (*renderbuffer) glDeleteRenderbuffersOES (1, renderbuffer);
551
552   create_framebuffer (framebuffer, renderbuffer);
553
554   //   redundant?
555   //     glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES,
556   //                               (int)size.width, (int)size.height);
557   [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES
558                   fromDrawable:(CAEAGLLayer*)self.layer];
559
560   glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES,  GL_COLOR_ATTACHMENT0_OES,
561                                 GL_RENDERBUFFER_OES, *renderbuffer);
562
563   [self addExtraRenderbuffers:screen_size];
564
565   check_framebuffer_status();
566 }
567 #endif // USE_IPHONE
568
569
570 - (void) startAnimation
571 {
572   NSAssert(![self isAnimating], @"already animating");
573   NSAssert(!initted_p && !xdata, @"already initialized");
574
575   // See comment in render_x11() for why this value is important:
576   [self setAnimationTimeInterval: 1.0 / 240.0];
577
578   [super startAnimation];
579   /* We can't draw on the window from this method, so we actually do the
580      initialization of the screen saver (xsft->init_cb) in the first call
581      to animateOneFrame() instead.
582    */
583
584 # ifdef USE_IPHONE
585   if (crash_timer)
586     [crash_timer invalidate];
587
588   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
589   [prefs removeObjectForKey:@"wasRunning"];
590   [prefs synchronize];
591
592   crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
593                          target:self
594                          selector:@selector(allSystemsGo:)
595                          userInfo:nil
596                          repeats:NO];
597
598 # endif // USE_IPHONE
599
600   // Never automatically turn the screen off if we are docked,
601   // and an animation is running.
602   //
603 # ifdef USE_IPHONE
604   [UIApplication sharedApplication].idleTimerDisabled =
605     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
606   [[UIApplication sharedApplication]
607     setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
608 # endif
609
610   xwindow = (Window) calloc (1, sizeof(*xwindow));
611   xwindow->type = WINDOW;
612   xwindow->window.view = self;
613   CFRetain (xwindow->window.view);   // needed for garbage collection?
614
615 #ifdef BACKBUFFER_OPENGL
616   CGSize new_backbuffer_size;
617
618   {
619 # ifndef USE_IPHONE
620     if (!ogl_ctx) {
621
622       pixfmt = [self getGLPixelFormat];
623       [pixfmt retain];
624
625       NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");
626
627       // Fun: On OS X 10.7, the second time an OpenGL context is created, after
628       // the preferences dialog is launched in SaverTester, the context only
629       // lasts until the first full GC. Then it turns black. Solution is to
630       // reuse the OpenGL context after this point.
631       // "Analyze" says that both pixfmt and ogl_ctx are leaked.
632       ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
633                                          shareContext:nil];
634
635       // Sync refreshes to the vertical blanking interval
636       GLint r = 1;
637       [ogl_ctx setValues:&r forParameter:NSOpenGLCPSwapInterval];
638 //    check_gl_error ("NSOpenGLCPSwapInterval");  // SEGV sometimes. Too early?
639     }
640
641     [ogl_ctx makeCurrentContext];
642     check_gl_error ("makeCurrentContext");
643
644     // NSOpenGLContext logs an 'invalid drawable' when this is called
645     // from initWithFrame.
646     [ogl_ctx setView:self];
647
648     // This may not be necessary if there's FBO support.
649 #  ifdef JWXYZ_GL
650     xwindow->window.pixfmt = pixfmt;
651     CFRetain (xwindow->window.pixfmt);
652     xwindow->window.virtual_screen = [ogl_ctx currentVirtualScreen];
653     xwindow->window.current_drawable = xwindow;
654     NSAssert (ogl_ctx, @"no CGContext");
655 #  endif
656
657     // Clear frame buffer ASAP, else there are bits left over from other apps.
658     glClearColor (0, 0, 0, 1);
659     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
660 //    glFinish ();
661 //    glXSwapBuffers (mi->dpy, mi->window);
662
663
664     // Enable multi-threading, if possible.  This runs most OpenGL commands
665     // and GPU management on a second CPU.
666     {
667 #  ifndef  kCGLCEMPEngine
668 #   define kCGLCEMPEngine 313  // Added in MacOS 10.4.8 + XCode 2.4.
669 #  endif
670       CGLContextObj cctx = CGLGetCurrentContext();
671       CGLError err = CGLEnable (cctx, kCGLCEMPEngine);
672       if (err != kCGLNoError) {
673         NSLog (@"enabling multi-threaded OpenGL failed: %d", err);
674       }
675     }
676
677     new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
678
679 # else  // USE_IPHONE
680     if (!ogl_ctx) {
681       CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
682       eagl_layer.opaque = TRUE;
683       eagl_layer.drawableProperties = [self getGLProperties];
684
685       // Without this, the GL frame buffer is half the screen resolution!
686       eagl_layer.contentsScale = [UIScreen mainScreen].scale;
687
688       ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
689 # ifdef JWXYZ_GL
690       ogl_ctx_pixmap = [[EAGLContext alloc]
691                         initWithAPI:kEAGLRenderingAPIOpenGLES1
692                         sharegroup:ogl_ctx.sharegroup];
693 # endif // JWXYZ_GL
694
695       eagl_layer.contentsGravity = [self getCAGravity];
696     }
697
698 # ifdef JWXYZ_GL
699     xwindow->window.ogl_ctx_pixmap = ogl_ctx_pixmap;
700 # endif // JWXYZ_GL
701
702     [EAGLContext setCurrentContext: ogl_ctx];
703
704     [self resizeGL];
705
706     double s = [self hackedContentScaleFactor];
707     new_backbuffer_size = self.bounds.size;
708     new_backbuffer_size.width *= s;
709     new_backbuffer_size.height *= s;
710
711 # endif // USE_IPHONE
712
713 # ifdef JWXYZ_GL
714     xwindow->ogl_ctx = ogl_ctx;
715 #  ifndef USE_IPHONE
716     CFRetain (xwindow->ogl_ctx);
717 #  endif // USE_IPHONE
718 # endif // JWXYZ_GL
719
720     check_gl_error ("startAnimation");
721
722 //  NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
723 //         glGetString (GL_RENDERER), glGetString (GL_VERSION));
724
725     [self enableBackbuffer:new_backbuffer_size];
726   }
727 #endif // BACKBUFFER_OPENGL
728
729   [self setViewport];
730   [self createBackbuffer:new_backbuffer_size];
731 }
732
733 - (void)stopAnimation
734 {
735   NSAssert([self isAnimating], @"not animating");
736
737   if (initted_p) {
738
739     [self lockFocus];       // in case something tries to draw from here
740     [self prepareContext];
741
742     /* I considered just not even calling the free callback at all...
743        But webcollage-cocoa needs it, to kill the inferior webcollage
744        processes (since the screen saver framework never generates a
745        SIGPIPE for them...)  Instead, I turned off the free call in
746        xlockmore.c, which is where all of the bogus calls are anyway.
747      */
748     xsft->free_cb (xdpy, xwindow, xdata);
749     [self unlockFocus];
750
751     jwxyz_free_display (xdpy);
752     xdpy = NULL;
753 # if defined JWXYZ_GL && !defined USE_IPHONE
754     CFRelease (xwindow->ogl_ctx);
755 # endif
756     CFRelease (xwindow->window.view);
757     free (xwindow);
758     xwindow = NULL;
759
760 //  setup_p = NO; // #### wait, do we need this?
761     initted_p = NO;
762     xdata = 0;
763   }
764
765 # ifdef USE_IPHONE
766   if (crash_timer)
767     [crash_timer invalidate];
768   crash_timer = 0;
769   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
770   [prefs removeObjectForKey:@"wasRunning"];
771   [prefs synchronize];
772 # endif // USE_IPHONE
773
774   [super stopAnimation];
775
776   // When an animation is no longer running (e.g., looking at the list)
777   // then it's ok to power off the screen when docked.
778   //
779 # ifdef USE_IPHONE
780   [UIApplication sharedApplication].idleTimerDisabled = NO;
781   [[UIApplication sharedApplication]
782     setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
783 # endif
784
785   // Without this, the GL frame stays on screen when switching tabs
786   // in System Preferences.
787   // (Or perhaps it used to. It doesn't seem to matter on 10.9.)
788   //
789 # ifndef USE_IPHONE
790   [NSOpenGLContext clearCurrentContext];
791 # endif // !USE_IPHONE
792
793   clear_gl_error();     // This hack is defunct, don't let this linger.
794
795 # ifdef JWXYZ_QUARTZ
796   CGContextRelease (backbuffer);
797   backbuffer = nil;
798
799   if (backbuffer_len)
800     munmap (backbuffer_data, backbuffer_len);
801   backbuffer_data = NULL;
802   backbuffer_len = 0;
803 # endif
804 }
805
806
807 - (NSOpenGLContext *) oglContext
808 {
809   return ogl_ctx;
810 }
811
812
813 // #### maybe this could/should just be on 'lockFocus' instead?
814 - (void) prepareContext
815 {
816   if (xwindow) {
817 #ifdef USE_IPHONE
818     [EAGLContext setCurrentContext:ogl_ctx];
819 #else  // !USE_IPHONE
820     [ogl_ctx makeCurrentContext];
821 //    check_gl_error ("makeCurrentContext");
822 #endif // !USE_IPHONE
823
824 #ifdef JWXYZ_GL
825     xwindow->window.current_drawable = xwindow;
826 #endif
827   }
828 }
829
830
831 static void
832 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
833 {
834   fps_compute (fpst, 0, -1);
835   fps_draw (fpst);
836 }
837
838
839 #ifdef USE_IPHONE
840
841 /* On iPhones with Retina displays, we can draw the savers in "real"
842    pixels, and that works great.  The 320x480 "point" screen is really
843    a 640x960 *pixel* screen.  However, Retina iPads have 768x1024
844    point screens which are 1536x2048 pixels, and apparently that's
845    enough pixels that copying those bits to the screen is slow.  Like,
846    drops us from 15fps to 7fps.  So, on Retina iPads, we don't draw in
847    real pixels.  This will probably make the savers look better
848    anyway, since that's a higher resolution than most desktop monitors
849    have even today.  (This is only true for X11 programs, not GL 
850    programs.  Those are fine at full rez.)
851
852    This method is overridden in XScreenSaverGLView, since this kludge
853    isn't necessary for GL programs, being resolution independent by
854    nature.
855  */
856 - (CGFloat) hackedContentScaleFactor
857 {
858   NSSize bsize = [self bounds].size;
859
860   CGFloat
861     max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height;
862
863   // Ratio of screen size in pixels to view size in points.
864   CGFloat s = self.contentScaleFactor;
865
866   // Two constraints:
867
868   // 1. Don't exceed -- let's say 1280 pixels in either direction.
869   //    (Otherwise the frame rate gets bad.)
870   //    Actually let's make that 1440 since iPhone 6 is natively 1334.
871   CGFloat mag0 = ceil(max_bsize * s / 1440);
872
873   // 2. Don't let the pixel size get too small.
874   //    (Otherwise pixels in IFS and similar are too fine.)
875   //    So don't let the result be > 2 pixels per point.
876   CGFloat mag1 = ceil(s / 2);
877
878   // As of iPhone 6, mag0 is always >= mag1. This may not be true in the future.
879   // (desired scale factor) = s / (desired magnification factor)
880   return s / (mag0 > mag1 ? mag0 : mag1);
881 }
882
883
884 double
885 current_device_rotation (void)
886 {
887   UIDeviceOrientation o = [[UIDevice currentDevice] orientation];
888
889   /* Sometimes UIDevice doesn't know the proper orientation, or the device is
890      face up/face down, so in those cases fall back to the status bar
891      orientation. The SaverViewController tries to set the status bar to the
892      proper orientation before it creates the XScreenSaverView; see
893      _storedOrientation in SaverViewController.
894    */
895   if (o == UIDeviceOrientationUnknown ||
896       o == UIDeviceOrientationFaceUp  ||
897       o == UIDeviceOrientationFaceDown) {
898     /* Mind the differences between UIInterfaceOrientation and
899        UIDeviceOrientation:
900        1. UIInterfaceOrientation does not include FaceUp and FaceDown.
901        2. LandscapeLeft and LandscapeRight are swapped between the two. But
902           converting between device and interface orientation doesn't need to
903           take this into account, because (from the UIInterfaceOrientation
904           description): "rotating the device requires rotating the content in
905           the opposite direction."
906          */
907     /* statusBarOrientation deprecated in iOS 9 */
908     o = [UIApplication sharedApplication].statusBarOrientation;
909   }
910
911   switch (o) {
912   case UIDeviceOrientationLandscapeLeft:      return -90; break;
913   case UIDeviceOrientationLandscapeRight:     return  90; break;
914   case UIDeviceOrientationPortraitUpsideDown: return 180; break;
915   default:                                    return 0;   break;
916   }
917 }
918
919
920 - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
921 {
922   if (i == 0) exit (-1);        // Cancel
923   [self stopAndClose:NO];       // Keep going
924 }
925
926 - (void) handleException: (NSException *)e
927 {
928   NSLog (@"Caught exception: %@", e);
929   [[[UIAlertView alloc] initWithTitle:
930                           [NSString stringWithFormat: @"%s crashed!",
931                                     xsft->progclass]
932                         message:
933                           [NSString stringWithFormat:
934                                       @"The error message was:"
935                                     "\n\n%@\n\n"
936                                     "If it keeps crashing, try "
937                                     "resetting its options.",
938                                     e]
939                         delegate: self
940                         cancelButtonTitle: @"Exit"
941                         otherButtonTitles: @"Keep going", nil]
942     show];
943   [self stopAnimation];
944 }
945
946 #endif // USE_IPHONE
947
948
949 #ifdef JWXYZ_QUARTZ
950
951 # ifndef USE_IPHONE
952
953 struct gl_version
954 {
955   // iOS always uses OpenGL ES 1.1.
956   unsigned major;
957   unsigned minor;
958 };
959
960 static GLboolean
961 gl_check_ver (const struct gl_version *caps,
962               unsigned gl_major,
963               unsigned gl_minor)
964 {
965   return caps->major > gl_major ||
966            (caps->major == gl_major && caps->minor >= gl_minor);
967 }
968
969 # endif
970
971 /* Called during startAnimation before the first call to createBackbuffer. */
972 - (void) enableBackbuffer:(CGSize)new_backbuffer_size
973 {
974 # ifndef USE_IPHONE
975   struct gl_version version;
976
977   {
978     const char *version_str = (const char *)glGetString (GL_VERSION);
979
980     /* iPhone is always OpenGL ES 1.1. */
981     if (sscanf ((const char *)version_str, "%u.%u",
982                 &version.major, &version.minor) < 2)
983     {
984       version.major = 1;
985       version.minor = 1;
986     }
987   }
988 # endif
989
990   // The OpenGL extensions in use in here are pretty are pretty much ubiquitous
991   // on OS X, but it's still good form to check.
992   const GLubyte *extensions = glGetString (GL_EXTENSIONS);
993
994   glGenTextures (1, &backbuffer_texture);
995
996   // On really old systems, it would make sense to split the texture
997   // into subsections
998 # ifndef USE_IPHONE
999   gl_texture_target = (gluCheckExtension ((const GLubyte *)
1000                                          "GL_ARB_texture_rectangle",
1001                                          extensions)
1002                        ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D);
1003 # else
1004   // OES_texture_npot also provides this, but iOS never provides it.
1005   gl_limited_npot_p = jwzgles_gluCheckExtension
1006     ((const GLubyte *) "GL_APPLE_texture_2D_limited_npot", extensions);
1007   gl_texture_target = GL_TEXTURE_2D;
1008 # endif
1009
1010   glBindTexture (gl_texture_target, backbuffer_texture);
1011   glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1012   // GL_LINEAR might make sense on Retina iPads.
1013   glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
1014   glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1015   glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1016
1017 # ifndef USE_IPHONE
1018   // There isn't much sense in supporting one of these if the other
1019   // isn't present.
1020   gl_apple_client_storage_p =
1021     gluCheckExtension ((const GLubyte *)"GL_APPLE_client_storage",
1022                        extensions) &&
1023     gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_range", extensions);
1024
1025   if (gl_apple_client_storage_p) {
1026     glTexParameteri (gl_texture_target, GL_TEXTURE_STORAGE_HINT_APPLE,
1027                      GL_STORAGE_SHARED_APPLE);
1028     glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
1029   }
1030 # endif
1031
1032   // If a video adapter suports BGRA textures, then that's probably as fast as
1033   // you're gonna get for getting a texture onto the screen.
1034 # ifdef USE_IPHONE
1035   gl_pixel_format =
1036     jwzgles_gluCheckExtension
1037       ((const GLubyte *)"GL_APPLE_texture_format_BGRA8888", extensions) ?
1038       GL_BGRA :
1039       GL_RGBA;
1040
1041   gl_pixel_type = GL_UNSIGNED_BYTE;
1042   // See also OES_read_format.
1043 # else
1044   if (gl_check_ver (&version, 1, 2) ||
1045       (gluCheckExtension ((const GLubyte *)"GL_EXT_bgra", extensions) &&
1046        gluCheckExtension ((const GLubyte *)"GL_APPLE_packed_pixels",
1047                           extensions))) {
1048     gl_pixel_format = GL_BGRA;
1049     // Both Intel and PowerPC-era docs say to use GL_UNSIGNED_INT_8_8_8_8_REV.
1050     gl_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1051   } else {
1052     gl_pixel_format = GL_RGBA;
1053     gl_pixel_type = GL_UNSIGNED_BYTE;
1054   }
1055   // GL_ABGR_EXT/GL_UNSIGNED_BYTE is another possibilty that may have made more
1056   // sense on PowerPC.
1057 # endif
1058
1059   glEnable (gl_texture_target);
1060   glEnableClientState (GL_VERTEX_ARRAY);
1061   glEnableClientState (GL_TEXTURE_COORD_ARRAY);
1062
1063   check_gl_error ("enableBackbuffer");
1064 }
1065
1066
1067 static GLsizei
1068 to_pow2 (size_t x)
1069 {
1070   if (x <= 1)
1071     return 1;
1072
1073   size_t mask = (size_t)-1;
1074   unsigned bits = sizeof(x) * CHAR_BIT;
1075   unsigned log2 = bits;
1076
1077   --x;
1078   while (bits) {
1079     if (!(x & mask)) {
1080       log2 -= bits;
1081       x <<= bits;
1082     }
1083
1084     bits >>= 1;
1085     mask <<= bits;
1086   }
1087
1088   return 1 << log2;
1089 }
1090
1091
1092 #ifdef USE_IPHONE
1093 - (BOOL) suppressRotationAnimation
1094 {
1095   return [self ignoreRotation]; // Don't animate if we aren't rotating
1096 }
1097
1098 - (BOOL) rotateTouches
1099 {
1100   return FALSE;                 // Adjust event coordinates only if rotating
1101 }
1102 #endif
1103
1104
1105 - (void) setViewport
1106 {
1107 # ifdef BACKBUFFER_OPENGL
1108   NSAssert ([NSOpenGLContext currentContext] ==
1109             ogl_ctx, @"invalid GL context");
1110
1111   NSSize new_size = self.bounds.size;
1112
1113 #  ifdef USE_IPHONE
1114   GLfloat s = self.contentScaleFactor;
1115   GLfloat hs = self.hackedContentScaleFactor;
1116 #  else // !USE_IPHONE
1117   const GLfloat s = 1;
1118   const GLfloat hs = s;
1119 #  endif
1120
1121   // On OS X this almost isn't necessary, except for the ugly aliasing
1122   // artifacts.
1123   glViewport (0, 0, new_size.width * s, new_size.height * s);
1124
1125   glMatrixMode (GL_PROJECTION);
1126   glLoadIdentity();
1127 #  ifdef USE_IPHONE
1128   glOrthof
1129 #  else
1130   glOrtho
1131 #  endif
1132     (-new_size.width * hs, new_size.width * hs,
1133      -new_size.height * hs, new_size.height * hs,
1134      -1, 1);
1135
1136 #  ifdef USE_IPHONE
1137   if ([self ignoreRotation]) {
1138     int o = (int) -current_device_rotation();
1139     glRotatef (o, 0, 0, 1);
1140   }
1141 #  endif // USE_IPHONE
1142 # endif // BACKBUFFER_OPENGL
1143 }
1144
1145
1146 /* Create a bitmap context into which we render everything.
1147    If the desired size has changed, re-created it.
1148    new_size is in rotated pixels, not points: the same size
1149    and shape as the X11 window as seen by the hacks.
1150  */
1151 - (void) createBackbuffer:(CGSize)new_size
1152 {
1153   CGSize osize = CGSizeZero;
1154   if (backbuffer) {
1155     osize.width = CGBitmapContextGetWidth(backbuffer);
1156     osize.height = CGBitmapContextGetHeight(backbuffer);
1157   }
1158
1159   if (backbuffer &&
1160       (int)osize.width  == (int)new_size.width &&
1161       (int)osize.height == (int)new_size.height)
1162     return;
1163
1164   CGContextRef ob = backbuffer;
1165   void *odata = backbuffer_data;
1166   size_t olen = backbuffer_len;
1167
1168 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1169   NSLog(@"backbuffer %.0fx%.0f",
1170         new_size.width, new_size.height);
1171 # endif
1172
1173   /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
1174      <https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html>.
1175
1176      iOS uses bog-standard glTexImage2D (for now).
1177
1178      glMapBuffer is the standard way to get data from system RAM to video
1179      memory asynchronously and without a memcpy, but support for
1180      APPLE_client_storage is ubiquitous on OS X (not so for glMapBuffer),
1181      and on iOS GL_PIXEL_UNPACK_BUFFER is only available on OpenGL ES 3
1182      (iPhone 5S or newer). Plus, glMapBuffer doesn't work well with
1183      CGBitmapContext: glMapBuffer can return a different pointer on each
1184      call, but a CGBitmapContext doesn't allow its data pointer to be
1185      changed -- and recreating the context for a new pointer can be
1186      expensive (glyph caches get dumped, for instance).
1187
1188      glMapBufferRange has MAP_FLUSH_EXPLICIT_BIT and MAP_UNSYNCHRONIZED_BIT,
1189      and these seem to allow mapping the buffer and leaving it where it is
1190      in client address space while OpenGL works with the buffer, but it
1191      requires OpenGL 3 Core profile on OS X (and ES 3 on iOS for
1192      GL_PIXEL_UNPACK_BUFFER), so point goes to APPLE_client_storage.
1193
1194      AMD_pinned_buffer provides the same advantage as glMapBufferRange, but
1195      Apple never implemented that one for OS X.
1196    */
1197
1198   backbuffer_data = NULL;
1199   gl_texture_w = (int)new_size.width;
1200   gl_texture_h = (int)new_size.height;
1201
1202   NSAssert (gl_texture_target == GL_TEXTURE_2D
1203 # ifndef USE_IPHONE
1204             || gl_texture_target == GL_TEXTURE_RECTANGLE_EXT
1205 # endif
1206                   , @"unexpected GL texture target");
1207
1208 # ifndef USE_IPHONE
1209   if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1210 # else
1211   if (!gl_limited_npot_p)
1212 # endif
1213   {
1214     gl_texture_w = to_pow2 (gl_texture_w);
1215     gl_texture_h = to_pow2 (gl_texture_h);
1216   }
1217
1218   size_t bytes_per_row = gl_texture_w * 4;
1219
1220 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1221   // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
1222   // it will fall back to a memcpy.
1223   // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW24
1224   bytes_per_row = (bytes_per_row + 31) & ~31;
1225 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1226
1227   backbuffer_len = bytes_per_row * gl_texture_h;
1228   if (backbuffer_len) // mmap requires this to be non-zero.
1229     backbuffer_data = mmap (NULL, backbuffer_len,
1230                             PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED,
1231                             -1, 0);
1232
1233   BOOL alpha_first_p, order_little_p;
1234
1235   if (gl_pixel_format == GL_BGRA) {
1236     alpha_first_p = YES;
1237     order_little_p = YES;
1238 /*
1239   } else if (gl_pixel_format == GL_ABGR_EXT) {
1240     alpha_first_p = NO;
1241     order_little_p = YES; */
1242   } else {
1243     NSAssert (gl_pixel_format == GL_RGBA, @"unknown GL pixel format");
1244     alpha_first_p = NO;
1245     order_little_p = NO;
1246   }
1247
1248 #ifdef USE_IPHONE
1249   NSAssert (gl_pixel_type == GL_UNSIGNED_BYTE, @"unknown GL pixel type");
1250 #else
1251   NSAssert (gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8 ||
1252             gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8_REV ||
1253             gl_pixel_type == GL_UNSIGNED_BYTE,
1254             @"unknown GL pixel type");
1255
1256 #if defined __LITTLE_ENDIAN__
1257   const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8;
1258 #elif defined __BIG_ENDIAN__
1259   const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1260 #else
1261 # error Unknown byte order.
1262 #endif
1263
1264   if (gl_pixel_type == backwards_pixel_type)
1265     order_little_p ^= YES;
1266 #endif
1267
1268   CGBitmapInfo bitmap_info =
1269     (alpha_first_p ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaNoneSkipLast) |
1270     (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
1271
1272   backbuffer = CGBitmapContextCreate (backbuffer_data,
1273                                       (int)new_size.width,
1274                                       (int)new_size.height,
1275                                       8,
1276                                       bytes_per_row,
1277                                       colorspace,
1278                                       bitmap_info);
1279   NSAssert (backbuffer, @"unable to allocate back buffer");
1280
1281   // Clear it.
1282   CGRect r;
1283   r.origin.x = r.origin.y = 0;
1284   r.size = new_size;
1285   CGContextSetGrayFillColor (backbuffer, 0, 1);
1286   CGContextFillRect (backbuffer, r);
1287
1288 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1289   if (gl_apple_client_storage_p)
1290     glTextureRangeAPPLE (gl_texture_target, backbuffer_len, backbuffer_data);
1291 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1292
1293   if (ob) {
1294     // Restore old bits, as much as possible, to the X11 upper left origin.
1295
1296     CGRect rect;   // pixels, not points
1297     rect.origin.x = 0;
1298     rect.origin.y = (new_size.height - osize.height);
1299     rect.size = osize;
1300
1301     CGImageRef img = CGBitmapContextCreateImage (ob);
1302     CGContextDrawImage (backbuffer, rect, img);
1303     CGImageRelease (img);
1304     CGContextRelease (ob);
1305
1306     if (olen)
1307       // munmap should round len up to the nearest page.
1308       munmap (odata, olen);
1309   }
1310
1311   check_gl_error ("createBackbuffer");
1312 }
1313
1314
1315 - (void) drawBackbuffer
1316 {
1317 # ifdef BACKBUFFER_OPENGL
1318
1319   NSAssert ([ogl_ctx isKindOfClass:[NSOpenGLContext class]],
1320             @"ogl_ctx is not an NSOpenGLContext");
1321
1322   NSAssert (! (CGBitmapContextGetBytesPerRow (backbuffer) % 4),
1323             @"improperly-aligned backbuffer");
1324
1325   // This gets width and height from the backbuffer in case
1326   // APPLE_client_storage is in use. See the note in createBackbuffer.
1327   // This still has to happen every frame even when APPLE_client_storage has
1328   // the video adapter pulling texture data straight from
1329   // XScreenSaverView-owned memory.
1330   glTexImage2D (gl_texture_target, 0, GL_RGBA,
1331                 (GLsizei)(CGBitmapContextGetBytesPerRow (backbuffer) / 4),
1332                 gl_texture_h, 0, gl_pixel_format, gl_pixel_type,
1333                 backbuffer_data);
1334
1335   GLfloat w = xwindow->frame.width, h = xwindow->frame.height;
1336
1337   GLfloat vertices[4][2] = {{-w,  h}, {w,  h}, {w, -h}, {-w, -h}};
1338
1339   GLfloat tex_coords[4][2];
1340
1341 #  ifndef USE_IPHONE
1342   if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1343 #  endif // USE_IPHONE
1344   {
1345     w /= gl_texture_w;
1346     h /= gl_texture_h;
1347   }
1348
1349   tex_coords[0][0] = 0;
1350   tex_coords[0][1] = 0;
1351   tex_coords[1][0] = w;
1352   tex_coords[1][1] = 0;
1353   tex_coords[2][0] = w;
1354   tex_coords[2][1] = h;
1355   tex_coords[3][0] = 0;
1356   tex_coords[3][1] = h;
1357
1358   glVertexPointer (2, GL_FLOAT, 0, vertices);
1359   glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
1360   glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
1361
1362 #  if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1363   check_gl_error ("drawBackbuffer");
1364 #  endif
1365 # endif // BACKBUFFER_OPENGL
1366 }
1367
1368 #endif // JWXYZ_QUARTZ
1369
1370 #ifdef JWXYZ_GL
1371
1372 - (void)enableBackbuffer:(CGSize)new_backbuffer_size;
1373 {
1374   jwxyz_set_matrices (new_backbuffer_size.width, new_backbuffer_size.height);
1375   check_gl_error ("enableBackbuffer");
1376 }
1377
1378 - (void)createBackbuffer:(CGSize)new_size
1379 {
1380   NSAssert ([NSOpenGLContext currentContext] ==
1381             ogl_ctx, @"invalid GL context");
1382   NSAssert (xwindow->window.current_drawable == xwindow,
1383             @"current_drawable not set properly");
1384
1385 # ifndef USE_IPHONE
1386   /* On iOS, Retina means glViewport gets called with the screen size instead
1387      of the backbuffer/xwindow size. This happens in startAnimation.
1388
1389      The GL screenhacks call glViewport themselves.
1390    */
1391   glViewport (0, 0, new_size.width, new_size.height);
1392 # endif
1393
1394   // TODO: Preserve contents on resize.
1395   glClear (GL_COLOR_BUFFER_BIT);
1396   check_gl_error ("createBackbuffer");
1397 }
1398
1399 #endif // JWXYZ_GL
1400
1401
1402 - (void)flushBackbuffer
1403 {
1404 # ifdef JWXYZ_GL
1405   // Make sure the right context is active: there's two under JWXYZ_GL.
1406   jwxyz_bind_drawable (xwindow, xwindow);
1407 # endif // JWXYZ_GL
1408
1409 # ifndef USE_IPHONE
1410
1411 #  ifdef JWXYZ_QUARTZ
1412   // The OpenGL pipeline is not automatically synchronized with the contents
1413   // of the backbuffer, so without glFinish, OpenGL can start rendering from
1414   // the backbuffer texture at the same time that JWXYZ is clearing and
1415   // drawing the next frame in the backing store for the backbuffer texture.
1416   // This is only a concern under JWXYZ_QUARTZ because of
1417   // APPLE_client_storage; JWXYZ_GL doesn't use that.
1418   glFinish();
1419 #  endif // JWXYZ_QUARTZ
1420
1421   // If JWXYZ_GL was single-buffered, there would need to be a glFinish (or
1422   // maybe just glFlush?) here, because single-buffered contexts don't always
1423   // update what's on the screen after drawing finishes. (i.e., in safe mode)
1424
1425 #  ifdef JWXYZ_QUARTZ
1426   // JWXYZ_GL is always double-buffered.
1427   if (double_buffered_p)
1428 #  endif // JWXYZ_QUARTZ
1429     [ogl_ctx flushBuffer]; // despite name, this actually swaps
1430 # else // USE_IPHONE
1431
1432   // jwxyz_bind_drawable() only binds the framebuffer, not the renderbuffer.
1433 #  ifdef JWXYZ_GL
1434   GLint gl_renderbuffer = xwindow->gl_renderbuffer;
1435 #  endif
1436
1437   glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
1438   [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
1439 # endif // USE_IPHONE
1440
1441 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1442   // glGetError waits for the OpenGL command pipe to flush, so skip it in
1443   // release builds.
1444   // OpenGL Programming Guide for Mac -> OpenGL Application Design
1445   // Strategies -> Allow OpenGL to Manage Your Resources
1446   // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_designstrategies/opengl_designstrategies.html#//apple_ref/doc/uid/TP40001987-CH2-SW7
1447   check_gl_error ("flushBackbuffer");
1448 # endif
1449 }
1450
1451
1452 /* Inform X11 that the size of our window has changed.
1453  */
1454 - (void) resize_x11
1455 {
1456   if (!xdpy) return;     // early
1457
1458   NSSize new_size;      // pixels, not points
1459
1460   new_size = self.bounds.size;
1461
1462 #  ifdef USE_IPHONE
1463
1464   // If this hack ignores rotation, then that means that it pretends to
1465   // always be in portrait mode.  If the View has been resized to a 
1466   // landscape shape, swap width and height to keep the backbuffer
1467   // in portrait.
1468   //
1469   double rot = current_device_rotation();
1470   if ([self ignoreRotation] && (rot == 90 || rot == -90)) {
1471     CGFloat swap    = new_size.width;
1472     new_size.width  = new_size.height;
1473     new_size.height = swap;
1474   }
1475
1476   double s = self.hackedContentScaleFactor;
1477   new_size.width *= s;
1478   new_size.height *= s;
1479 #  endif // USE_IPHONE
1480
1481   [self prepareContext];
1482   [self setViewport];
1483
1484   // On first resize, xwindow->frame is 0x0.
1485   if (xwindow->frame.width == new_size.width &&
1486       xwindow->frame.height == new_size.height)
1487     return;
1488
1489 #  if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1490   [ogl_ctx update];
1491 #  endif // BACKBUFFER_OPENGL && !USE_IPHONE
1492
1493   NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
1494   xwindow->frame.x    = 0;
1495   xwindow->frame.y    = 0;
1496   xwindow->frame.width  = new_size.width;
1497   xwindow->frame.height = new_size.height;
1498
1499   [self createBackbuffer:CGSizeMake(xwindow->frame.width,
1500                                     xwindow->frame.height)];
1501
1502 # if defined JWXYZ_QUARTZ
1503   xwindow->cgc = backbuffer;
1504   NSAssert (xwindow->cgc, @"no CGContext");
1505 # elif defined JWXYZ_GL && !defined USE_IPHONE
1506   [ogl_ctx update];
1507   [ogl_ctx setView:xwindow->window.view]; // (Is this necessary?)
1508 # endif // JWXYZ_GL && USE_IPHONE
1509
1510   jwxyz_window_resized (xdpy);
1511
1512 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1513   NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
1514 # endif
1515
1516   // Next time render_x11 is called, run the saver's reshape_cb.
1517   resized_p = YES;
1518 }
1519
1520
1521 #ifdef USE_IPHONE
1522
1523 /* Called by SaverRunner when the device has changed orientation.
1524    That means we need to generate a resize event, even if the size
1525    has not changed (e.g., from LandscapeLeft to LandscapeRight).
1526  */
1527 - (void) orientationChanged
1528 {
1529   [self setViewport];
1530   resized_p = YES;
1531   next_frame_time = 0;  // Get a new frame on screen quickly
1532 }
1533
1534 /* A hook run after the 'reshape_' method has been called.  Used by
1535   XScreenSaverGLView to adjust the in-scene GL viewport.
1536  */
1537 - (void) postReshape
1538 {
1539 }
1540 #endif // USE_IPHONE
1541
1542
1543 // Only render_x11 should call this.  XScreenSaverGLView specializes it.
1544 - (void) reshape_x11
1545 {
1546   xsft->reshape_cb (xdpy, xwindow, xdata,
1547                     xwindow->frame.width, xwindow->frame.height);
1548 }
1549
1550 - (void) render_x11
1551 {
1552 # ifdef USE_IPHONE
1553   @try {
1554 # endif
1555
1556   // jwxyz_make_display needs this.
1557   [self prepareContext]; // resize_x11 also calls this.
1558
1559   if (!initted_p) {
1560
1561     if (! xdpy) {
1562 # ifdef JWXYZ_QUARTZ
1563       xwindow->cgc = backbuffer;
1564 # endif // JWXYZ_QUARTZ
1565       xdpy = jwxyz_make_display (xwindow);
1566
1567 # if defined USE_IPHONE
1568       /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
1569       _ignoreRotation =
1570 #  ifdef JWXYZ_GL
1571         TRUE; // Rotation doesn't work yet. TODO: Make rotation work.
1572 #  else  // !JWXYZ_GL
1573         get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
1574 #  endif // !JWXYZ_GL
1575 # endif // USE_IPHONE
1576
1577       [self resize_x11];
1578     }
1579
1580     if (!setup_p) {
1581       setup_p = YES;
1582       if (xsft->setup_cb)
1583         xsft->setup_cb (xsft, xsft->setup_arg);
1584     }
1585     initted_p = YES;
1586     resized_p = NO;
1587     NSAssert(!xdata, @"xdata already initialized");
1588
1589
1590 # undef ya_rand_init
1591     ya_rand_init (0);
1592     
1593     XSetWindowBackground (xdpy, xwindow,
1594                           get_pixel_resource (xdpy, 0,
1595                                               "background", "Background"));
1596     XClearWindow (xdpy, xwindow);
1597     
1598 # ifndef USE_IPHONE
1599     [[self window] setAcceptsMouseMovedEvents:YES];
1600 # endif
1601
1602     /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
1603        drawing primitives will run on the GPU instead of the CPU.
1604        It seems like it might make things worse rather than better,
1605        though...  Plus it makes us binary-incompatible with 10.4.
1606
1607 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
1608     [[self window] setPreferredBackingLocation:
1609                      NSWindowBackingLocationVideoMemory];
1610 # endif
1611      */
1612
1613     /* Kludge: even though the init_cb functions are declared to take 2 args,
1614       actually call them with 3, for the benefit of xlockmore_init() and
1615       xlockmore_setup().
1616       */
1617     void *(*init_cb) (Display *, Window, void *) = 
1618       (void *(*) (Display *, Window, void *)) xsft->init_cb;
1619     
1620     xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
1621     // NSAssert(xdata, @"no xdata from init");
1622     if (! xdata) abort();
1623
1624     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
1625       fpst = fps_init (xdpy, xwindow);
1626       if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
1627     } else {
1628       fpst = NULL;
1629       xsft->fps_cb = 0;
1630     }
1631
1632 # ifdef USE_IPHONE
1633     if (current_device_rotation() != 0)   // launched while rotated
1634       resized_p = YES;
1635 # endif
1636
1637     [self checkForUpdates];
1638   }
1639
1640
1641   /* I don't understand why we have to do this *every frame*, but we do,
1642      or else the cursor comes back on.
1643    */
1644 # ifndef USE_IPHONE
1645   if (![self isPreview])
1646     [NSCursor setHiddenUntilMouseMoves:YES];
1647 # endif
1648
1649
1650   if (fpst)
1651     {
1652       /* This is just a guess, but the -fps code wants to know how long
1653          we were sleeping between frames.
1654        */
1655       long usecs = 1000000 * [self animationTimeInterval];
1656       usecs -= 200;  // caller apparently sleeps for slightly less sometimes...
1657       if (usecs < 0) usecs = 0;
1658       fps_slept (fpst, usecs);
1659     }
1660
1661
1662   /* Run any XtAppAddInput and XtAppAddTimeOut callbacks now.
1663      Do this before delaying for next_frame_time to avoid throttling
1664      timers to the hack's frame rate.
1665    */
1666   XtAppProcessEvent (XtDisplayToApplicationContext (xdpy),
1667                      XtIMTimer | XtIMAlternateInput);
1668
1669
1670   /* It turns out that on some systems (possibly only 10.5 and older?)
1671      [ScreenSaverView setAnimationTimeInterval] does nothing.  This means
1672      that we cannot rely on it.
1673
1674      Some of the screen hacks want to delay for long periods, and letting the
1675      framework run the update function at 30 FPS when it really wanted half a
1676      minute between frames would be bad.  So instead, we assume that the
1677      framework's animation timer might fire whenever, but we only invoke the
1678      screen hack's "draw frame" method when enough time has expired.
1679   
1680      This means two extra calls to gettimeofday() per frame.  For fast-cycling
1681      screen savers, that might actually slow them down.  Oh well.
1682
1683      A side-effect of this is that it's not possible for a saver to request
1684      an animation interval that is faster than animationTimeInterval.
1685
1686      HOWEVER!  On modern systems where setAnimationTimeInterval is *not*
1687      ignored, it's important that it be faster than 30 FPS.  240 FPS is good.
1688
1689      An NSTimer won't fire if the timer is already running the invocation
1690      function from a previous firing.  So, if we use a 30 FPS
1691      animationTimeInterval (33333 Âµs) and a screenhack takes 40000 Âµs for a
1692      frame, there will be a 26666 Âµs delay until the next frame, 66666 Âµs
1693      after the beginning of the current frame.  In other words, 25 FPS
1694      becomes 15 FPS.
1695
1696      Frame rates tend to snap to values of 30/N, where N is a positive
1697      integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1698      is rounded down from what it would normally be.
1699
1700      So if we set animationTimeInterval to 1/240 instead of 1/30, frame rates
1701      become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1702      steps for higher or lower animation time intervals respectively.
1703    */
1704   struct timeval tv;
1705   gettimeofday (&tv, 0);
1706   double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1707   if (now < next_frame_time) return;
1708
1709   // [self flushBackbuffer];
1710
1711   if (resized_p) {
1712     // We do this here instead of in setFrame so that all the
1713     // Xlib drawing takes place under the animation timer.
1714
1715 # ifndef USE_IPHONE
1716     if (ogl_ctx)
1717       [ogl_ctx setView:self];
1718 # endif // !USE_IPHONE
1719
1720     [self reshape_x11];
1721     resized_p = NO;
1722   }
1723
1724
1725   // And finally:
1726   //
1727   // NSAssert(xdata, @"no xdata when drawing");
1728   if (! xdata) abort();
1729   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1730   if (fpst && xsft->fps_cb)
1731     xsft->fps_cb (xdpy, xwindow, fpst, xdata);
1732
1733   gettimeofday (&tv, 0);
1734   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1735   next_frame_time = now + (delay / 1000000.0);
1736
1737 # ifdef JWXYZ_QUARTZ
1738   [self drawBackbuffer];
1739 # endif
1740   // This can also happen near the beginning of render_x11.
1741   [self flushBackbuffer];
1742
1743 # ifdef USE_IPHONE      // Allow savers on the iPhone to run full-tilt.
1744   if (delay < [self animationTimeInterval])
1745     [self setAnimationTimeInterval:(delay / 1000000.0)];
1746 # endif
1747
1748 # ifdef DO_GC_HACKERY
1749   /* Current theory is that the 10.6 garbage collector sucks in the
1750      following way:
1751
1752      It only does a collection when a threshold of outstanding
1753      collectable allocations has been surpassed.  However, CoreGraphics
1754      creates lots of small collectable allocations that contain pointers
1755      to very large non-collectable allocations: a small CG object that's
1756      collectable referencing large malloc'd allocations (non-collectable)
1757      containing bitmap data.  So the large allocation doesn't get freed
1758      until GC collects the small allocation, which triggers its finalizer
1759      to run which frees the large allocation.  So GC is deciding that it
1760      doesn't really need to run, even though the process has gotten
1761      enormous.  GC eventually runs once pageouts have happened, but by
1762      then it's too late, and the machine's resident set has been
1763      sodomized.
1764
1765      So, we force an exhaustive garbage collection in this process
1766      approximately every 5 seconds whether the system thinks it needs 
1767      one or not.
1768   */
1769   {
1770     static int tick = 0;
1771     if (++tick > 5*30) {
1772       tick = 0;
1773       objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1774     }
1775   }
1776 # endif // DO_GC_HACKERY
1777
1778 # ifdef USE_IPHONE
1779   }
1780   @catch (NSException *e) {
1781     [self handleException: e];
1782   }
1783 # endif // USE_IPHONE
1784 }
1785
1786
1787 - (void) animateOneFrame
1788 {
1789   // Render X11 into the backing store bitmap...
1790
1791 # ifdef JWXYZ_QUARTZ
1792   NSAssert (backbuffer, @"no back buffer");
1793
1794 #  ifdef USE_IPHONE
1795   UIGraphicsPushContext (backbuffer);
1796 #  endif
1797 # endif // JWXYZ_QUARTZ
1798
1799   [self render_x11];
1800
1801 # if defined USE_IPHONE && defined JWXYZ_QUARTZ
1802   UIGraphicsPopContext();
1803 # endif
1804 }
1805
1806
1807 # ifndef USE_IPHONE  // Doesn't exist on iOS
1808
1809 - (void) setFrame:(NSRect) newRect
1810 {
1811   [super setFrame:newRect];
1812
1813   if (xwindow)     // inform Xlib that the window has changed now.
1814     [self resize_x11];
1815 }
1816
1817 - (void) setFrameSize:(NSSize) newSize
1818 {
1819   [super setFrameSize:newSize];
1820   if (xwindow)
1821     [self resize_x11];
1822 }
1823
1824 # else // USE_IPHONE
1825
1826 - (void) layoutSubviews
1827 {
1828   [super layoutSubviews];
1829   [self resizeGL];
1830   if (xwindow)
1831     [self resize_x11];
1832 }
1833
1834 # endif
1835
1836
1837 +(BOOL) performGammaFade
1838 {
1839   return YES;
1840 }
1841
1842 - (BOOL) hasConfigureSheet
1843 {
1844   return YES;
1845 }
1846
1847 + (NSString *) decompressXML: (NSData *)data
1848 {
1849   if (! data) return 0;
1850   BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1851
1852   // If it's not already XML, decompress it.
1853   NSAssert (compressed_p, @"xml isn't compressed");
1854   if (compressed_p) {
1855     NSMutableData *data2 = 0;
1856     int ret = -1;
1857     z_stream zs;
1858     memset (&zs, 0, sizeof(zs));
1859     ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1860     if (ret == Z_OK) {
1861       UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1862       data2 = [NSMutableData dataWithLength: usize];
1863       zs.next_in   = (Bytef *) data.bytes;
1864       zs.avail_in  = (uint) data.length;
1865       zs.next_out  = (Bytef *) data2.bytes;
1866       zs.avail_out = (uint) data2.length;
1867       ret = inflate (&zs, Z_FINISH);
1868       inflateEnd (&zs);
1869     }
1870     if (ret == Z_OK || ret == Z_STREAM_END)
1871       data = data2;
1872     else
1873       NSAssert2 (0, @"gunzip error: %d: %s",
1874                  ret, (zs.msg ? zs.msg : "<null>"));
1875   }
1876
1877   NSString *s = [[NSString alloc]
1878                   initWithData:data encoding:NSUTF8StringEncoding];
1879   [s autorelease];
1880   return s;
1881 }
1882
1883
1884 #ifndef USE_IPHONE
1885 - (NSWindow *) configureSheet
1886 #else
1887 - (UIViewController *) configureView
1888 #endif
1889 {
1890   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1891   NSString *file = [NSString stringWithCString:xsft->progclass
1892                                       encoding:NSISOLatin1StringEncoding];
1893   file = [file lowercaseString];
1894   NSString *path = [bundle pathForResource:file ofType:@"xml"];
1895   if (!path) {
1896     NSLog (@"%@.xml does not exist in the application bundle: %@/",
1897            file, [bundle resourcePath]);
1898     return nil;
1899   }
1900   
1901 # ifdef USE_IPHONE
1902   UIViewController *sheet;
1903 # else  // !USE_IPHONE
1904   NSWindow *sheet;
1905 # endif // !USE_IPHONE
1906
1907   NSData *xmld = [NSData dataWithContentsOfFile:path];
1908   NSString *xml = [[self class] decompressXML: xmld];
1909   sheet = [[XScreenSaverConfigSheet alloc]
1910             initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1911                 options:xsft->options
1912              controller:[prefsReader userDefaultsController]
1913        globalController:[prefsReader globalDefaultsController]
1914                defaults:[prefsReader defaultOptions]];
1915
1916   // #### am I expected to retain this, or not? wtf.
1917   //      I thought not, but if I don't do this, we (sometimes) crash.
1918   // #### Analyze says "potential leak of an object stored into sheet"
1919   // [sheet retain];
1920
1921   return sheet;
1922 }
1923
1924
1925 - (NSUserDefaultsController *) userDefaultsController
1926 {
1927   return [prefsReader userDefaultsController];
1928 }
1929
1930
1931 /* Announce our willingness to accept keyboard input.
1932  */
1933 - (BOOL)acceptsFirstResponder
1934 {
1935   return YES;
1936 }
1937
1938
1939 - (void) beep
1940 {
1941 # ifndef USE_IPHONE
1942   NSBeep();
1943 # else // USE_IPHONE 
1944
1945   // There's no way to play a standard system alert sound!
1946   // We'd have to include our own WAV for that.
1947   //
1948   // Or we could vibrate:
1949   // #import <AudioToolbox/AudioToolbox.h>
1950   // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1951   //
1952   // Instead, just flash the screen white, then fade.
1953   //
1954   UIView *v = [[UIView alloc] initWithFrame: [self frame]]; 
1955   [v setBackgroundColor: [UIColor whiteColor]];
1956   [[self window] addSubview:v];
1957   [UIView animateWithDuration: 0.1
1958           animations:^{ [v setAlpha: 0.0]; }
1959           completion:^(BOOL finished) { [v removeFromSuperview]; } ];
1960
1961 # endif  // USE_IPHONE
1962 }
1963
1964
1965 /* Send an XEvent to the hack.  Returns YES if it was handled.
1966  */
1967 - (BOOL) sendEvent: (XEvent *) e
1968 {
1969   if (!initted_p || ![self isAnimating]) // no event handling unless running.
1970     return NO;
1971
1972   [self lockFocus];
1973   [self prepareContext];
1974   BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
1975   [self unlockFocus];
1976   return result;
1977 }
1978
1979
1980 #ifndef USE_IPHONE
1981
1982 /* Convert an NSEvent into an XEvent, and pass it along.
1983    Returns YES if it was handled.
1984  */
1985 - (BOOL) convertEvent: (NSEvent *) e
1986             type: (int) type
1987 {
1988   XEvent xe;
1989   memset (&xe, 0, sizeof(xe));
1990   
1991   int state = 0;
1992   
1993   int flags = [e modifierFlags];
1994   if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1995   if (flags & NSShiftKeyMask)      state |= ShiftMask;
1996   if (flags & NSControlKeyMask)    state |= ControlMask;
1997   if (flags & NSAlternateKeyMask)  state |= Mod1Mask;
1998   if (flags & NSCommandKeyMask)    state |= Mod2Mask;
1999   
2000   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
2001                                             toView:self];
2002 # ifdef USE_IPHONE
2003   double s = [self hackedContentScaleFactor];
2004 # else
2005   int s = 1;
2006 # endif
2007   int x = s * p.x;
2008   int y = s * ([self bounds].size.height - p.y);
2009
2010   xe.xany.type = type;
2011   switch (type) {
2012     case ButtonPress:
2013     case ButtonRelease:
2014       xe.xbutton.x = x;
2015       xe.xbutton.y = y;
2016       xe.xbutton.state = state;
2017       if ([e type] == NSScrollWheel)
2018         xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
2019                              [e deltaY] < 0 ? Button5 :
2020                              [e deltaX] > 0 ? Button6 :
2021                              [e deltaX] < 0 ? Button7 :
2022                              0);
2023       else
2024         xe.xbutton.button = [e buttonNumber] + 1;
2025       break;
2026     case MotionNotify:
2027       xe.xmotion.x = x;
2028       xe.xmotion.y = y;
2029       xe.xmotion.state = state;
2030       break;
2031     case KeyPress:
2032     case KeyRelease:
2033       {
2034         NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
2035                         [e charactersIgnoringModifiers]);
2036         KeySym k = 0;
2037
2038         if (!ns || [ns length] == 0)                    // dead key
2039           {
2040             // Cocoa hides the difference between left and right keys.
2041             // Also we only get KeyPress events for these, no KeyRelease
2042             // (unless we hack the mod state manually.  Bleh.)
2043             //
2044             if      (flags & NSAlphaShiftKeyMask)   k = XK_Caps_Lock;
2045             else if (flags & NSShiftKeyMask)        k = XK_Shift_L;
2046             else if (flags & NSControlKeyMask)      k = XK_Control_L;
2047             else if (flags & NSAlternateKeyMask)    k = XK_Alt_L;
2048             else if (flags & NSCommandKeyMask)      k = XK_Meta_L;
2049           }
2050         else if ([ns length] == 1)                      // real key
2051           {
2052             switch ([ns characterAtIndex:0]) {
2053             case NSLeftArrowFunctionKey:  k = XK_Left;      break;
2054             case NSRightArrowFunctionKey: k = XK_Right;     break;
2055             case NSUpArrowFunctionKey:    k = XK_Up;        break;
2056             case NSDownArrowFunctionKey:  k = XK_Down;      break;
2057             case NSPageUpFunctionKey:     k = XK_Page_Up;   break;
2058             case NSPageDownFunctionKey:   k = XK_Page_Down; break;
2059             case NSHomeFunctionKey:       k = XK_Home;      break;
2060             case NSPrevFunctionKey:       k = XK_Prior;     break;
2061             case NSNextFunctionKey:       k = XK_Next;      break;
2062             case NSBeginFunctionKey:      k = XK_Begin;     break;
2063             case NSEndFunctionKey:        k = XK_End;       break;
2064             case NSF1FunctionKey:         k = XK_F1;        break;
2065             case NSF2FunctionKey:         k = XK_F2;        break;
2066             case NSF3FunctionKey:         k = XK_F3;        break;
2067             case NSF4FunctionKey:         k = XK_F4;        break;
2068             case NSF5FunctionKey:         k = XK_F5;        break;
2069             case NSF6FunctionKey:         k = XK_F6;        break;
2070             case NSF7FunctionKey:         k = XK_F7;        break;
2071             case NSF8FunctionKey:         k = XK_F8;        break;
2072             case NSF9FunctionKey:         k = XK_F9;        break;
2073             case NSF10FunctionKey:        k = XK_F10;       break;
2074             case NSF11FunctionKey:        k = XK_F11;       break;
2075             case NSF12FunctionKey:        k = XK_F12;       break;
2076             default:
2077               {
2078                 const char *s =
2079                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
2080                 k = (s && *s ? *s : 0);
2081               }
2082               break;
2083             }
2084           }
2085
2086         if (! k) return YES;   // E.g., "KeyRelease XK_Shift_L"
2087
2088         xe.xkey.keycode = k;
2089         xe.xkey.state = state;
2090         break;
2091       }
2092     default:
2093       NSAssert1 (0, @"unknown X11 event type: %d", type);
2094       break;
2095   }
2096
2097   return [self sendEvent: &xe];
2098 }
2099
2100
2101 - (void) mouseDown: (NSEvent *) e
2102 {
2103   if (! [self convertEvent:e type:ButtonPress])
2104     [super mouseDown:e];
2105 }
2106
2107 - (void) mouseUp: (NSEvent *) e
2108 {
2109   if (! [self convertEvent:e type:ButtonRelease])
2110     [super mouseUp:e];
2111 }
2112
2113 - (void) otherMouseDown: (NSEvent *) e
2114 {
2115   if (! [self convertEvent:e type:ButtonPress])
2116     [super otherMouseDown:e];
2117 }
2118
2119 - (void) otherMouseUp: (NSEvent *) e
2120 {
2121   if (! [self convertEvent:e type:ButtonRelease])
2122     [super otherMouseUp:e];
2123 }
2124
2125 - (void) mouseMoved: (NSEvent *) e
2126 {
2127   if (! [self convertEvent:e type:MotionNotify])
2128     [super mouseMoved:e];
2129 }
2130
2131 - (void) mouseDragged: (NSEvent *) e
2132 {
2133   if (! [self convertEvent:e type:MotionNotify])
2134     [super mouseDragged:e];
2135 }
2136
2137 - (void) otherMouseDragged: (NSEvent *) e
2138 {
2139   if (! [self convertEvent:e type:MotionNotify])
2140     [super otherMouseDragged:e];
2141 }
2142
2143 - (void) scrollWheel: (NSEvent *) e
2144 {
2145   if (! [self convertEvent:e type:ButtonPress])
2146     [super scrollWheel:e];
2147 }
2148
2149 - (void) keyDown: (NSEvent *) e
2150 {
2151   if (! [self convertEvent:e type:KeyPress])
2152     [super keyDown:e];
2153 }
2154
2155 - (void) keyUp: (NSEvent *) e
2156 {
2157   if (! [self convertEvent:e type:KeyRelease])
2158     [super keyUp:e];
2159 }
2160
2161 - (void) flagsChanged: (NSEvent *) e
2162 {
2163   if (! [self convertEvent:e type:KeyPress])
2164     [super flagsChanged:e];
2165 }
2166
2167
2168 - (NSOpenGLPixelFormat *) getGLPixelFormat
2169 {
2170   NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat");
2171
2172   NSOpenGLPixelFormatAttribute attrs[40];
2173   int i = 0;
2174   attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
2175
2176 /* OpenGL's core profile removes a lot of the same stuff that was removed in
2177    OpenGL ES (e.g. glBegin, glDrawPixels), so it might be a possibility.
2178
2179   opengl_core_p = True;
2180   if (opengl_core_p) {
2181     attrs[i++] = NSOpenGLPFAOpenGLProfile;
2182     attrs[i++] = NSOpenGLProfileVersion3_2Core;
2183   }
2184  */
2185
2186 /* Eventually: multisampled pixmaps. May not be supported everywhere.
2187    if (multi_sample_p) {
2188      attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 1;
2189      attrs[i++] = NSOpenGLPFASamples;       attrs[i++] = 6;
2190    }
2191  */
2192
2193 # ifdef JWXYZ_QUARTZ
2194   // Under Quartz, we're just blitting a texture.
2195   if (double_buffered_p)
2196     attrs[i++] = NSOpenGLPFADoubleBuffer;
2197 # endif
2198
2199 # ifdef JWXYZ_GL
2200   /* Under OpenGL, all sorts of drawing commands are being issued, and it might
2201      be a performance problem if this activity occurs on the front buffer.
2202      Also, some screenhacks expect OS X/iOS to always double-buffer.
2203      NSOpenGLPFABackingStore prevents flickering with screenhacks that
2204      don't redraw the entire screen every frame.
2205    */
2206   attrs[i++] = NSOpenGLPFADoubleBuffer;
2207   attrs[i++] = NSOpenGLPFABackingStore;
2208 # endif
2209
2210   attrs[i++] = NSOpenGLPFAWindow;
2211 # ifdef JWXYZ_GL
2212   attrs[i++] = NSOpenGLPFAPixelBuffer;
2213   /* ...But not NSOpenGLPFAFullScreen, because that would be for
2214      [NSOpenGLContext setFullScreen].
2215    */
2216 # endif
2217
2218   /* NSOpenGLPFAFullScreen would go here if initWithFrame's isPreview == NO.
2219    */
2220
2221   attrs[i] = 0;
2222
2223   NSOpenGLPixelFormat *p = [[NSOpenGLPixelFormat alloc]
2224                              initWithAttributes:attrs];
2225   [p autorelease];
2226   return p;
2227 }
2228
2229 #else  // USE_IPHONE
2230
2231
2232 - (void) stopAndClose:(Bool)relaunch_p
2233 {
2234   if ([self isAnimating])
2235     [self stopAnimation];
2236
2237   /* Need to make the SaverListController be the firstResponder again
2238      so that it can continue to receive its own shake events.  I
2239      suppose that this abstraction-breakage means that I'm adding
2240      XScreenSaverView to the UINavigationController wrong...
2241    */
2242 //  UIViewController *v = [[self window] rootViewController];
2243 //  if ([v isKindOfClass: [UINavigationController class]]) {
2244 //    UINavigationController *n = (UINavigationController *) v;
2245 //    [[n topViewController] becomeFirstResponder];
2246 //  }
2247   [self resignFirstResponder];
2248
2249   if (relaunch_p) {   // Fake a shake on the SaverListController.
2250     [_delegate didShake:self];
2251   } else {      // Not launching another, animate our return to the list.
2252 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
2253     NSLog (@"fading back to saver list");
2254 # endif
2255     [_delegate wantsFadeOut:self];
2256   }
2257 }
2258
2259
2260 /* We distinguish between taps and drags.
2261
2262    - Drags/pans (down, motion, up) are sent to the saver to handle.
2263    - Single-taps exit the saver.
2264    - Double-taps are sent to the saver as a "Space" keypress.
2265    - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
2266
2267    This means a saver cannot respond to a single-tap.  Only a few try to.
2268  */
2269
2270 - (void)initGestures
2271 {
2272   UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
2273                                    initWithTarget:self
2274                                    action:@selector(handleDoubleTap)];
2275   dtap.numberOfTapsRequired = 2;
2276   dtap.numberOfTouchesRequired = 1;
2277
2278   UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
2279                                    initWithTarget:self
2280                                    action:@selector(handleTap)];
2281   stap.numberOfTapsRequired = 1;
2282   stap.numberOfTouchesRequired = 1;
2283  
2284   UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
2285                                   initWithTarget:self
2286                                   action:@selector(handlePan:)];
2287   pan.maximumNumberOfTouches = 1;
2288   pan.minimumNumberOfTouches = 1;
2289  
2290   // I couldn't get Swipe to work, but using a second Pan recognizer works.
2291   UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
2292                                    initWithTarget:self
2293                                    action:@selector(handlePan2:)];
2294   pan2.maximumNumberOfTouches = 2;
2295   pan2.minimumNumberOfTouches = 2;
2296
2297   // Also handle long-touch, and treat that the same as Pan.
2298   // Without this, panning doesn't start until there's motion, so the trick
2299   // of holding down your finger to freeze the scene doesn't work.
2300   //
2301   UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
2302                                          initWithTarget:self
2303                                          action:@selector(handleLongPress:)];
2304   hold.numberOfTapsRequired = 0;
2305   hold.numberOfTouchesRequired = 1;
2306   hold.minimumPressDuration = 0.25;   /* 1/4th second */
2307
2308   [stap requireGestureRecognizerToFail: dtap];
2309   [stap requireGestureRecognizerToFail: hold];
2310   [dtap requireGestureRecognizerToFail: hold];
2311   [pan  requireGestureRecognizerToFail: hold];
2312
2313   [self setMultipleTouchEnabled:YES];
2314
2315   [self addGestureRecognizer: dtap];
2316   [self addGestureRecognizer: stap];
2317   [self addGestureRecognizer: pan];
2318   [self addGestureRecognizer: pan2];
2319   [self addGestureRecognizer: hold];
2320
2321   [dtap release];
2322   [stap release];
2323   [pan  release];
2324   [pan2 release];
2325   [hold release];
2326 }
2327
2328
2329 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
2330    convert it to what X11 and OpenGL expect.
2331
2332    Getting this crap right is tricky, given the confusion of the various
2333    scale factors, so here's a checklist that I think covers all of the X11
2334    and OpenGL cases. For each of these: rotate to all 4 orientations;
2335    ensure the mouse tracks properly to all 4 corners.
2336
2337    Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator.
2338
2339    Test hacks must cover:
2340      X11 ignoreRotation = true
2341      X11 ignoreRotation = false
2342      OpenGL (rotation is handled manually, so they never ignoreRotation)
2343
2344    Test devices must cover:
2345      contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2)
2346      contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air)
2347      contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+)
2348
2349      iPad 2:    768x1024 / 1 = 768x1024
2350      iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical)
2351      iPhone 4s:  640x960 / 2 = 320x480
2352      iPhone 5:  640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical)
2353      iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3)
2354    
2355    Tests:
2356                       iPad2 iPadAir iPhone4s iPhone5 iPhone6+
2357      Attraction X  yes  -       -       -       -       Y
2358      Fireworkx  X  no   -       -       -       -       Y
2359      Carousel   GL yes  -       -       -       -       Y
2360      Voronoi    GL no   -       -       -       -       -
2361  */
2362 - (void) convertMouse:(CGPoint *)p
2363 {
2364   CGFloat xx = p->x, yy = p->y;
2365
2366 # if TARGET_IPHONE_SIMULATOR
2367   {
2368     XWindowAttributes xgwa;
2369     XGetWindowAttributes (xdpy, xwindow, &xgwa);
2370     NSLog (@"TOUCH %4g, %-4g in %4d x %-4d  cs=%.0f hcs=%.0f r=%d ig=%d\n",
2371            p->x, p->y,
2372            xgwa.width, xgwa.height,
2373            [self contentScaleFactor],
2374            [self hackedContentScaleFactor],
2375            [self rotateTouches], [self ignoreRotation]);
2376   }
2377 # endif // TARGET_IPHONE_SIMULATOR
2378
2379   if ([self rotateTouches]) {
2380
2381     // The XScreenSaverGLView case:
2382     // The X11 window is rotated, as is the framebuffer.
2383     // The device coordinates match the framebuffer dimensions,
2384     // but might have axes swapped... and we need to swap them
2385     // by ratios.
2386     //
2387     int w = [self frame].size.width;
2388     int h = [self frame].size.height;
2389     GLfloat xr = (GLfloat) xx / w;
2390     GLfloat yr = (GLfloat) yy / h;
2391     GLfloat swap;
2392     int o = (int) current_device_rotation();
2393     switch (o) {
2394     case -90: case  270: swap = xr; xr = 1-yr; yr = swap;   break;
2395     case  90: case -270: swap = xr; xr = yr;   yr = 1-swap; break;
2396     case 180: case -180:            xr = 1-xr; yr = 1-yr;   break;
2397     default: break;
2398     }
2399     xx = xr * w;
2400     yy = yr * h;
2401
2402   } else if ([self ignoreRotation]) {
2403
2404     // The X11 case, where the hack has opted not to rotate:
2405     // The X11 window is unrotated, but the framebuffer is rotated.
2406     // The device coordinates match the framebuffer, so they need to
2407     // be de-rotated to match the X11 window.
2408     //
2409     int w = [self frame].size.width;
2410     int h = [self frame].size.height;
2411     int swap;
2412     int o = (int) current_device_rotation();
2413     switch (o) {
2414     case -90: case  270: swap = xx; xx = h-yy; yy = swap;   break;
2415     case  90: case -270: swap = xx; xx = yy;   yy = w-swap; break;
2416     case 180: case -180:            xx = w-xx; yy = h-yy;   break;
2417     default: break;
2418     }
2419   }
2420
2421   double s = [self hackedContentScaleFactor];
2422   p->x = xx * s;
2423   p->y = yy * s;
2424
2425 # if TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
2426   {
2427     XWindowAttributes xgwa;
2428     XGetWindowAttributes (xdpy, xwindow, &xgwa);
2429     NSLog (@"touch %4g, %-4g in %4d x %-4d  cs=%.0f hcs=%.0f r=%d ig=%d\n",
2430            p->x, p->y,
2431            xgwa.width, xgwa.height,
2432            [self contentScaleFactor],
2433            [self hackedContentScaleFactor],
2434            [self rotateTouches], [self ignoreRotation]);
2435     if (p->x < 0 || p->y < 0 || p->x > xgwa.width || p->y > xgwa.height)
2436       abort();
2437   }
2438 # endif // TARGET_IPHONE_SIMULATOR
2439 }
2440
2441
2442 /* Single click exits saver.
2443  */
2444 - (void) handleTap
2445 {
2446   [self stopAndClose:NO];
2447 }
2448
2449
2450 /* Double click sends Space KeyPress.
2451  */
2452 - (void) handleDoubleTap
2453 {
2454   if (!xsft->event_cb || !xwindow) return;
2455
2456   XEvent xe;
2457   memset (&xe, 0, sizeof(xe));
2458   xe.xkey.keycode = ' ';
2459   xe.xany.type = KeyPress;
2460   BOOL ok1 = [self sendEvent: &xe];
2461   xe.xany.type = KeyRelease;
2462   BOOL ok2 = [self sendEvent: &xe];
2463   if (!(ok1 || ok2))
2464     [self beep];
2465 }
2466
2467
2468 /* Drag with one finger down: send MotionNotify.
2469  */
2470 - (void) handlePan:(UIGestureRecognizer *)sender
2471 {
2472   if (!xsft->event_cb || !xwindow) return;
2473
2474   XEvent xe;
2475   memset (&xe, 0, sizeof(xe));
2476
2477   CGPoint p = [sender locationInView:self];  // this is in points, not pixels
2478   [self convertMouse:&p];
2479   NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
2480   xwindow->window.last_mouse_x = p.x;
2481   xwindow->window.last_mouse_y = p.y;
2482
2483   switch (sender.state) {
2484   case UIGestureRecognizerStateBegan:
2485     xe.xany.type = ButtonPress;
2486     xe.xbutton.button = 1;
2487     xe.xbutton.x = p.x;
2488     xe.xbutton.y = p.y;
2489     break;
2490
2491   case UIGestureRecognizerStateEnded:
2492     xe.xany.type = ButtonRelease;
2493     xe.xbutton.button = 1;
2494     xe.xbutton.x = p.x;
2495     xe.xbutton.y = p.y;
2496     break;
2497
2498   case UIGestureRecognizerStateChanged:
2499     xe.xany.type = MotionNotify;
2500     xe.xmotion.x = p.x;
2501     xe.xmotion.y = p.y;
2502     break;
2503
2504   default:
2505     break;
2506   }
2507
2508   BOOL ok = [self sendEvent: &xe];
2509   if (!ok && xe.xany.type == ButtonRelease)
2510     [self beep];
2511 }
2512
2513
2514 /* Hold one finger down: assume we're about to start dragging.
2515    Treat the same as Pan.
2516  */
2517 - (void) handleLongPress:(UIGestureRecognizer *)sender
2518 {
2519   [self handlePan:sender];
2520 }
2521
2522
2523
2524 /* Drag with 2 fingers down: send arrow keys.
2525  */
2526 - (void) handlePan2:(UIPanGestureRecognizer *)sender
2527 {
2528   if (!xsft->event_cb || !xwindow) return;
2529
2530   if (sender.state != UIGestureRecognizerStateEnded)
2531     return;
2532
2533   XEvent xe;
2534   memset (&xe, 0, sizeof(xe));
2535
2536   CGPoint p = [sender locationInView:self];  // this is in points, not pixels
2537   [self convertMouse:&p];
2538
2539   if (fabs(p.x) > fabs(p.y))
2540     xe.xkey.keycode = (p.x > 0 ? XK_Right : XK_Left);
2541   else
2542     xe.xkey.keycode = (p.y > 0 ? XK_Down : XK_Up);
2543
2544   BOOL ok1 = [self sendEvent: &xe];
2545   xe.xany.type = KeyRelease;
2546   BOOL ok2 = [self sendEvent: &xe];
2547   if (!(ok1 || ok2))
2548     [self beep];
2549 }
2550
2551
2552 /* We need this to respond to "shake" gestures
2553  */
2554 - (BOOL)canBecomeFirstResponder
2555 {
2556   return YES;
2557 }
2558
2559 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
2560 {
2561 }
2562
2563
2564 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
2565 {
2566 }
2567
2568 /* Shake means exit and launch a new saver.
2569  */
2570 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
2571 {
2572   [self stopAndClose:YES];
2573 }
2574
2575
2576 - (void)setScreenLocked:(BOOL)locked
2577 {
2578   if (screenLocked == locked) return;
2579   screenLocked = locked;
2580   if (locked) {
2581     if ([self isAnimating])
2582       [self stopAnimation];
2583   } else {
2584     if (! [self isAnimating])
2585       [self startAnimation];
2586   }
2587 }
2588
2589 - (NSDictionary *)getGLProperties
2590 {
2591   return [NSDictionary dictionaryWithObjectsAndKeys:
2592           kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
2593 # ifdef JWXYZ_GL
2594           /* This could be disabled if we knew the screen would be redrawn
2595              entirely for every frame.
2596            */
2597           [NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
2598 # endif // JWXYZ_GL
2599           nil];
2600 }
2601
2602 - (void)addExtraRenderbuffers:(CGSize)size
2603 {
2604   // No extra renderbuffers are needed for 2D screenhacks.
2605 }
2606  
2607
2608 - (NSString *)getCAGravity
2609 {
2610   return kCAGravityCenter;  // Looks better in e.g. Compass.
2611 //  return kCAGravityBottomLeft;
2612 }
2613
2614 #endif // USE_IPHONE
2615
2616
2617 - (void) checkForUpdates
2618 {
2619 # ifndef USE_IPHONE
2620   // We only check once at startup, even if there are multiple screens,
2621   // and even if this saver is running for many days.
2622   // (Uh, except this doesn't work because this static isn't shared,
2623   // even if we make it an exported global. Not sure why. Oh well.)
2624   static BOOL checked_p = NO;
2625   if (checked_p) return;
2626   checked_p = YES;
2627
2628   // If it's off, don't bother running the updater.  Otherwise, the
2629   // updater will decide if it's time to hit the network.
2630   if (! get_boolean_resource (xdpy,
2631                               SUSUEnableAutomaticChecksKey,
2632                               SUSUEnableAutomaticChecksKey))
2633     return;
2634
2635   NSString *updater = @"XScreenSaverUpdater.app";
2636
2637   // There may be multiple copies of the updater: e.g., one in /Applications
2638   // and one in the mounted installer DMG!  It's important that we run the
2639   // one from the disk and not the DMG, so search for the right one.
2640   //
2641   NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2642   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2643   NSArray *search =
2644     @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2645       [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2646       @"/Library/Screen Savers",
2647       @"/System/Library/Screen Savers",
2648       @"/Applications",
2649       @"/Applications/Utilities"];
2650   NSString *app_path = nil;
2651   for (NSString *dir in search) {
2652     NSString *p = [dir stringByAppendingPathComponent:updater];
2653     if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2654       app_path = p;
2655       break;
2656     }
2657   }
2658
2659   if (! app_path)
2660     app_path = [workspace fullPathForApplication:updater];
2661
2662   if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
2663     app_path = 0;  // The DMG version will not do.
2664
2665   if (!app_path) {
2666     NSLog(@"Unable to find %@", updater);
2667     return;
2668   }
2669
2670   NSError *err = nil;
2671   if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2672                    options:(NSWorkspaceLaunchWithoutAddingToRecents |
2673                             NSWorkspaceLaunchWithoutActivation |
2674                             NSWorkspaceLaunchAndHide)
2675                    configuration:[NSMutableDictionary dictionary]
2676                    error:&err]) {
2677     NSLog(@"Unable to launch %@: %@", app_path, err);
2678   }
2679
2680 # endif // !USE_IPHONE
2681 }
2682
2683
2684 @end
2685
2686 /* Utility functions...
2687  */
2688
2689 static PrefsReader *
2690 get_prefsReader (Display *dpy)
2691 {
2692   XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2693   if (!view) return 0;
2694   return [view prefsReader];
2695 }
2696
2697
2698 char *
2699 get_string_resource (Display *dpy, char *name, char *class)
2700 {
2701   return [get_prefsReader(dpy) getStringResource:name];
2702 }
2703
2704 Bool
2705 get_boolean_resource (Display *dpy, char *name, char *class)
2706 {
2707   return [get_prefsReader(dpy) getBooleanResource:name];
2708 }
2709
2710 int
2711 get_integer_resource (Display *dpy, char *name, char *class)
2712 {
2713   return [get_prefsReader(dpy) getIntegerResource:name];
2714 }
2715
2716 double
2717 get_float_resource (Display *dpy, char *name, char *class)
2718 {
2719   return [get_prefsReader(dpy) getFloatResource:name];
2720 }