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