1 /* xscreensaver, Copyright (c) 2006-2017 Jamie Zawinski <jwz@jwz.org>
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
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.
18 #import <QuartzCore/QuartzCore.h>
21 #import "XScreenSaverView.h"
22 #import "XScreenSaverConfigSheet.h"
24 #import "screenhackI.h"
25 #import "xlockmoreI.h"
28 #import "jwxyz-cocoa.h"
29 #import "jwxyz-timers.h"
32 // XScreenSaverView.m speaks OpenGL ES just fine, but enableBackbuffer does
33 // need (jwzgles_)gluCheckExtension.
36 # import <OpenGL/glu.h>
39 /* Garbage collection only exists if we are being compiled against the
40 10.6 SDK or newer, not if we are building against the 10.4 SDK.
42 #ifndef MAC_OS_X_VERSION_10_6
43 # define MAC_OS_X_VERSION_10_6 1060 /* undefined in 10.4 SDK, grr */
45 #ifndef MAC_OS_X_VERSION_10_12
46 # define MAC_OS_X_VERSION_10_12 101200
48 #if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 && \
49 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12)
50 /* 10.6 SDK or later, and earlier than 10.12 SDK */
51 # import <objc/objc-auto.h>
52 # define DO_GC_HACKERY
55 /* Duplicated in xlockmoreI.h and XScreenSaverGLView.m. */
56 extern void clear_gl_error (void);
57 extern void check_gl_error (const char *type);
59 extern struct xscreensaver_function_table *xscreensaver_function_table;
61 /* Global variables used by the screen savers
64 const char *progclass;
70 # define NSSizeToCGSize(x) (x)
72 extern NSDictionary *make_function_table_dict(void); // ios-function-table.m
74 /* Stub definition of the superclass, for iPhone.
76 @implementation ScreenSaverView
78 NSTimeInterval anim_interval;
83 - (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
84 self = [super initWithFrame:frame];
86 anim_interval = 1.0/30;
89 - (NSTimeInterval)animationTimeInterval { return anim_interval; }
90 - (void)setAnimationTimeInterval:(NSTimeInterval)i { anim_interval = i; }
91 - (BOOL)hasConfigureSheet { return NO; }
92 - (NSWindow *)configureSheet { return nil; }
93 - (NSView *)configureView { return nil; }
94 - (BOOL)isPreview { return NO; }
95 - (BOOL)isAnimating { return animating_p; }
96 - (void)animateOneFrame { }
98 - (void)startAnimation {
99 if (animating_p) return;
101 anim_timer = [NSTimer scheduledTimerWithTimeInterval: anim_interval
103 selector:@selector(animateOneFrame)
108 - (void)stopAnimation {
110 [anim_timer invalidate];
117 # endif // !USE_IPHONE
121 @interface XScreenSaverView (Private)
122 - (void) stopAndClose;
123 - (void) stopAndClose:(Bool)relaunch;
126 @implementation XScreenSaverView
128 // Given a lower-cased saver name, returns the function table for it.
129 // If no name, guess the name from the class's bundle name.
131 - (struct xscreensaver_function_table *) findFunctionTable:(NSString *)name
133 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
134 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
136 NSString *path = [nsb bundlePath];
137 CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
139 kCFURLPOSIXPathStyle,
141 CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url);
143 NSAssert1 (cfb, @"no CFBundle for \"%@\"", path);
144 // #### Analyze says "Potential leak of an object stored into cfb"
147 name = [[path lastPathComponent] stringByDeletingPathExtension];
149 name = [[name lowercaseString]
150 stringByReplacingOccurrencesOfString:@" "
154 // CFBundleGetDataPointerForName doesn't work in "Archive" builds.
155 // I'm guessing that symbol-stripping is mandatory. Fuck.
156 NSString *table_name = [name stringByAppendingString:
157 @"_xscreensaver_function_table"];
158 void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name);
162 NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path);
165 // Depends on the auto-generated "ios-function-table.m" being up to date.
166 if (! function_tables)
167 function_tables = [make_function_table_dict() retain];
168 NSValue *v = [function_tables objectForKey: name];
169 void *addr = v ? [v pointerValue] : 0;
170 # endif // USE_IPHONE
172 return (struct xscreensaver_function_table *) addr;
176 // Add the "Contents/Resources/" subdirectory of this screen saver's .bundle
177 // to $PATH for the benefit of savers that include helper shell scripts.
179 - (void) setShellPath
181 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
182 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
184 NSString *nsdir = [nsb resourcePath];
185 NSAssert1 (nsdir, @"no resourcePath for class %@", [self class]);
186 const char *dir = [nsdir cStringUsingEncoding:NSUTF8StringEncoding];
187 const char *opath = getenv ("PATH");
188 if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
189 char *npath = (char *) malloc (strlen (opath) + strlen (dir) + 2);
192 strcat (npath, opath);
193 if (setenv ("PATH", npath, 1)) {
195 NSAssert1 (0, @"setenv \"PATH=%s\" failed", npath);
202 // set an $XSCREENSAVER_CLASSPATH variable so that included shell scripts
203 // (e.g., "xscreensaver-text") know how to look up resources.
205 - (void) setResourcesEnv:(NSString *) name
207 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
208 NSAssert1 (nsb, @"no bundle for class %@", [self class]);
210 const char *s = [name cStringUsingEncoding:NSUTF8StringEncoding];
211 if (setenv ("XSCREENSAVER_CLASSPATH", s, 1)) {
213 NSAssert1 (0, @"setenv \"XSCREENSAVER_CLASSPATH=%s\" failed", s);
218 - (void) loadCustomFonts
221 NSBundle *nsb = [NSBundle bundleForClass:[self class]];
222 NSMutableArray *fonts = [NSMutableArray arrayWithCapacity:20];
223 for (NSString *ext in @[@"ttf", @"otf"]) {
224 [fonts addObjectsFromArray: [nsb pathsForResourcesOfType:ext
227 for (NSString *font in fonts) {
228 CFURLRef url = (CFURLRef) [NSURL fileURLWithPath: font];
230 if (! CTFontManagerRegisterFontsForURL (url, kCTFontManagerScopeProcess,
232 // Just ignore errors:
233 // "The file has already been registered in the specified scope."
234 // NSLog (@"loading font: %@ %@", url, err);
237 # endif // !USE_IPHONE
242 add_default_options (const XrmOptionDescRec *opts,
243 const char * const *defs,
244 XrmOptionDescRec **opts_ret,
245 const char ***defs_ret)
247 /* These aren't "real" command-line options (there are no actual command-line
248 options in the Cocoa version); but this is the somewhat kludgey way that
249 the <xscreensaver-text /> and <xscreensaver-image /> tags in the
250 ../hacks/config/\*.xml files communicate with the preferences database.
252 static const XrmOptionDescRec default_options [] = {
253 { "-text-mode", ".textMode", XrmoptionSepArg, 0 },
254 { "-text-literal", ".textLiteral", XrmoptionSepArg, 0 },
255 { "-text-file", ".textFile", XrmoptionSepArg, 0 },
256 { "-text-url", ".textURL", XrmoptionSepArg, 0 },
257 { "-text-program", ".textProgram", XrmoptionSepArg, 0 },
258 { "-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "True" },
259 { "-no-grab-desktop", ".grabDesktopImages", XrmoptionNoArg, "False"},
260 { "-choose-random-images", ".chooseRandomImages",XrmoptionNoArg, "True" },
261 { "-no-choose-random-images",".chooseRandomImages",XrmoptionNoArg, "False"},
262 { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 },
263 { "-fps", ".doFPS", XrmoptionNoArg, "True" },
264 { "-no-fps", ".doFPS", XrmoptionNoArg, "False"},
265 { "-foreground", ".foreground", XrmoptionSepArg, 0 },
266 { "-fg", ".foreground", XrmoptionSepArg, 0 },
267 { "-background", ".background", XrmoptionSepArg, 0 },
268 { "-bg", ".background", XrmoptionSepArg, 0 },
271 // <xscreensaver-updater />
272 { "-" SUSUEnableAutomaticChecksKey,
273 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True" },
274 { "-no-" SUSUEnableAutomaticChecksKey,
275 "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" },
276 { "-" SUAutomaticallyUpdateKey,
277 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True" },
278 { "-no-" SUAutomaticallyUpdateKey,
279 "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" },
280 { "-" SUSendProfileInfoKey,
281 "." SUSendProfileInfoKey, XrmoptionNoArg,"True" },
282 { "-no-" SUSendProfileInfoKey,
283 "." SUSendProfileInfoKey, XrmoptionNoArg,"False"},
284 { "-" SUScheduledCheckIntervalKey,
285 "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 },
286 # endif // !USE_IPHONE
290 static const char *default_defaults [] = {
292 # if defined(USE_IPHONE) && !defined(__OPTIMIZE__)
297 ".doubleBuffer: True",
298 ".multiSample: False",
306 ".textURL: https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss",
308 ".grabDesktopImages: yes",
310 ".chooseRandomImages: no",
312 ".chooseRandomImages: yes",
314 ".imageDirectory: ~/Pictures",
316 ".texFontCacheSize: 30",
320 # define STR(S) STR1(S)
321 # define __objc_yes Yes
322 # define __objc_no No
323 "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef),
324 "." SUAutomaticallyUpdateKey ": " STR(SUAutomaticallyUpdateDef),
325 "." SUSendProfileInfoKey ": " STR(SUSendProfileInfoDef),
326 "." SUScheduledCheckIntervalKey ": " STR(SUScheduledCheckIntervalDef),
331 # endif // USE_IPHONE
336 for (i = 0; default_options[i].option; i++)
338 for (i = 0; opts[i].option; i++)
341 XrmOptionDescRec *opts2 = (XrmOptionDescRec *)
342 calloc (count + 1, sizeof (*opts2));
346 while (default_options[j].option) {
347 opts2[i] = default_options[j];
351 while (opts[j].option) {
362 for (i = 0; default_defaults[i]; i++)
364 for (i = 0; defs[i]; i++)
367 const char **defs2 = (const char **) calloc (count + 1, sizeof (*defs2));
371 while (default_defaults[j]) {
372 defs2[i] = default_defaults[j];
385 - (id) initWithFrame:(NSRect)frame
386 saverName:(NSString *)saverName
387 isPreview:(BOOL)isPreview
389 if (! (self = [super initWithFrame:frame isPreview:isPreview]))
392 xsft = [self findFunctionTable: saverName];
402 xsft->setup_cb (xsft, xsft->setup_arg);
404 /* The plist files for these preferences show up in
405 $HOME/Library/Preferences/ByHost/ in a file named like
406 "org.jwz.xscreensaver.<SAVERNAME>.<NUMBERS>.plist"
408 NSString *name = [NSString stringWithCString:xsft->progclass
409 encoding:NSISOLatin1StringEncoding];
410 name = [@"org.jwz.xscreensaver." stringByAppendingString:name];
411 [self setResourcesEnv:name];
412 [self loadCustomFonts];
414 XrmOptionDescRec *opts = 0;
415 const char **defs = 0;
416 add_default_options (xsft->options, xsft->defaults, &opts, &defs);
417 prefsReader = [[PrefsReader alloc]
418 initWithName:name xrmKeys:opts defaults:defs];
420 // free (opts); // bah, we need these! #### leak!
421 xsft->options = opts;
423 progname = progclass = xsft->progclass;
427 # if !defined USE_IPHONE && defined JWXYZ_QUARTZ
428 // When the view fills the screen and double buffering is enabled, OS X will
429 // use page flipping for a minor CPU/FPS boost. In windowed mode, double
430 // buffering reduces the frame rate to 1/2 the screen's refresh rate.
431 double_buffered_p = !isPreview;
437 // So we can tell when we're docked.
438 [UIDevice currentDevice].batteryMonitoringEnabled = YES;
440 [self setBackgroundColor:[NSColor blackColor]];
441 # endif // USE_IPHONE
444 // Colorspaces and CGContexts only happen with non-GL hacks.
445 colorspace = CGColorSpaceCreateDeviceRGB ();
455 return [CAEAGLLayer class];
460 - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p
462 return [self initWithFrame:frame saverName:0 isPreview:p];
468 if ([self isAnimating])
469 [self stopAnimation];
470 NSAssert(!xdata, @"xdata not yet freed");
471 NSAssert(!xdpy, @"xdpy not yet freed");
474 [[NSNotificationCenter defaultCenter] removeObserver:self];
477 # ifdef BACKBUFFER_OPENGL
480 # endif // !USE_IPHONE
482 // Releasing the OpenGL context should also free any OpenGL objects,
483 // including the backbuffer texture and frame/render/depthbuffers.
484 # endif // BACKBUFFER_OPENGL
486 # if defined JWXYZ_GL && defined USE_IPHONE
487 [ogl_ctx_pixmap release];
492 CGColorSpaceRelease (colorspace);
493 # endif // JWXYZ_QUARTZ
495 [prefsReader release];
503 - (PrefsReader *) prefsReader
510 - (void) lockFocus { }
511 - (void) unlockFocus { }
517 /* A few seconds after the saver launches, we store the "wasRunning"
518 preference. This is so that if the saver is crashing at startup,
519 we don't launch it again next time, getting stuck in a crash loop.
521 - (void) allSystemsGo: (NSTimer *) timer
523 NSAssert (timer == crash_timer, @"crash timer screwed up");
526 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
527 [prefs setBool:YES forKey:@"wasRunning"];
537 CGSize screen_size = self.bounds.size;
538 double s = self.contentScaleFactor;
539 screen_size.width *= s;
540 screen_size.height *= s;
543 GLuint *framebuffer = &xwindow->gl_framebuffer;
544 GLuint *renderbuffer = &xwindow->gl_renderbuffer;
545 xwindow->window.current_drawable = xwindow;
546 #elif defined JWXYZ_QUARTZ
547 GLuint *framebuffer = &gl_framebuffer;
548 GLuint *renderbuffer = &gl_renderbuffer;
549 #endif // JWXYZ_QUARTZ
551 if (*framebuffer) glDeleteFramebuffersOES (1, framebuffer);
552 if (*renderbuffer) glDeleteRenderbuffersOES (1, renderbuffer);
554 create_framebuffer (framebuffer, renderbuffer);
557 // glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES,
558 // (int)size.width, (int)size.height);
559 [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES
560 fromDrawable:(CAEAGLLayer*)self.layer];
562 glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES,
563 GL_RENDERBUFFER_OES, *renderbuffer);
565 [self addExtraRenderbuffers:screen_size];
567 check_framebuffer_status();
572 - (void) startAnimation
574 NSAssert(![self isAnimating], @"already animating");
575 NSAssert(!initted_p && !xdata, @"already initialized");
577 // See comment in render_x11() for why this value is important:
578 [self setAnimationTimeInterval: 1.0 / 240.0];
580 [super startAnimation];
581 /* We can't draw on the window from this method, so we actually do the
582 initialization of the screen saver (xsft->init_cb) in the first call
583 to animateOneFrame() instead.
588 [crash_timer invalidate];
590 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
591 [prefs removeObjectForKey:@"wasRunning"];
594 crash_timer = [NSTimer scheduledTimerWithTimeInterval: 5
596 selector:@selector(allSystemsGo:)
600 # endif // USE_IPHONE
602 // Never automatically turn the screen off if we are docked,
603 // and an animation is running.
606 [UIApplication sharedApplication].idleTimerDisabled =
607 ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
610 xwindow = (Window) calloc (1, sizeof(*xwindow));
611 xwindow->type = WINDOW;
612 xwindow->window.view = self;
613 CFRetain (xwindow->window.view); // needed for garbage collection?
615 #ifdef BACKBUFFER_OPENGL
616 CGSize new_backbuffer_size;
622 pixfmt = [self getGLPixelFormat];
625 NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat");
627 // Fun: On OS X 10.7, the second time an OpenGL context is created, after
628 // the preferences dialog is launched in SaverTester, the context only
629 // lasts until the first full GC. Then it turns black. Solution is to
630 // reuse the OpenGL context after this point.
631 // "Analyze" says that both pixfmt and ogl_ctx are leaked.
632 ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt
635 // Sync refreshes to the vertical blanking interval
637 [ogl_ctx setValues:&r forParameter:NSOpenGLCPSwapInterval];
638 // check_gl_error ("NSOpenGLCPSwapInterval"); // SEGV sometimes. Too early?
641 [ogl_ctx makeCurrentContext];
642 check_gl_error ("makeCurrentContext");
644 // NSOpenGLContext logs an 'invalid drawable' when this is called
645 // from initWithFrame.
646 [ogl_ctx setView:self];
648 // This may not be necessary if there's FBO support.
650 xwindow->window.pixfmt = pixfmt;
651 CFRetain (xwindow->window.pixfmt);
652 xwindow->window.virtual_screen = [ogl_ctx currentVirtualScreen];
653 xwindow->window.current_drawable = xwindow;
654 NSAssert (ogl_ctx, @"no CGContext");
657 // Clear frame buffer ASAP, else there are bits left over from other apps.
658 glClearColor (0, 0, 0, 1);
659 glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
661 // glXSwapBuffers (mi->dpy, mi->window);
664 // Enable multi-threading, if possible. This runs most OpenGL commands
665 // and GPU management on a second CPU.
667 # ifndef kCGLCEMPEngine
668 # define kCGLCEMPEngine 313 // Added in MacOS 10.4.8 + XCode 2.4.
670 CGLContextObj cctx = CGLGetCurrentContext();
671 CGLError err = CGLEnable (cctx, kCGLCEMPEngine);
672 if (err != kCGLNoError) {
673 NSLog (@"enabling multi-threaded OpenGL failed: %d", err);
677 new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
681 CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
682 eagl_layer.opaque = TRUE;
683 eagl_layer.drawableProperties = [self getGLProperties];
685 // Without this, the GL frame buffer is half the screen resolution!
686 eagl_layer.contentsScale = [UIScreen mainScreen].scale;
688 ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
690 ogl_ctx_pixmap = [[EAGLContext alloc]
691 initWithAPI:kEAGLRenderingAPIOpenGLES1
692 sharegroup:ogl_ctx.sharegroup];
695 eagl_layer.contentsGravity = [self getCAGravity];
699 xwindow->window.ogl_ctx_pixmap = ogl_ctx_pixmap;
702 [EAGLContext setCurrentContext: ogl_ctx];
706 double s = [self hackedContentScaleFactor];
707 new_backbuffer_size = self.bounds.size;
708 new_backbuffer_size.width *= s;
709 new_backbuffer_size.height *= s;
711 # endif // USE_IPHONE
714 xwindow->ogl_ctx = ogl_ctx;
716 CFRetain (xwindow->ogl_ctx);
717 # endif // USE_IPHONE
720 check_gl_error ("startAnimation");
722 // NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR),
723 // glGetString (GL_RENDERER), glGetString (GL_VERSION));
725 [self enableBackbuffer:new_backbuffer_size];
727 #endif // BACKBUFFER_OPENGL
730 [self createBackbuffer:new_backbuffer_size];
733 - (void)stopAnimation
735 NSAssert([self isAnimating], @"not animating");
739 [self lockFocus]; // in case something tries to draw from here
740 [self prepareContext];
742 /* All of the xlockmore hacks need to have their release functions
743 called, or launching the same saver twice does not work. Also
744 webcollage-cocoa needs it in order to kill the inferior webcollage
745 processes (since the screen saver framework never generates a
749 xsft->free_cb (xdpy, xwindow, xdata);
752 jwxyz_free_display (xdpy);
754 # if defined JWXYZ_GL && !defined USE_IPHONE
755 CFRelease (xwindow->ogl_ctx);
757 CFRelease (xwindow->window.view);
761 // setup_p = NO; // #### wait, do we need this?
768 [crash_timer invalidate];
770 NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
771 [prefs removeObjectForKey:@"wasRunning"];
773 # endif // USE_IPHONE
775 [super stopAnimation];
777 // When an animation is no longer running (e.g., looking at the list)
778 // then it's ok to power off the screen when docked.
781 [UIApplication sharedApplication].idleTimerDisabled = NO;
784 // Without this, the GL frame stays on screen when switching tabs
785 // in System Preferences.
786 // (Or perhaps it used to. It doesn't seem to matter on 10.9.)
789 [NSOpenGLContext clearCurrentContext];
790 # endif // !USE_IPHONE
792 clear_gl_error(); // This hack is defunct, don't let this linger.
795 CGContextRelease (backbuffer);
799 munmap (backbuffer_data, backbuffer_len);
800 backbuffer_data = NULL;
806 - (NSOpenGLContext *) oglContext
812 // #### maybe this could/should just be on 'lockFocus' instead?
813 - (void) prepareContext
817 [EAGLContext setCurrentContext:ogl_ctx];
819 [ogl_ctx makeCurrentContext];
820 // check_gl_error ("makeCurrentContext");
821 #endif // !USE_IPHONE
824 xwindow->window.current_drawable = xwindow;
831 screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
833 fps_compute (fpst, 0, -1);
840 /* On iPhones with Retina displays, we can draw the savers in "real"
841 pixels, and that works great. The 320x480 "point" screen is really
842 a 640x960 *pixel* screen. However, Retina iPads have 768x1024
843 point screens which are 1536x2048 pixels, and apparently that's
844 enough pixels that copying those bits to the screen is slow. Like,
845 drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in
846 real pixels. This will probably make the savers look better
847 anyway, since that's a higher resolution than most desktop monitors
848 have even today. (This is only true for X11 programs, not GL
849 programs. Those are fine at full rez.)
851 This method is overridden in XScreenSaverGLView, since this kludge
852 isn't necessary for GL programs, being resolution independent by
855 - (CGFloat) hackedContentScaleFactor
857 NSSize bsize = [self bounds].size;
860 max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height;
862 // Ratio of screen size in pixels to view size in points.
863 CGFloat s = self.contentScaleFactor;
867 // 1. Don't exceed -- let's say 1280 pixels in either direction.
868 // (Otherwise the frame rate gets bad.)
869 // Actually let's make that 1440 since iPhone 6 is natively 1334.
870 CGFloat mag0 = ceil(max_bsize * s / 1440);
872 // 2. Don't let the pixel size get too small.
873 // (Otherwise pixels in IFS and similar are too fine.)
874 // So don't let the result be > 2 pixels per point.
875 CGFloat mag1 = ceil(s / 2);
877 // As of iPhone 6, mag0 is always >= mag1. This may not be true in the future.
878 // (desired scale factor) = s / (desired magnification factor)
879 return s / (mag0 > mag1 ? mag0 : mag1);
884 current_device_rotation (void)
886 UIDeviceOrientation o = [[UIDevice currentDevice] orientation];
888 /* Sometimes UIDevice doesn't know the proper orientation, or the device is
889 face up/face down, so in those cases fall back to the status bar
890 orientation. The SaverViewController tries to set the status bar to the
891 proper orientation before it creates the XScreenSaverView; see
892 _storedOrientation in SaverViewController.
894 if (o == UIDeviceOrientationUnknown ||
895 o == UIDeviceOrientationFaceUp ||
896 o == UIDeviceOrientationFaceDown) {
897 /* Mind the differences between UIInterfaceOrientation and
899 1. UIInterfaceOrientation does not include FaceUp and FaceDown.
900 2. LandscapeLeft and LandscapeRight are swapped between the two. But
901 converting between device and interface orientation doesn't need to
902 take this into account, because (from the UIInterfaceOrientation
903 description): "rotating the device requires rotating the content in
904 the opposite direction."
906 /* statusBarOrientation deprecated in iOS 9 */
907 o = (UIDeviceOrientation) // from UIInterfaceOrientation
908 [UIApplication sharedApplication].statusBarOrientation;
912 case UIDeviceOrientationLandscapeLeft: return -90; break;
913 case UIDeviceOrientationLandscapeRight: return 90; break;
914 case UIDeviceOrientationPortraitUpsideDown: return 180; break;
915 default: return 0; break;
920 - (void) handleException: (NSException *)e
922 NSLog (@"Caught exception: %@", e);
923 UIAlertController *c = [UIAlertController
924 alertControllerWithTitle:
925 [NSString stringWithFormat: @"%s crashed!",
927 message: [NSString stringWithFormat:
928 @"The error message was:"
930 "If it keeps crashing, try "
931 "resetting its options.",
933 preferredStyle:UIAlertControllerStyleAlert];
935 [c addAction: [UIAlertAction actionWithTitle: @"Exit"
936 style: UIAlertActionStyleDefault
937 handler: ^(UIAlertAction *a) {
940 [c addAction: [UIAlertAction actionWithTitle: @"Keep going"
941 style: UIAlertActionStyleDefault
942 handler: ^(UIAlertAction *a) {
943 [self stopAndClose:NO];
946 UIViewController *vc =
947 [UIApplication sharedApplication].keyWindow.rootViewController;
948 while (vc.presentedViewController)
949 vc = vc.presentedViewController;
950 [vc presentViewController:c animated:YES completion:nil];
951 [self stopAnimation];
963 // iOS always uses OpenGL ES 1.1.
969 gl_check_ver (const struct gl_version *caps,
973 return caps->major > gl_major ||
974 (caps->major == gl_major && caps->minor >= gl_minor);
979 /* Called during startAnimation before the first call to createBackbuffer. */
980 - (void) enableBackbuffer:(CGSize)new_backbuffer_size
983 struct gl_version version;
986 const char *version_str = (const char *)glGetString (GL_VERSION);
988 /* iPhone is always OpenGL ES 1.1. */
989 if (sscanf ((const char *)version_str, "%u.%u",
990 &version.major, &version.minor) < 2)
998 // The OpenGL extensions in use in here are pretty are pretty much ubiquitous
999 // on OS X, but it's still good form to check.
1000 const GLubyte *extensions = glGetString (GL_EXTENSIONS);
1002 glGenTextures (1, &backbuffer_texture);
1004 // On really old systems, it would make sense to split the texture
1007 gl_texture_target = (gluCheckExtension ((const GLubyte *)
1008 "GL_ARB_texture_rectangle",
1010 ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D);
1012 // OES_texture_npot also provides this, but iOS never provides it.
1013 gl_limited_npot_p = jwzgles_gluCheckExtension
1014 ((const GLubyte *) "GL_APPLE_texture_2D_limited_npot", extensions);
1015 gl_texture_target = GL_TEXTURE_2D;
1018 glBindTexture (gl_texture_target, backbuffer_texture);
1019 glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1020 // GL_LINEAR might make sense on Retina iPads.
1021 glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
1022 glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1023 glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1026 // There isn't much sense in supporting one of these if the other
1028 gl_apple_client_storage_p =
1029 gluCheckExtension ((const GLubyte *)"GL_APPLE_client_storage",
1031 gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_range", extensions);
1033 if (gl_apple_client_storage_p) {
1034 glTexParameteri (gl_texture_target, GL_TEXTURE_STORAGE_HINT_APPLE,
1035 GL_STORAGE_SHARED_APPLE);
1036 glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
1040 // If a video adapter suports BGRA textures, then that's probably as fast as
1041 // you're gonna get for getting a texture onto the screen.
1044 jwzgles_gluCheckExtension
1045 ((const GLubyte *)"GL_APPLE_texture_format_BGRA8888", extensions) ?
1049 gl_pixel_type = GL_UNSIGNED_BYTE;
1050 // See also OES_read_format.
1052 if (gl_check_ver (&version, 1, 2) ||
1053 (gluCheckExtension ((const GLubyte *)"GL_EXT_bgra", extensions) &&
1054 gluCheckExtension ((const GLubyte *)"GL_APPLE_packed_pixels",
1056 gl_pixel_format = GL_BGRA;
1057 // Both Intel and PowerPC-era docs say to use GL_UNSIGNED_INT_8_8_8_8_REV.
1058 gl_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1060 gl_pixel_format = GL_RGBA;
1061 gl_pixel_type = GL_UNSIGNED_BYTE;
1063 // GL_ABGR_EXT/GL_UNSIGNED_BYTE is another possibilty that may have made more
1064 // sense on PowerPC.
1067 glEnable (gl_texture_target);
1068 glEnableClientState (GL_VERTEX_ARRAY);
1069 glEnableClientState (GL_TEXTURE_COORD_ARRAY);
1071 check_gl_error ("enableBackbuffer");
1076 - (BOOL) suppressRotationAnimation
1078 return [self ignoreRotation]; // Don't animate if we aren't rotating
1081 - (BOOL) rotateTouches
1083 return FALSE; // Adjust event coordinates only if rotating
1088 - (void) setViewport
1090 # ifdef BACKBUFFER_OPENGL
1091 NSAssert ([NSOpenGLContext currentContext] ==
1092 ogl_ctx, @"invalid GL context");
1094 NSSize new_size = self.bounds.size;
1097 GLfloat s = self.contentScaleFactor;
1098 GLfloat hs = self.hackedContentScaleFactor;
1099 # else // !USE_IPHONE
1100 const GLfloat s = 1;
1101 const GLfloat hs = s;
1104 // On OS X this almost isn't necessary, except for the ugly aliasing
1106 glViewport (0, 0, new_size.width * s, new_size.height * s);
1108 glMatrixMode (GL_PROJECTION);
1115 (-new_size.width * hs, new_size.width * hs,
1116 -new_size.height * hs, new_size.height * hs,
1120 if ([self ignoreRotation]) {
1121 int o = (int) -current_device_rotation();
1122 glRotatef (o, 0, 0, 1);
1124 # endif // USE_IPHONE
1125 # endif // BACKBUFFER_OPENGL
1129 /* Create a bitmap context into which we render everything.
1130 If the desired size has changed, re-created it.
1131 new_size is in rotated pixels, not points: the same size
1132 and shape as the X11 window as seen by the hacks.
1134 - (void) createBackbuffer:(CGSize)new_size
1136 CGSize osize = CGSizeZero;
1138 osize.width = CGBitmapContextGetWidth(backbuffer);
1139 osize.height = CGBitmapContextGetHeight(backbuffer);
1143 (int)osize.width == (int)new_size.width &&
1144 (int)osize.height == (int)new_size.height)
1147 CGContextRef ob = backbuffer;
1148 void *odata = backbuffer_data;
1149 GLsizei olen = backbuffer_len;
1151 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1152 NSLog(@"backbuffer %.0fx%.0f",
1153 new_size.width, new_size.height);
1156 /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in
1157 <https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html>.
1159 iOS uses bog-standard glTexImage2D (for now).
1161 glMapBuffer is the standard way to get data from system RAM to video
1162 memory asynchronously and without a memcpy, but support for
1163 APPLE_client_storage is ubiquitous on OS X (not so for glMapBuffer),
1164 and on iOS GL_PIXEL_UNPACK_BUFFER is only available on OpenGL ES 3
1165 (iPhone 5S or newer). Plus, glMapBuffer doesn't work well with
1166 CGBitmapContext: glMapBuffer can return a different pointer on each
1167 call, but a CGBitmapContext doesn't allow its data pointer to be
1168 changed -- and recreating the context for a new pointer can be
1169 expensive (glyph caches get dumped, for instance).
1171 glMapBufferRange has MAP_FLUSH_EXPLICIT_BIT and MAP_UNSYNCHRONIZED_BIT,
1172 and these seem to allow mapping the buffer and leaving it where it is
1173 in client address space while OpenGL works with the buffer, but it
1174 requires OpenGL 3 Core profile on OS X (and ES 3 on iOS for
1175 GL_PIXEL_UNPACK_BUFFER), so point goes to APPLE_client_storage.
1177 AMD_pinned_buffer provides the same advantage as glMapBufferRange, but
1178 Apple never implemented that one for OS X.
1181 backbuffer_data = NULL;
1182 gl_texture_w = (int)new_size.width;
1183 gl_texture_h = (int)new_size.height;
1185 NSAssert (gl_texture_target == GL_TEXTURE_2D
1187 || gl_texture_target == GL_TEXTURE_RECTANGLE_EXT
1189 , @"unexpected GL texture target");
1192 if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1194 if (!gl_limited_npot_p)
1197 gl_texture_w = (GLsizei) to_pow2 (gl_texture_w);
1198 gl_texture_h = (GLsizei) to_pow2 (gl_texture_h);
1201 GLsizei bytes_per_row = gl_texture_w * 4;
1203 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1204 // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
1205 // it will fall back to a memcpy.
1206 // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW24
1207 bytes_per_row = (bytes_per_row + 31) & ~31;
1208 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1210 backbuffer_len = bytes_per_row * gl_texture_h;
1211 if (backbuffer_len) // mmap requires this to be non-zero.
1212 backbuffer_data = mmap (NULL, backbuffer_len,
1213 PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED,
1216 BOOL alpha_first_p, order_little_p;
1218 if (gl_pixel_format == GL_BGRA) {
1219 alpha_first_p = YES;
1220 order_little_p = YES;
1222 } else if (gl_pixel_format == GL_ABGR_EXT) {
1224 order_little_p = YES; */
1226 NSAssert (gl_pixel_format == GL_RGBA, @"unknown GL pixel format");
1228 order_little_p = NO;
1232 NSAssert (gl_pixel_type == GL_UNSIGNED_BYTE, @"unknown GL pixel type");
1234 NSAssert (gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8 ||
1235 gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8_REV ||
1236 gl_pixel_type == GL_UNSIGNED_BYTE,
1237 @"unknown GL pixel type");
1239 #if defined __LITTLE_ENDIAN__
1240 const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8;
1241 #elif defined __BIG_ENDIAN__
1242 const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV;
1244 # error Unknown byte order.
1247 if (gl_pixel_type == backwards_pixel_type)
1248 order_little_p ^= YES;
1251 CGBitmapInfo bitmap_info =
1252 (alpha_first_p ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaNoneSkipLast) |
1253 (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big);
1255 backbuffer = CGBitmapContextCreate (backbuffer_data,
1256 (int)new_size.width,
1257 (int)new_size.height,
1262 NSAssert (backbuffer, @"unable to allocate back buffer");
1266 r.origin.x = r.origin.y = 0;
1268 CGContextSetGrayFillColor (backbuffer, 0, 1);
1269 CGContextFillRect (backbuffer, r);
1271 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1272 if (gl_apple_client_storage_p)
1273 glTextureRangeAPPLE (gl_texture_target, backbuffer_len, backbuffer_data);
1274 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1277 // Restore old bits, as much as possible, to the X11 upper left origin.
1279 CGRect rect; // pixels, not points
1281 rect.origin.y = (new_size.height - osize.height);
1284 CGImageRef img = CGBitmapContextCreateImage (ob);
1285 CGContextDrawImage (backbuffer, rect, img);
1286 CGImageRelease (img);
1287 CGContextRelease (ob);
1290 // munmap should round len up to the nearest page.
1291 munmap (odata, olen);
1294 check_gl_error ("createBackbuffer");
1298 - (void) drawBackbuffer
1300 # ifdef BACKBUFFER_OPENGL
1302 NSAssert ([ogl_ctx isKindOfClass:[NSOpenGLContext class]],
1303 @"ogl_ctx is not an NSOpenGLContext");
1305 NSAssert (! (CGBitmapContextGetBytesPerRow (backbuffer) % 4),
1306 @"improperly-aligned backbuffer");
1308 // This gets width and height from the backbuffer in case
1309 // APPLE_client_storage is in use. See the note in createBackbuffer.
1310 // This still has to happen every frame even when APPLE_client_storage has
1311 // the video adapter pulling texture data straight from
1312 // XScreenSaverView-owned memory.
1313 glTexImage2D (gl_texture_target, 0, GL_RGBA,
1314 (GLsizei)(CGBitmapContextGetBytesPerRow (backbuffer) / 4),
1315 gl_texture_h, 0, gl_pixel_format, gl_pixel_type,
1318 GLfloat w = xwindow->frame.width, h = xwindow->frame.height;
1320 GLfloat vertices[4][2] = {{-w, h}, {w, h}, {w, -h}, {-w, -h}};
1322 GLfloat tex_coords[4][2];
1325 if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT)
1326 # endif // USE_IPHONE
1332 tex_coords[0][0] = 0;
1333 tex_coords[0][1] = 0;
1334 tex_coords[1][0] = w;
1335 tex_coords[1][1] = 0;
1336 tex_coords[2][0] = w;
1337 tex_coords[2][1] = h;
1338 tex_coords[3][0] = 0;
1339 tex_coords[3][1] = h;
1341 glVertexPointer (2, GL_FLOAT, 0, vertices);
1342 glTexCoordPointer (2, GL_FLOAT, 0, tex_coords);
1343 glDrawArrays (GL_TRIANGLE_FAN, 0, 4);
1345 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1346 check_gl_error ("drawBackbuffer");
1348 # endif // BACKBUFFER_OPENGL
1351 #endif // JWXYZ_QUARTZ
1355 - (void)enableBackbuffer:(CGSize)new_backbuffer_size;
1357 jwxyz_set_matrices (new_backbuffer_size.width, new_backbuffer_size.height);
1358 check_gl_error ("enableBackbuffer");
1361 - (void)createBackbuffer:(CGSize)new_size
1363 NSAssert ([NSOpenGLContext currentContext] ==
1364 ogl_ctx, @"invalid GL context");
1365 NSAssert (xwindow->window.current_drawable == xwindow,
1366 @"current_drawable not set properly");
1369 /* On iOS, Retina means glViewport gets called with the screen size instead
1370 of the backbuffer/xwindow size. This happens in startAnimation.
1372 The GL screenhacks call glViewport themselves.
1374 glViewport (0, 0, new_size.width, new_size.height);
1377 // TODO: Preserve contents on resize.
1378 glClear (GL_COLOR_BUFFER_BIT);
1379 check_gl_error ("createBackbuffer");
1385 - (void)flushBackbuffer
1388 // Make sure the right context is active: there's two under JWXYZ_GL.
1389 jwxyz_bind_drawable (xwindow, xwindow);
1394 # ifdef JWXYZ_QUARTZ
1395 // The OpenGL pipeline is not automatically synchronized with the contents
1396 // of the backbuffer, so without glFinish, OpenGL can start rendering from
1397 // the backbuffer texture at the same time that JWXYZ is clearing and
1398 // drawing the next frame in the backing store for the backbuffer texture.
1399 // This is only a concern under JWXYZ_QUARTZ because of
1400 // APPLE_client_storage; JWXYZ_GL doesn't use that.
1402 # endif // JWXYZ_QUARTZ
1404 // If JWXYZ_GL was single-buffered, there would need to be a glFinish (or
1405 // maybe just glFlush?) here, because single-buffered contexts don't always
1406 // update what's on the screen after drawing finishes. (i.e., in safe mode)
1408 # ifdef JWXYZ_QUARTZ
1409 // JWXYZ_GL is always double-buffered.
1410 if (double_buffered_p)
1411 # endif // JWXYZ_QUARTZ
1412 [ogl_ctx flushBuffer]; // despite name, this actually swaps
1413 # else // USE_IPHONE
1415 // jwxyz_bind_drawable() only binds the framebuffer, not the renderbuffer.
1417 GLint gl_renderbuffer = xwindow->gl_renderbuffer;
1420 glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer);
1421 [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES];
1422 # endif // USE_IPHONE
1424 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1425 // glGetError waits for the OpenGL command pipe to flush, so skip it in
1427 // OpenGL Programming Guide for Mac -> OpenGL Application Design
1428 // Strategies -> Allow OpenGL to Manage Your Resources
1429 // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_designstrategies/opengl_designstrategies.html#//apple_ref/doc/uid/TP40001987-CH2-SW7
1430 check_gl_error ("flushBackbuffer");
1435 /* Inform X11 that the size of our window has changed.
1439 if (!xdpy) return; // early
1441 NSSize new_size; // pixels, not points
1443 new_size = self.bounds.size;
1447 // If this hack ignores rotation, then that means that it pretends to
1448 // always be in portrait mode. If the View has been resized to a
1449 // landscape shape, swap width and height to keep the backbuffer
1452 double rot = current_device_rotation();
1453 if ([self ignoreRotation] && (rot == 90 || rot == -90)) {
1454 CGFloat swap = new_size.width;
1455 new_size.width = new_size.height;
1456 new_size.height = swap;
1459 double s = self.hackedContentScaleFactor;
1460 new_size.width *= s;
1461 new_size.height *= s;
1462 # endif // USE_IPHONE
1464 [self prepareContext];
1467 // On first resize, xwindow->frame is 0x0.
1468 if (xwindow->frame.width == new_size.width &&
1469 xwindow->frame.height == new_size.height)
1472 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
1474 # endif // BACKBUFFER_OPENGL && !USE_IPHONE
1476 NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
1477 xwindow->frame.x = 0;
1478 xwindow->frame.y = 0;
1479 xwindow->frame.width = new_size.width;
1480 xwindow->frame.height = new_size.height;
1482 [self createBackbuffer:CGSizeMake(xwindow->frame.width,
1483 xwindow->frame.height)];
1485 # if defined JWXYZ_QUARTZ
1486 xwindow->cgc = backbuffer;
1487 NSAssert (xwindow->cgc, @"no CGContext");
1488 # elif defined JWXYZ_GL && !defined USE_IPHONE
1490 [ogl_ctx setView:xwindow->window.view]; // (Is this necessary?)
1491 # endif // JWXYZ_GL && USE_IPHONE
1493 jwxyz_window_resized (xdpy);
1495 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
1496 NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height);
1499 // Next time render_x11 is called, run the saver's reshape_cb.
1506 /* Called by SaverRunner when the device has changed orientation.
1507 That means we need to generate a resize event, even if the size
1508 has not changed (e.g., from LandscapeLeft to LandscapeRight).
1510 - (void) orientationChanged
1514 next_frame_time = 0; // Get a new frame on screen quickly
1517 /* A hook run after the 'reshape_' method has been called. Used by
1518 XScreenSaverGLView to adjust the in-scene GL viewport.
1520 - (void) postReshape
1523 #endif // USE_IPHONE
1526 // Only render_x11 should call this. XScreenSaverGLView specializes it.
1527 - (void) reshape_x11
1529 xsft->reshape_cb (xdpy, xwindow, xdata,
1530 xwindow->frame.width, xwindow->frame.height);
1539 // jwxyz_make_display needs this.
1540 [self prepareContext]; // resize_x11 also calls this.
1545 # ifdef JWXYZ_QUARTZ
1546 xwindow->cgc = backbuffer;
1547 # endif // JWXYZ_QUARTZ
1548 xdpy = jwxyz_make_display (xwindow);
1550 # if defined USE_IPHONE
1551 /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
1554 TRUE; // Rotation doesn't work yet. TODO: Make rotation work.
1556 get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation");
1557 # endif // !JWXYZ_GL
1558 # endif // USE_IPHONE
1566 xsft->setup_cb (xsft, xsft->setup_arg);
1570 NSAssert(!xdata, @"xdata already initialized");
1573 # undef ya_rand_init
1576 XSetWindowBackground (xdpy, xwindow,
1577 get_pixel_resource (xdpy, 0,
1578 "background", "Background"));
1579 XClearWindow (xdpy, xwindow);
1582 [[self window] setAcceptsMouseMovedEvents:YES];
1585 /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz
1586 drawing primitives will run on the GPU instead of the CPU.
1587 It seems like it might make things worse rather than better,
1588 though... Plus it makes us binary-incompatible with 10.4.
1590 # if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
1591 [[self window] setPreferredBackingLocation:
1592 NSWindowBackingLocationVideoMemory];
1596 /* Kludge: even though the init_cb functions are declared to take 2 args,
1597 actually call them with 3, for the benefit of xlockmore_init() and
1600 void *(*init_cb) (Display *, Window, void *) =
1601 (void *(*) (Display *, Window, void *)) xsft->init_cb;
1603 xdata = init_cb (xdpy, xwindow, xsft->setup_arg);
1604 // NSAssert(xdata, @"no xdata from init");
1605 if (! xdata) abort();
1607 if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
1608 fpst = fps_init (xdpy, xwindow);
1609 fps_cb = xsft->fps_cb;
1610 if (! fps_cb) fps_cb = screenhack_do_fps;
1617 if (current_device_rotation() != 0) // launched while rotated
1621 [self checkForUpdates];
1625 /* I don't understand why we have to do this *every frame*, but we do,
1626 or else the cursor comes back on.
1629 if (![self isPreview])
1630 [NSCursor setHiddenUntilMouseMoves:YES];
1636 /* This is just a guess, but the -fps code wants to know how long
1637 we were sleeping between frames.
1639 long usecs = 1000000 * [self animationTimeInterval];
1640 usecs -= 200; // caller apparently sleeps for slightly less sometimes...
1641 if (usecs < 0) usecs = 0;
1642 fps_slept (fpst, usecs);
1646 /* Run any XtAppAddInput and XtAppAddTimeOut callbacks now.
1647 Do this before delaying for next_frame_time to avoid throttling
1648 timers to the hack's frame rate.
1650 XtAppProcessEvent (XtDisplayToApplicationContext (xdpy),
1651 XtIMTimer | XtIMAlternateInput);
1654 /* It turns out that on some systems (possibly only 10.5 and older?)
1655 [ScreenSaverView setAnimationTimeInterval] does nothing. This means
1656 that we cannot rely on it.
1658 Some of the screen hacks want to delay for long periods, and letting the
1659 framework run the update function at 30 FPS when it really wanted half a
1660 minute between frames would be bad. So instead, we assume that the
1661 framework's animation timer might fire whenever, but we only invoke the
1662 screen hack's "draw frame" method when enough time has expired.
1664 This means two extra calls to gettimeofday() per frame. For fast-cycling
1665 screen savers, that might actually slow them down. Oh well.
1667 A side-effect of this is that it's not possible for a saver to request
1668 an animation interval that is faster than animationTimeInterval.
1670 HOWEVER! On modern systems where setAnimationTimeInterval is *not*
1671 ignored, it's important that it be faster than 30 FPS. 240 FPS is good.
1673 An NSTimer won't fire if the timer is already running the invocation
1674 function from a previous firing. So, if we use a 30 FPS
1675 animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a
1676 frame, there will be a 26666 µs delay until the next frame, 66666 µs
1677 after the beginning of the current frame. In other words, 25 FPS
1680 Frame rates tend to snap to values of 30/N, where N is a positive
1681 integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate
1682 is rounded down from what it would normally be.
1684 So if we set animationTimeInterval to 1/240 instead of 1/30, frame rates
1685 become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate
1686 steps for higher or lower animation time intervals respectively.
1689 gettimeofday (&tv, 0);
1690 double now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1691 if (now < next_frame_time) return;
1693 // [self flushBackbuffer];
1696 // We do this here instead of in setFrame so that all the
1697 // Xlib drawing takes place under the animation timer.
1701 [ogl_ctx setView:self];
1702 # endif // !USE_IPHONE
1711 // NSAssert(xdata, @"no xdata when drawing");
1712 if (! xdata) abort();
1713 unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
1715 fps_cb (xdpy, xwindow, fpst, xdata);
1717 gettimeofday (&tv, 0);
1718 now = tv.tv_sec + (tv.tv_usec / 1000000.0);
1719 next_frame_time = now + (delay / 1000000.0);
1721 # ifdef JWXYZ_QUARTZ
1722 [self drawBackbuffer];
1724 // This can also happen near the beginning of render_x11.
1725 [self flushBackbuffer];
1727 # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt.
1728 if (delay < [self animationTimeInterval])
1729 [self setAnimationTimeInterval:(delay / 1000000.0)];
1732 # ifdef DO_GC_HACKERY
1733 /* Current theory is that the 10.6 garbage collector sucks in the
1736 It only does a collection when a threshold of outstanding
1737 collectable allocations has been surpassed. However, CoreGraphics
1738 creates lots of small collectable allocations that contain pointers
1739 to very large non-collectable allocations: a small CG object that's
1740 collectable referencing large malloc'd allocations (non-collectable)
1741 containing bitmap data. So the large allocation doesn't get freed
1742 until GC collects the small allocation, which triggers its finalizer
1743 to run which frees the large allocation. So GC is deciding that it
1744 doesn't really need to run, even though the process has gotten
1745 enormous. GC eventually runs once pageouts have happened, but by
1746 then it's too late, and the machine's resident set has been
1749 So, we force an exhaustive garbage collection in this process
1750 approximately every 5 seconds whether the system thinks it needs
1754 static int tick = 0;
1755 if (++tick > 5*30) {
1757 objc_collect (OBJC_EXHAUSTIVE_COLLECTION);
1760 # endif // DO_GC_HACKERY
1764 @catch (NSException *e) {
1765 [self handleException: e];
1767 # endif // USE_IPHONE
1771 - (void) animateOneFrame
1773 // Render X11 into the backing store bitmap...
1775 # ifdef JWXYZ_QUARTZ
1776 NSAssert (backbuffer, @"no back buffer");
1779 UIGraphicsPushContext (backbuffer);
1781 # endif // JWXYZ_QUARTZ
1785 # if defined USE_IPHONE && defined JWXYZ_QUARTZ
1786 UIGraphicsPopContext();
1791 # ifndef USE_IPHONE // Doesn't exist on iOS
1793 - (void) setFrame:(NSRect) newRect
1795 [super setFrame:newRect];
1797 if (xwindow) // inform Xlib that the window has changed now.
1801 - (void) setFrameSize:(NSSize) newSize
1803 [super setFrameSize:newSize];
1808 # else // USE_IPHONE
1810 - (void) layoutSubviews
1812 [super layoutSubviews];
1821 +(BOOL) performGammaFade
1826 - (BOOL) hasConfigureSheet
1831 + (NSString *) decompressXML: (NSData *)data
1833 if (! data) return 0;
1834 BOOL compressed_p = !!strncmp ((const char *) data.bytes, "<?xml", 5);
1836 // If it's not already XML, decompress it.
1837 NSAssert (compressed_p, @"xml isn't compressed");
1839 NSMutableData *data2 = 0;
1842 memset (&zs, 0, sizeof(zs));
1843 ret = inflateInit2 (&zs, 16 + MAX_WBITS);
1845 UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4);
1846 data2 = [NSMutableData dataWithLength: usize];
1847 zs.next_in = (Bytef *) data.bytes;
1848 zs.avail_in = (uint) data.length;
1849 zs.next_out = (Bytef *) data2.bytes;
1850 zs.avail_out = (uint) data2.length;
1851 ret = inflate (&zs, Z_FINISH);
1854 if (ret == Z_OK || ret == Z_STREAM_END)
1857 NSAssert2 (0, @"gunzip error: %d: %s",
1858 ret, (zs.msg ? zs.msg : "<null>"));
1861 NSString *s = [[NSString alloc]
1862 initWithData:data encoding:NSUTF8StringEncoding];
1869 - (NSWindow *) configureSheet
1871 - (UIViewController *) configureView
1874 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
1875 NSString *file = [NSString stringWithCString:xsft->progclass
1876 encoding:NSISOLatin1StringEncoding];
1877 file = [file lowercaseString];
1878 NSString *path = [bundle pathForResource:file ofType:@"xml"];
1880 NSLog (@"%@.xml does not exist in the application bundle: %@/",
1881 file, [bundle resourcePath]);
1886 UIViewController *sheet;
1887 # else // !USE_IPHONE
1889 # endif // !USE_IPHONE
1891 NSData *xmld = [NSData dataWithContentsOfFile:path];
1892 NSString *xml = [[self class] decompressXML: xmld];
1893 sheet = [[XScreenSaverConfigSheet alloc]
1894 initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding]
1895 options:xsft->options
1896 controller:[prefsReader userDefaultsController]
1897 globalController:[prefsReader globalDefaultsController]
1898 defaults:[prefsReader defaultOptions]];
1900 // #### am I expected to retain this, or not? wtf.
1901 // I thought not, but if I don't do this, we (sometimes) crash.
1902 // #### Analyze says "potential leak of an object stored into sheet"
1909 - (NSUserDefaultsController *) userDefaultsController
1911 return [prefsReader userDefaultsController];
1915 /* Announce our willingness to accept keyboard input.
1917 - (BOOL)acceptsFirstResponder
1927 # else // USE_IPHONE
1929 // There's no way to play a standard system alert sound!
1930 // We'd have to include our own WAV for that.
1932 // Or we could vibrate:
1933 // #import <AudioToolbox/AudioToolbox.h>
1934 // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate);
1936 // Instead, just flash the screen white, then fade.
1938 UIView *v = [[UIView alloc] initWithFrame: [self frame]];
1939 [v setBackgroundColor: [UIColor whiteColor]];
1940 [[self window] addSubview:v];
1941 [UIView animateWithDuration: 0.1
1942 animations:^{ [v setAlpha: 0.0]; }
1943 completion:^(BOOL finished) { [v removeFromSuperview]; } ];
1945 # endif // USE_IPHONE
1949 /* Send an XEvent to the hack. Returns YES if it was handled.
1951 - (BOOL) sendEvent: (XEvent *) e
1953 if (!initted_p || ![self isAnimating]) // no event handling unless running.
1957 [self prepareContext];
1958 BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e);
1966 /* Convert an NSEvent into an XEvent, and pass it along.
1967 Returns YES if it was handled.
1969 - (BOOL) convertEvent: (NSEvent *) e
1973 memset (&xe, 0, sizeof(xe));
1977 int flags = [e modifierFlags];
1978 if (flags & NSAlphaShiftKeyMask) state |= LockMask;
1979 if (flags & NSShiftKeyMask) state |= ShiftMask;
1980 if (flags & NSControlKeyMask) state |= ControlMask;
1981 if (flags & NSAlternateKeyMask) state |= Mod1Mask;
1982 if (flags & NSCommandKeyMask) state |= Mod2Mask;
1984 NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
1987 double s = [self hackedContentScaleFactor];
1992 int y = s * ([self bounds].size.height - p.y);
1994 xe.xany.type = type;
2000 xe.xbutton.state = state;
2001 if ([e type] == NSScrollWheel)
2002 xe.xbutton.button = ([e deltaY] > 0 ? Button4 :
2003 [e deltaY] < 0 ? Button5 :
2004 [e deltaX] > 0 ? Button6 :
2005 [e deltaX] < 0 ? Button7 :
2008 xe.xbutton.button = (unsigned int) [e buttonNumber] + 1;
2013 xe.xmotion.state = state;
2018 NSString *ns = (([e type] == NSFlagsChanged) ? 0 :
2019 [e charactersIgnoringModifiers]);
2022 if (!ns || [ns length] == 0) // dead key
2024 // Cocoa hides the difference between left and right keys.
2025 // Also we only get KeyPress events for these, no KeyRelease
2026 // (unless we hack the mod state manually. Bleh.)
2028 if (flags & NSAlphaShiftKeyMask) k = XK_Caps_Lock;
2029 else if (flags & NSShiftKeyMask) k = XK_Shift_L;
2030 else if (flags & NSControlKeyMask) k = XK_Control_L;
2031 else if (flags & NSAlternateKeyMask) k = XK_Alt_L;
2032 else if (flags & NSCommandKeyMask) k = XK_Meta_L;
2034 else if ([ns length] == 1) // real key
2036 switch ([ns characterAtIndex:0]) {
2037 case NSLeftArrowFunctionKey: k = XK_Left; break;
2038 case NSRightArrowFunctionKey: k = XK_Right; break;
2039 case NSUpArrowFunctionKey: k = XK_Up; break;
2040 case NSDownArrowFunctionKey: k = XK_Down; break;
2041 case NSPageUpFunctionKey: k = XK_Page_Up; break;
2042 case NSPageDownFunctionKey: k = XK_Page_Down; break;
2043 case NSHomeFunctionKey: k = XK_Home; break;
2044 case NSPrevFunctionKey: k = XK_Prior; break;
2045 case NSNextFunctionKey: k = XK_Next; break;
2046 case NSBeginFunctionKey: k = XK_Begin; break;
2047 case NSEndFunctionKey: k = XK_End; break;
2048 case NSF1FunctionKey: k = XK_F1; break;
2049 case NSF2FunctionKey: k = XK_F2; break;
2050 case NSF3FunctionKey: k = XK_F3; break;
2051 case NSF4FunctionKey: k = XK_F4; break;
2052 case NSF5FunctionKey: k = XK_F5; break;
2053 case NSF6FunctionKey: k = XK_F6; break;
2054 case NSF7FunctionKey: k = XK_F7; break;
2055 case NSF8FunctionKey: k = XK_F8; break;
2056 case NSF9FunctionKey: k = XK_F9; break;
2057 case NSF10FunctionKey: k = XK_F10; break;
2058 case NSF11FunctionKey: k = XK_F11; break;
2059 case NSF12FunctionKey: k = XK_F12; break;
2063 [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
2064 k = (ss && *ss ? *ss : 0);
2070 if (! k) return YES; // E.g., "KeyRelease XK_Shift_L"
2072 xe.xkey.keycode = k;
2073 xe.xkey.state = state;
2077 NSAssert1 (0, @"unknown X11 event type: %d", type);
2081 return [self sendEvent: &xe];
2085 - (void) mouseDown: (NSEvent *) e
2087 if (! [self convertEvent:e type:ButtonPress])
2088 [super mouseDown:e];
2091 - (void) mouseUp: (NSEvent *) e
2093 if (! [self convertEvent:e type:ButtonRelease])
2097 - (void) otherMouseDown: (NSEvent *) e
2099 if (! [self convertEvent:e type:ButtonPress])
2100 [super otherMouseDown:e];
2103 - (void) otherMouseUp: (NSEvent *) e
2105 if (! [self convertEvent:e type:ButtonRelease])
2106 [super otherMouseUp:e];
2109 - (void) mouseMoved: (NSEvent *) e
2111 if (! [self convertEvent:e type:MotionNotify])
2112 [super mouseMoved:e];
2115 - (void) mouseDragged: (NSEvent *) e
2117 if (! [self convertEvent:e type:MotionNotify])
2118 [super mouseDragged:e];
2121 - (void) otherMouseDragged: (NSEvent *) e
2123 if (! [self convertEvent:e type:MotionNotify])
2124 [super otherMouseDragged:e];
2127 - (void) scrollWheel: (NSEvent *) e
2129 if (! [self convertEvent:e type:ButtonPress])
2130 [super scrollWheel:e];
2133 - (void) keyDown: (NSEvent *) e
2135 if (! [self convertEvent:e type:KeyPress])
2139 - (void) keyUp: (NSEvent *) e
2141 if (! [self convertEvent:e type:KeyRelease])
2145 - (void) flagsChanged: (NSEvent *) e
2147 if (! [self convertEvent:e type:KeyPress])
2148 [super flagsChanged:e];
2152 - (NSOpenGLPixelFormat *) getGLPixelFormat
2154 NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat");
2156 NSOpenGLPixelFormatAttribute attrs[40];
2158 attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
2160 /* OpenGL's core profile removes a lot of the same stuff that was removed in
2161 OpenGL ES (e.g. glBegin, glDrawPixels), so it might be a possibility.
2163 opengl_core_p = True;
2164 if (opengl_core_p) {
2165 attrs[i++] = NSOpenGLPFAOpenGLProfile;
2166 attrs[i++] = NSOpenGLProfileVersion3_2Core;
2170 /* Eventually: multisampled pixmaps. May not be supported everywhere.
2171 if (multi_sample_p) {
2172 attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 1;
2173 attrs[i++] = NSOpenGLPFASamples; attrs[i++] = 6;
2177 # ifdef JWXYZ_QUARTZ
2178 // Under Quartz, we're just blitting a texture.
2179 if (double_buffered_p)
2180 attrs[i++] = NSOpenGLPFADoubleBuffer;
2184 /* Under OpenGL, all sorts of drawing commands are being issued, and it might
2185 be a performance problem if this activity occurs on the front buffer.
2186 Also, some screenhacks expect OS X/iOS to always double-buffer.
2187 NSOpenGLPFABackingStore prevents flickering with screenhacks that
2188 don't redraw the entire screen every frame.
2190 attrs[i++] = NSOpenGLPFADoubleBuffer;
2191 attrs[i++] = NSOpenGLPFABackingStore;
2194 attrs[i++] = NSOpenGLPFAWindow;
2196 attrs[i++] = NSOpenGLPFAPixelBuffer;
2197 /* ...But not NSOpenGLPFAFullScreen, because that would be for
2198 [NSOpenGLContext setFullScreen].
2202 /* NSOpenGLPFAFullScreen would go here if initWithFrame's isPreview == NO.
2207 NSOpenGLPixelFormat *p = [[NSOpenGLPixelFormat alloc]
2208 initWithAttributes:attrs];
2216 - (void) stopAndClose
2218 [self stopAndClose:NO];
2222 - (void) stopAndClose:(Bool)relaunch_p
2224 if ([self isAnimating])
2225 [self stopAnimation];
2227 /* Need to make the SaverListController be the firstResponder again
2228 so that it can continue to receive its own shake events. I
2229 suppose that this abstraction-breakage means that I'm adding
2230 XScreenSaverView to the UINavigationController wrong...
2232 // UIViewController *v = [[self window] rootViewController];
2233 // if ([v isKindOfClass: [UINavigationController class]]) {
2234 // UINavigationController *n = (UINavigationController *) v;
2235 // [[n topViewController] becomeFirstResponder];
2237 [self resignFirstResponder];
2239 if (relaunch_p) { // Fake a shake on the SaverListController.
2240 [_delegate didShake:self];
2241 } else { // Not launching another, animate our return to the list.
2242 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
2243 NSLog (@"fading back to saver list");
2245 [_delegate wantsFadeOut:self];
2250 /* We distinguish between taps and drags.
2252 - Drags/pans (down, motion, up) are sent to the saver to handle.
2253 - Single-taps are sent to the saver to handle.
2254 - Double-taps are sent to the saver as a "Space" keypress.
2255 - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
2256 - All taps expose the momentary "Close" button.
2259 - (void)initGestures
2261 UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc]
2263 action:@selector(handleDoubleTap)];
2264 dtap.numberOfTapsRequired = 2;
2265 dtap.numberOfTouchesRequired = 1;
2267 UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
2269 action:@selector(handleTap:)];
2270 stap.numberOfTapsRequired = 1;
2271 stap.numberOfTouchesRequired = 1;
2273 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]
2275 action:@selector(handlePan:)];
2276 pan.maximumNumberOfTouches = 1;
2277 pan.minimumNumberOfTouches = 1;
2279 // I couldn't get Swipe to work, but using a second Pan recognizer works.
2280 UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc]
2282 action:@selector(handlePan2:)];
2283 pan2.maximumNumberOfTouches = 2;
2284 pan2.minimumNumberOfTouches = 2;
2286 // Also handle long-touch, and treat that the same as Pan.
2287 // Without this, panning doesn't start until there's motion, so the trick
2288 // of holding down your finger to freeze the scene doesn't work.
2290 UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc]
2292 action:@selector(handleLongPress:)];
2293 hold.numberOfTapsRequired = 0;
2294 hold.numberOfTouchesRequired = 1;
2295 hold.minimumPressDuration = 0.25; /* 1/4th second */
2297 // Two finger pinch to zoom in on the view.
2298 UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]
2300 action:@selector(handlePinch:)];
2302 [stap requireGestureRecognizerToFail: dtap];
2303 [stap requireGestureRecognizerToFail: hold];
2304 [dtap requireGestureRecognizerToFail: hold];
2305 [pan requireGestureRecognizerToFail: hold];
2306 [pan2 requireGestureRecognizerToFail: pinch];
2308 [self setMultipleTouchEnabled:YES];
2310 [self addGestureRecognizer: dtap];
2311 [self addGestureRecognizer: stap];
2312 [self addGestureRecognizer: pan];
2313 [self addGestureRecognizer: pan2];
2314 [self addGestureRecognizer: hold];
2315 [self addGestureRecognizer: pinch];
2326 /* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates,
2327 convert it to what X11 and OpenGL expect.
2329 Getting this crap right is tricky, given the confusion of the various
2330 scale factors, so here's a checklist that I think covers all of the X11
2331 and OpenGL cases. For each of these: rotate to all 4 orientations;
2332 ensure the mouse tracks properly to all 4 corners.
2334 Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator.
2336 Test hacks must cover:
2337 X11 ignoreRotation = true
2338 X11 ignoreRotation = false
2339 OpenGL (rotation is handled manually, so they never ignoreRotation)
2341 Test devices must cover:
2342 contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2)
2343 contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air)
2344 contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+)
2346 iPad 2: 768x1024 / 1 = 768x1024
2347 iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical)
2348 iPhone 4s: 640x960 / 2 = 320x480
2349 iPhone 5: 640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical)
2350 iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3)
2353 iPad2 iPadAir iPhone4s iPhone5 iPhone6+
2354 Attraction X yes - - - - Y
2355 Fireworkx X no - - - - Y
2356 Carousel GL yes - - - - Y
2357 Voronoi GL no - - - - -
2359 - (void) convertMouse:(CGPoint *)p
2361 CGFloat xx = p->x, yy = p->y;
2363 # if 0 // TARGET_IPHONE_SIMULATOR
2365 XWindowAttributes xgwa;
2366 XGetWindowAttributes (xdpy, xwindow, &xgwa);
2367 NSLog (@"TOUCH %4g, %-4g in %4d x %-4d cs=%.0f hcs=%.0f r=%d ig=%d\n",
2369 xgwa.width, xgwa.height,
2370 [self contentScaleFactor],
2371 [self hackedContentScaleFactor],
2372 [self rotateTouches], [self ignoreRotation]);
2374 # endif // TARGET_IPHONE_SIMULATOR
2376 if ([self rotateTouches]) {
2378 // The XScreenSaverGLView case:
2379 // The X11 window is rotated, as is the framebuffer.
2380 // The device coordinates match the framebuffer dimensions,
2381 // but might have axes swapped... and we need to swap them
2384 int w = [self frame].size.width;
2385 int h = [self frame].size.height;
2386 GLfloat xr = (GLfloat) xx / w;
2387 GLfloat yr = (GLfloat) yy / h;
2389 int o = (int) current_device_rotation();
2391 case -90: case 270: swap = xr; xr = 1-yr; yr = swap; break;
2392 case 90: case -270: swap = xr; xr = yr; yr = 1-swap; break;
2393 case 180: case -180: xr = 1-xr; yr = 1-yr; break;
2399 } else if ([self ignoreRotation]) {
2401 // The X11 case, where the hack has opted not to rotate:
2402 // The X11 window is unrotated, but the framebuffer is rotated.
2403 // The device coordinates match the framebuffer, so they need to
2404 // be de-rotated to match the X11 window.
2406 int w = [self frame].size.width;
2407 int h = [self frame].size.height;
2409 int o = (int) current_device_rotation();
2411 case -90: case 270: swap = xx; xx = h-yy; yy = swap; break;
2412 case 90: case -270: swap = xx; xx = yy; yy = w-swap; break;
2413 case 180: case -180: xx = w-xx; yy = h-yy; break;
2418 double s = [self hackedContentScaleFactor];
2422 # if 0 // TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
2424 XWindowAttributes xgwa;
2425 XGetWindowAttributes (xdpy, xwindow, &xgwa);
2426 NSLog (@"touch %4g, %-4g in %4d x %-4d cs=%.0f hcs=%.0f r=%d ig=%d\n",
2428 xgwa.width, xgwa.height,
2429 [self contentScaleFactor],
2430 [self hackedContentScaleFactor],
2431 [self rotateTouches], [self ignoreRotation]);
2432 if (p->x < 0 || p->y < 0 || p->x > xgwa.width || p->y > xgwa.height)
2435 # endif // TARGET_IPHONE_SIMULATOR
2439 /* Single click exits saver.
2441 - (void) handleTap:(UIGestureRecognizer *)sender
2447 memset (&xe, 0, sizeof(xe));
2449 [self showCloseButton];
2451 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2452 [self convertMouse:&p];
2453 NSAssert (xwindow->type == WINDOW, @"not a window");
2454 xwindow->window.last_mouse_x = p.x;
2455 xwindow->window.last_mouse_y = p.y;
2457 xe.xany.type = ButtonPress;
2458 xe.xbutton.button = 1;
2462 if (! [self sendEvent: &xe])
2465 xe.xany.type = ButtonRelease;
2466 xe.xbutton.button = 1;
2470 [self sendEvent: &xe];
2474 /* Double click sends Space KeyPress.
2476 - (void) handleDoubleTap
2478 if (!xsft->event_cb || !xwindow) return;
2480 [self showCloseButton];
2483 memset (&xe, 0, sizeof(xe));
2484 xe.xkey.keycode = ' ';
2485 xe.xany.type = KeyPress;
2486 BOOL ok1 = [self sendEvent: &xe];
2487 xe.xany.type = KeyRelease;
2488 BOOL ok2 = [self sendEvent: &xe];
2494 /* Drag with one finger down: send MotionNotify.
2496 - (void) handlePan:(UIGestureRecognizer *)sender
2498 if (!xsft->event_cb || !xwindow) return;
2500 [self showCloseButton];
2503 memset (&xe, 0, sizeof(xe));
2505 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2506 [self convertMouse:&p];
2507 NSAssert (xwindow && xwindow->type == WINDOW, @"not a window");
2508 xwindow->window.last_mouse_x = p.x;
2509 xwindow->window.last_mouse_y = p.y;
2511 switch (sender.state) {
2512 case UIGestureRecognizerStateBegan:
2513 xe.xany.type = ButtonPress;
2514 xe.xbutton.button = 1;
2519 case UIGestureRecognizerStateEnded:
2520 xe.xany.type = ButtonRelease;
2521 xe.xbutton.button = 1;
2526 case UIGestureRecognizerStateChanged:
2527 xe.xany.type = MotionNotify;
2536 BOOL ok = [self sendEvent: &xe];
2537 if (!ok && xe.xany.type == ButtonRelease)
2542 /* Hold one finger down: assume we're about to start dragging.
2543 Treat the same as Pan.
2545 - (void) handleLongPress:(UIGestureRecognizer *)sender
2547 [self handlePan:sender];
2552 /* Drag with 2 fingers down: send arrow keys.
2554 - (void) handlePan2:(UIPanGestureRecognizer *)sender
2556 if (!xsft->event_cb || !xwindow) return;
2558 [self showCloseButton];
2560 if (sender.state != UIGestureRecognizerStateEnded)
2564 memset (&xe, 0, sizeof(xe));
2566 CGPoint p = [sender locationInView:self]; // this is in points, not pixels
2567 [self convertMouse:&p];
2569 if (fabs(p.x) > fabs(p.y))
2570 xe.xkey.keycode = (p.x > 0 ? XK_Right : XK_Left);
2572 xe.xkey.keycode = (p.y > 0 ? XK_Down : XK_Up);
2574 BOOL ok1 = [self sendEvent: &xe];
2575 xe.xany.type = KeyRelease;
2576 BOOL ok2 = [self sendEvent: &xe];
2582 /* Pinch with 2 fingers: zoom in around the center of the fingers.
2584 - (void) handlePinch:(UIPinchGestureRecognizer *)sender
2586 if (!xsft->event_cb || !xwindow) return;
2588 [self showCloseButton];
2590 if (sender.state == UIGestureRecognizerStateBegan)
2591 pinch_transform = self.transform; // Save the base transform
2593 switch (sender.state) {
2594 case UIGestureRecognizerStateBegan:
2595 case UIGestureRecognizerStateChanged:
2597 double scale = sender.scale;
2602 self.transform = CGAffineTransformScale (pinch_transform, scale, scale);
2604 CGPoint p = [sender locationInView: self];
2605 p.x /= self.layer.bounds.size.width;
2606 p.y /= self.layer.bounds.size.height;
2608 CGPoint np = CGPointMake (self.bounds.size.width * p.x,
2609 self.bounds.size.height * p.y);
2610 CGPoint op = CGPointMake (self.bounds.size.width *
2611 self.layer.anchorPoint.x,
2612 self.bounds.size.height *
2613 self.layer.anchorPoint.y);
2614 np = CGPointApplyAffineTransform (np, self.transform);
2615 op = CGPointApplyAffineTransform (op, self.transform);
2617 CGPoint pos = self.layer.position;
2622 self.layer.position = pos;
2623 self.layer.anchorPoint = p;
2627 case UIGestureRecognizerStateEnded:
2629 // When released, snap back to the default zoom (but animate it).
2631 CABasicAnimation *a1 = [CABasicAnimation
2632 animationWithKeyPath:@"position.x"];
2633 a1.fromValue = [NSNumber numberWithFloat: self.layer.position.x];
2634 a1.toValue = [NSNumber numberWithFloat: self.bounds.size.width / 2];
2636 CABasicAnimation *a2 = [CABasicAnimation
2637 animationWithKeyPath:@"position.y"];
2638 a2.fromValue = [NSNumber numberWithFloat: self.layer.position.y];
2639 a2.toValue = [NSNumber numberWithFloat: self.bounds.size.height / 2];
2641 CABasicAnimation *a3 = [CABasicAnimation
2642 animationWithKeyPath:@"anchorPoint.x"];
2643 a3.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.x];
2644 a3.toValue = [NSNumber numberWithFloat: 0.5];
2646 CABasicAnimation *a4 = [CABasicAnimation
2647 animationWithKeyPath:@"anchorPoint.y"];
2648 a4.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.y];
2649 a4.toValue = [NSNumber numberWithFloat: 0.5];
2651 CABasicAnimation *a5 = [CABasicAnimation
2652 animationWithKeyPath:@"transform.scale"];
2653 a5.fromValue = [NSNumber numberWithFloat: sender.scale];
2654 a5.toValue = [NSNumber numberWithFloat: 1.0];
2656 CAAnimationGroup *group = [CAAnimationGroup animation];
2657 group.duration = 0.3;
2658 group.repeatCount = 1;
2659 group.autoreverses = NO;
2660 group.animations = @[ a1, a2, a3, a4, a5 ];
2661 group.timingFunction = [CAMediaTimingFunction
2663 kCAMediaTimingFunctionEaseIn];
2664 [self.layer addAnimation:group forKey:@"unpinch"];
2666 self.transform = pinch_transform;
2667 self.layer.anchorPoint = CGPointMake (0.5, 0.5);
2668 self.layer.position = CGPointMake (self.bounds.size.width / 2,
2669 self.bounds.size.height / 2);
2678 /* We need this to respond to "shake" gestures
2680 - (BOOL)canBecomeFirstResponder
2685 - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
2690 - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
2694 /* Shake means exit and launch a new saver.
2696 - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
2698 [self stopAndClose:YES];
2702 - (void) showCloseButton
2709 int width = self.bounds.size.width;
2710 closeBox = [[UIView alloc]
2711 initWithFrame:CGRectMake(0, 0, width, ih + off)];
2712 closeBox.backgroundColor = [UIColor clearColor];
2713 closeBox.autoresizingMask =
2714 UIViewAutoresizingFlexibleBottomMargin |
2715 UIViewAutoresizingFlexibleWidth;
2717 // Add the buttons to the bar
2718 UIImage *img1 = [UIImage imageNamed:@"stop"];
2719 UIImage *img2 = [UIImage imageNamed:@"settings"];
2721 UIButton *button = [[UIButton alloc] init];
2722 [button setFrame: CGRectMake(off, off, iw, ih)];
2723 [button setBackgroundImage:img1 forState:UIControlStateNormal];
2724 [button addTarget:self
2725 action:@selector(stopAndClose)
2726 forControlEvents:UIControlEventTouchUpInside];
2727 [closeBox addSubview:button];
2730 button = [[UIButton alloc] init];
2731 [button setFrame: CGRectMake(width - iw - off, off, iw, ih)];
2732 [button setBackgroundImage:img2 forState:UIControlStateNormal];
2733 [button addTarget:self
2734 action:@selector(stopAndOpenSettings)
2735 forControlEvents:UIControlEventTouchUpInside];
2736 button.autoresizingMask =
2737 UIViewAutoresizingFlexibleBottomMargin |
2738 UIViewAutoresizingFlexibleLeftMargin;
2739 [closeBox addSubview:button];
2742 [self addSubview:closeBox];
2745 if (closeBox.layer.opacity <= 0) { // Fade in
2747 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
2748 anim.duration = 0.2;
2749 anim.repeatCount = 1;
2750 anim.autoreverses = NO;
2751 anim.fromValue = [NSNumber numberWithFloat:0.0];
2752 anim.toValue = [NSNumber numberWithFloat:1.0];
2753 [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
2754 closeBox.layer.opacity = 1;
2757 // Fade out N seconds from now.
2759 [closeBoxTimer invalidate];
2760 closeBoxTimer = [NSTimer scheduledTimerWithTimeInterval: 3
2762 selector:@selector(closeBoxOff)
2770 if (closeBoxTimer) {
2771 [closeBoxTimer invalidate];
2777 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
2778 anim.duration = 0.2;
2779 anim.repeatCount = 1;
2780 anim.autoreverses = NO;
2781 anim.fromValue = [NSNumber numberWithFloat: 1];
2782 anim.toValue = [NSNumber numberWithFloat: 0];
2783 [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
2784 closeBox.layer.opacity = 0;
2788 - (void) stopAndOpenSettings
2790 NSString *s = [NSString stringWithCString:xsft->progclass
2791 encoding:NSISOLatin1StringEncoding];
2792 if ([self isAnimating])
2793 [self stopAnimation];
2794 [self resignFirstResponder];
2795 [_delegate wantsFadeOut:self];
2796 [_delegate openPreferences: s];
2801 - (void)setScreenLocked:(BOOL)locked
2803 if (screenLocked == locked) return;
2804 screenLocked = locked;
2806 if ([self isAnimating])
2807 [self stopAnimation];
2809 if (! [self isAnimating])
2810 [self startAnimation];
2814 - (NSDictionary *)getGLProperties
2816 return [NSDictionary dictionaryWithObjectsAndKeys:
2817 kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
2819 /* This could be disabled if we knew the screen would be redrawn
2820 entirely for every frame.
2822 [NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking,
2827 - (void)addExtraRenderbuffers:(CGSize)size
2829 // No extra renderbuffers are needed for 2D screenhacks.
2833 - (NSString *)getCAGravity
2835 return kCAGravityCenter; // Looks better in e.g. Compass.
2836 // return kCAGravityBottomLeft;
2839 #endif // USE_IPHONE
2842 - (void) checkForUpdates
2845 // We only check once at startup, even if there are multiple screens,
2846 // and even if this saver is running for many days.
2847 // (Uh, except this doesn't work because this static isn't shared,
2848 // even if we make it an exported global. Not sure why. Oh well.)
2849 static BOOL checked_p = NO;
2850 if (checked_p) return;
2853 // If it's off, don't bother running the updater. Otherwise, the
2854 // updater will decide if it's time to hit the network.
2855 if (! get_boolean_resource (xdpy,
2856 SUSUEnableAutomaticChecksKey,
2857 SUSUEnableAutomaticChecksKey))
2860 NSString *updater = @"XScreenSaverUpdater.app";
2862 // There may be multiple copies of the updater: e.g., one in /Applications
2863 // and one in the mounted installer DMG! It's important that we run the
2864 // one from the disk and not the DMG, so search for the right one.
2866 NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
2867 NSBundle *bundle = [NSBundle bundleForClass:[self class]];
2869 @[[[bundle bundlePath] stringByDeletingLastPathComponent],
2870 [@"~/Library/Screen Savers" stringByExpandingTildeInPath],
2871 @"/Library/Screen Savers",
2872 @"/System/Library/Screen Savers",
2874 @"/Applications/Utilities"];
2875 NSString *app_path = nil;
2876 for (NSString *dir in search) {
2877 NSString *p = [dir stringByAppendingPathComponent:updater];
2878 if ([[NSFileManager defaultManager] fileExistsAtPath:p]) {
2885 app_path = [workspace fullPathForApplication:updater];
2887 if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "])
2888 app_path = 0; // The DMG version will not do.
2891 NSLog(@"Unable to find %@", updater);
2896 if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path]
2897 options:(NSWorkspaceLaunchWithoutAddingToRecents |
2898 NSWorkspaceLaunchWithoutActivation |
2899 NSWorkspaceLaunchAndHide)
2900 configuration:[NSMutableDictionary dictionary]
2902 NSLog(@"Unable to launch %@: %@", app_path, err);
2905 # endif // !USE_IPHONE
2911 /* Utility functions...
2914 static PrefsReader *
2915 get_prefsReader (Display *dpy)
2917 XScreenSaverView *view = jwxyz_window_view (XRootWindow (dpy, 0));
2918 if (!view) return 0;
2919 return [view prefsReader];
2924 get_string_resource (Display *dpy, char *name, char *class)
2926 return [get_prefsReader(dpy) getStringResource:name];
2930 get_boolean_resource (Display *dpy, char *name, char *class)
2932 return [get_prefsReader(dpy) getBooleanResource:name];
2936 get_integer_resource (Display *dpy, char *name, char *class)
2938 return [get_prefsReader(dpy) getIntegerResource:name];
2942 get_float_resource (Display *dpy, char *name, char *class)
2944 return [get_prefsReader(dpy) getFloatResource:name];