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