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