X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=OSX%2FXScreenSaverView.m;h=657835b9c9b1e5ebe9eeeb4e045cd616b2c75f6a;hb=88cfe534a698a0562e81345957a50714af1453bc;hp=438c05f9c712922a7c8ef8087477df1c508088c1;hpb=6f5482d73adb0165c0130bb47d852644ab0c4869;p=xscreensaver diff --git a/OSX/XScreenSaverView.m b/OSX/XScreenSaverView.m index 438c05f9..657835b9 100644 --- a/OSX/XScreenSaverView.m +++ b/OSX/XScreenSaverView.m @@ -1,13 +1,13 @@ -/* xscreensaver, Copyright (c) 2006-2012 Jamie Zawinski -* -* Permission to use, copy, modify, distribute, and sell this software and its -* documentation for any purpose is hereby granted without fee, provided that -* the above copyright notice appear in all copies and that both that -* copyright notice and this permission notice appear in supporting -* documentation. No representations are made about the suitability of this -* software for any purpose. It is provided "as is" without express or -* implied warranty. -*/ +/* xscreensaver, Copyright (c) 2006-2015 Jamie Zawinski + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + */ /* This is a subclass of Apple's ScreenSaverView that knows how to run xscreensaver programs without X11 via the dark magic of the "jwxyz" @@ -16,12 +16,19 @@ */ #import +#import +#import #import "XScreenSaverView.h" #import "XScreenSaverConfigSheet.h" +#import "Updater.h" #import "screenhackI.h" #import "xlockmoreI.h" #import "jwxyz-timers.h" +#ifndef USE_IPHONE +# import +#endif + /* Garbage collection only exists if we are being compiled against the 10.6 SDK or newer, not if we are building against the 10.4 SDK. */ @@ -33,6 +40,10 @@ # define DO_GC_HACKERY #endif +/* Duplicated in xlockmoreI.h and XScreenSaverGLView.m. */ +extern void clear_gl_error (void); +extern void check_gl_error (const char *type); + extern struct xscreensaver_function_table *xscreensaver_function_table; /* Global variables used by the screen savers @@ -44,6 +55,10 @@ int mono_p = 0; # ifdef USE_IPHONE +# define NSSizeToCGSize(x) (x) + +extern NSDictionary *make_function_table_dict(void); // ios-function-table.m + /* Stub definition of the superclass, for iPhone. */ @implementation ScreenSaverView @@ -92,7 +107,6 @@ int mono_p = 0; @interface XScreenSaverView (Private) -- (void) stopAndClose; - (void) stopAndClose:(Bool)relaunch; @end @@ -114,21 +128,34 @@ int mono_p = 0; CFBundleRef cfb = CFBundleCreate (kCFAllocatorDefault, url); CFRelease (url); NSAssert1 (cfb, @"no CFBundle for \"%@\"", path); + // #### Analyze says "Potential leak of an object stored into cfb" if (! name) name = [[path lastPathComponent] stringByDeletingPathExtension]; - NSString *table_name = [[[name lowercaseString] - stringByReplacingOccurrencesOfString:@" " - withString:@""] - stringByAppendingString: - @"_xscreensaver_function_table"]; + name = [[name lowercaseString] + stringByReplacingOccurrencesOfString:@" " + withString:@""]; + +# ifndef USE_IPHONE + // CFBundleGetDataPointerForName doesn't work in "Archive" builds. + // I'm guessing that symbol-stripping is mandatory. Fuck. + NSString *table_name = [name stringByAppendingString: + @"_xscreensaver_function_table"]; void *addr = CFBundleGetDataPointerForName (cfb, (CFStringRef) table_name); CFRelease (cfb); if (! addr) NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path); +# else // USE_IPHONE + // Depends on the auto-generated "ios-function-table.m" being up to date. + if (! function_tables) + function_tables = [make_function_table_dict() retain]; + NSValue *v = [function_tables objectForKey: name]; + void *addr = v ? [v pointerValue] : 0; +# endif // USE_IPHONE + return (struct xscreensaver_function_table *) addr; } @@ -204,6 +231,29 @@ add_default_options (const XrmOptionDescRec *opts, { "-image-directory", ".imageDirectory", XrmoptionSepArg, 0 }, { "-fps", ".doFPS", XrmoptionNoArg, "True" }, { "-no-fps", ".doFPS", XrmoptionNoArg, "False"}, + { "-foreground", ".foreground", XrmoptionSepArg, 0 }, + { "-fg", ".foreground", XrmoptionSepArg, 0 }, + { "-background", ".background", XrmoptionSepArg, 0 }, + { "-bg", ".background", XrmoptionSepArg, 0 }, + +# ifndef USE_IPHONE + // + { "-" SUSUEnableAutomaticChecksKey, + "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "True" }, + { "-no-" SUSUEnableAutomaticChecksKey, + "." SUSUEnableAutomaticChecksKey, XrmoptionNoArg, "False" }, + { "-" SUAutomaticallyUpdateKey, + "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "True" }, + { "-no-" SUAutomaticallyUpdateKey, + "." SUAutomaticallyUpdateKey, XrmoptionNoArg, "False" }, + { "-" SUSendProfileInfoKey, + "." SUSendProfileInfoKey, XrmoptionNoArg,"True" }, + { "-no-" SUSendProfileInfoKey, + "." SUSendProfileInfoKey, XrmoptionNoArg,"False"}, + { "-" SUScheduledCheckIntervalKey, + "." SUScheduledCheckIntervalKey, XrmoptionSepArg, 0 }, +# endif // !USE_IPHONE + { 0, 0, 0, 0 } }; static const char *default_defaults [] = { @@ -217,7 +267,7 @@ add_default_options (const XrmOptionDescRec *opts, # endif // ".textLiteral: ", // ".textFile: ", - ".textURL: http://twitter.com/statuses/public_timeline.atom", + ".textURL: https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss", // ".textProgram: ", ".grabDesktopImages: yes", # ifndef USE_IPHONE @@ -227,6 +277,22 @@ add_default_options (const XrmOptionDescRec *opts, # endif ".imageDirectory: ~/Pictures", ".relaunchDelay: 2", + ".texFontCacheSize: 30", + +# ifndef USE_IPHONE +# define STR1(S) #S +# define STR(S) STR1(S) +# define __objc_yes Yes +# define __objc_no No + "." SUSUEnableAutomaticChecksKey ": " STR(SUSUEnableAutomaticChecksDef), + "." SUAutomaticallyUpdateKey ": " STR(SUAutomaticallyUpdateDef), + "." SUSendProfileInfoKey ": " STR(SUSendProfileInfoDef), + "." SUScheduledCheckIntervalKey ": " STR(SUScheduledCheckIntervalDef), +# undef __objc_yes +# undef __objc_no +# undef STR1 +# undef STR +# endif // USE_IPHONE 0 }; @@ -298,18 +364,28 @@ double_time (void) } #endif // USE_IPHONE +#if TARGET_IPHONE_SIMULATOR +static const char * +orientname(unsigned long o) +{ + switch (o) { + case UIDeviceOrientationUnknown: return "Unknown"; + case UIDeviceOrientationPortrait: return "Portrait"; + case UIDeviceOrientationPortraitUpsideDown: return "PortraitUpsideDown"; + case UIDeviceOrientationLandscapeLeft: return "LandscapeLeft"; + case UIDeviceOrientationLandscapeRight: return "LandscapeRight"; + case UIDeviceOrientationFaceUp: return "FaceUp"; + case UIDeviceOrientationFaceDown: return "FaceDown"; + default: return "ERROR"; + } +} +#endif // TARGET_IPHONE_SIMULATOR + - (id) initWithFrame:(NSRect)frame saverName:(NSString *)saverName isPreview:(BOOL)isPreview { -# ifdef USE_IPHONE - rot_current_size = frame.size; // needs to be early, because - rot_from = rot_current_size; // [self setFrame] is called by - rot_to = rot_current_size; // [super initWithFrame]. - rotation_ratio = -1; -# endif - if (! (self = [super initWithFrame:frame isPreview:isPreview])) return 0; @@ -321,12 +397,6 @@ double_time (void) [self setShellPath]; -# ifdef USE_IPHONE - [self setMultipleTouchEnabled:YES]; - orientation = UIDeviceOrientationUnknown; - [self didRotate:nil]; -# endif // USE_IPHONE - setup_p = YES; if (xsft->setup_cb) xsft->setup_cb (xsft, xsft->setup_arg); @@ -353,17 +423,55 @@ double_time (void) progname = progclass = xsft->progclass; next_frame_time = 0; - + +# ifndef USE_IPHONE + // When the view fills the screen and double buffering is enabled, OS X will + // use page flipping for a minor CPU/FPS boost. In windowed mode, double + // buffering reduces the frame rate to 1/2 the screen's refresh rate. + double_buffered_p = !isPreview; +# endif + +# ifdef USE_IPHONE + double s = [self hackedContentScaleFactor]; +# else + double s = 1; +# endif + + CGSize bb_size; // pixels, not points + bb_size.width = s * frame.size.width; + bb_size.height = s * frame.size.height; + # ifdef USE_IPHONE - [self createBackbuffer]; + initial_bounds = rot_current_size = rot_from = rot_to = bb_size; + rotation_ratio = -1; + + orientation = UIDeviceOrientationUnknown; + [self didRotate:nil]; + [self initGestures]; // So we can tell when we're docked. [UIDevice currentDevice].batteryMonitoringEnabled = YES; + + [self setBackgroundColor:[NSColor blackColor]]; + + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(didRotate:) + name:UIDeviceOrientationDidChangeNotification object:nil]; # endif // USE_IPHONE return self; } + +#ifdef USE_IPHONE ++ (Class) layerClass +{ + return [CAEAGLLayer class]; +} +#endif + + - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p { return [self initWithFrame:frame saverName:0 isPreview:p]; @@ -372,16 +480,28 @@ double_time (void) - (void) dealloc { - NSAssert(![self isAnimating], @"still animating"); + if ([self isAnimating]) + [self stopAnimation]; NSAssert(!xdata, @"xdata not yet freed"); - if (xdpy) - jwxyz_free_display (xdpy); + NSAssert(!xdpy, @"xdpy not yet freed"); # ifdef USE_IPHONE - if (backbuffer) - CGContextRelease (backbuffer); + [[NSNotificationCenter defaultCenter] removeObserver:self]; # endif +# ifdef USE_BACKBUFFER + +# ifdef BACKBUFFER_OPENGL + [ogl_ctx release]; + // Releasing the OpenGL context should also free any OpenGL objects, + // including the backbuffer texture and frame/render/depthbuffers. +# endif // BACKBUFFER_OPENGL + + if (colorspace) + CGColorSpaceRelease (colorspace); + +# endif // USE_BACKBUFFER + [prefsReader release]; // xsft @@ -424,6 +544,10 @@ double_time (void) { NSAssert(![self isAnimating], @"already animating"); NSAssert(!initted_p && !xdata, @"already initialized"); + + // See comment in render_x11() for why this value is important: + [self setAnimationTimeInterval: 1.0 / 240.0]; + [super startAnimation]; /* We can't draw on the window from this method, so we actually do the initialization of the screen saver (xsft->init_cb) in the first call @@ -431,6 +555,16 @@ double_time (void) */ # ifdef USE_IPHONE + { + CGSize b = self.bounds.size; + double s = [self hackedContentScaleFactor]; + b.width *= s; + b.height *= s; + NSAssert (initial_bounds.width == b.width && + initial_bounds.height == b.height, + @"bounds changed unexpectedly"); + } + if (crash_timer) [crash_timer invalidate]; @@ -443,6 +577,7 @@ double_time (void) selector:@selector(allSystemsGo:) userInfo:nil repeats:NO]; + # endif // USE_IPHONE // Never automatically turn the screen off if we are docked, @@ -451,9 +586,168 @@ double_time (void) # ifdef USE_IPHONE [UIApplication sharedApplication].idleTimerDisabled = ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged); + [[UIApplication sharedApplication] + setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone]; # endif -} +#ifdef BACKBUFFER_OPENGL + CGSize new_backbuffer_size; + + { +# ifndef USE_IPHONE + if (!ogl_ctx) { + + NSOpenGLPixelFormat *pixfmt = [self getGLPixelFormat]; + + NSAssert (pixfmt, @"unable to create NSOpenGLPixelFormat"); + + [pixfmt retain]; // #### ??? + + // Fun: On OS X 10.7, the second time an OpenGL context is created, after + // the preferences dialog is launched in SaverTester, the context only + // lasts until the first full GC. Then it turns black. Solution is to + // reuse the OpenGL context after this point. + ogl_ctx = [[NSOpenGLContext alloc] initWithFormat:pixfmt + shareContext:nil]; + + // Sync refreshes to the vertical blanking interval + GLint r = 1; + [ogl_ctx setValues:&r forParameter:NSOpenGLCPSwapInterval]; +// check_gl_error ("NSOpenGLCPSwapInterval"); // SEGV sometimes. Too early? + } + + [ogl_ctx makeCurrentContext]; + check_gl_error ("makeCurrentContext"); + + // NSOpenGLContext logs an 'invalid drawable' when this is called + // from initWithFrame. + [ogl_ctx setView:self]; + + // Clear frame buffer ASAP, else there are bits left over from other apps. + glClearColor (0, 0, 0, 1); + glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +// glFinish (); +// glXSwapBuffers (mi->dpy, mi->window); + + + // Enable multi-threading, if possible. This runs most OpenGL commands + // and GPU management on a second CPU. + { +# ifndef kCGLCEMPEngine +# define kCGLCEMPEngine 313 // Added in MacOS 10.4.8 + XCode 2.4. +# endif + CGLContextObj cctx = CGLGetCurrentContext(); + CGLError err = CGLEnable (cctx, kCGLCEMPEngine); + if (err != kCGLNoError) { + NSLog (@"enabling multi-threaded OpenGL failed: %d", err); + } + } + + new_backbuffer_size = NSSizeToCGSize ([self bounds].size); + +# else // USE_IPHONE + if (!ogl_ctx) { + CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer; + eagl_layer.opaque = TRUE; + eagl_layer.drawableProperties = [self getGLProperties]; + + // Without this, the GL frame buffer is half the screen resolution! + eagl_layer.contentsScale = [UIScreen mainScreen].scale; + + ogl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1]; + } + + [EAGLContext setCurrentContext: ogl_ctx]; + + CGSize screen_size = [[[UIScreen mainScreen] currentMode] size]; + // iPad, simulator: 768x1024 + // iPad, physical: 1024x768 + if (screen_size.width > screen_size.height) { + CGFloat w = screen_size.width; + screen_size.width = screen_size.height; + screen_size.height = w; + } + + if (gl_framebuffer) glDeleteFramebuffersOES (1, &gl_framebuffer); + if (gl_renderbuffer) glDeleteRenderbuffersOES (1, &gl_renderbuffer); + + glGenFramebuffersOES (1, &gl_framebuffer); + glBindFramebufferOES (GL_FRAMEBUFFER_OES, gl_framebuffer); + + glGenRenderbuffersOES (1, &gl_renderbuffer); + glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer); + +// redundant? +// glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES, +// (int)size.width, (int)size.height); + [ogl_ctx renderbufferStorage:GL_RENDERBUFFER_OES + fromDrawable:(CAEAGLLayer*)self.layer]; + + glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, + GL_RENDERBUFFER_OES, gl_renderbuffer); + + [self addExtraRenderbuffers:screen_size]; + + int err = glCheckFramebufferStatusOES (GL_FRAMEBUFFER_OES); + switch (err) { + case GL_FRAMEBUFFER_COMPLETE_OES: + break; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES: + NSAssert (0, @"framebuffer incomplete attachment"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES: + NSAssert (0, @"framebuffer incomplete missing attachment"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES: + NSAssert (0, @"framebuffer incomplete dimensions"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES: + NSAssert (0, @"framebuffer incomplete formats"); + break; + case GL_FRAMEBUFFER_UNSUPPORTED_OES: + NSAssert (0, @"framebuffer unsupported"); + break; +/* + case GL_FRAMEBUFFER_UNDEFINED: + NSAssert (0, @"framebuffer undefined"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: + NSAssert (0, @"framebuffer incomplete draw buffer"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: + NSAssert (0, @"framebuffer incomplete read buffer"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: + NSAssert (0, @"framebuffer incomplete multisample"); + break; + case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: + NSAssert (0, @"framebuffer incomplete layer targets"); + break; + */ + default: + NSAssert (0, @"framebuffer incomplete, unknown error 0x%04X", err); + break; + } + + glViewport (0, 0, screen_size.width, screen_size.height); + + new_backbuffer_size = initial_bounds; + +# endif // USE_IPHONE + + check_gl_error ("startAnimation"); + +// NSLog (@"%s / %s / %s\n", glGetString (GL_VENDOR), +// glGetString (GL_RENDERER), glGetString (GL_VERSION)); + + [self enableBackbuffer:new_backbuffer_size]; + } +#endif // BACKBUFFER_OPENGL + +#ifdef USE_BACKBUFFER + [self createBackbuffer:new_backbuffer_size]; +#endif +} - (void)stopAnimation { @@ -473,6 +767,12 @@ double_time (void) xsft->free_cb (xdpy, xwindow, xdata); [self unlockFocus]; + // xdpy must be freed before dealloc is called, because xdpy owns a + // circular reference to the parent XScreenSaverView. + jwxyz_free_display (xdpy); + xdpy = NULL; + xwindow = NULL; + // setup_p = NO; // #### wait, do we need this? initted_p = NO; xdata = 0; @@ -494,20 +794,41 @@ double_time (void) // # ifdef USE_IPHONE [UIApplication sharedApplication].idleTimerDisabled = NO; + [[UIApplication sharedApplication] + setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone]; # endif -} + // Without this, the GL frame stays on screen when switching tabs + // in System Preferences. + // (Or perhaps it used to. It doesn't seem to matter on 10.9.) + // +# ifndef USE_IPHONE + [NSOpenGLContext clearCurrentContext]; +# endif // !USE_IPHONE -/* Hook for the XScreenSaverGLView subclass - */ -- (void) prepareContext -{ + clear_gl_error(); // This hack is defunct, don't let this linger. + + CGContextRelease (backbuffer); + backbuffer = nil; + + if (backbuffer_len) + munmap (backbuffer_data, backbuffer_len); + backbuffer_data = NULL; + backbuffer_len = 0; } -/* Hook for the XScreenSaverGLView subclass - */ -- (void) resizeContext + +// #### maybe this could/should just be on 'lockFocus' instead? +- (void) prepareContext { + if (ogl_ctx) { +#ifdef USE_IPHONE + [EAGLContext setCurrentContext:ogl_ctx]; +#else // !USE_IPHONE + [ogl_ctx makeCurrentContext]; +// check_gl_error ("makeCurrentContext"); +#endif // !USE_IPHONE + } } @@ -518,48 +839,53 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) fps_draw (fpst); } + #ifdef USE_IPHONE -/* Create a bitmap context into which we render everything. +/* On iPhones with Retina displays, we can draw the savers in "real" + pixels, and that works great. The 320x480 "point" screen is really + a 640x960 *pixel* screen. However, Retina iPads have 768x1024 + point screens which are 1536x2048 pixels, and apparently that's + enough pixels that copying those bits to the screen is slow. Like, + drops us from 15fps to 7fps. So, on Retina iPads, we don't draw in + real pixels. This will probably make the savers look better + anyway, since that's a higher resolution than most desktop monitors + have even today. (This is only true for X11 programs, not GL + programs. Those are fine at full rez.) + + This method is overridden in XScreenSaverGLView, since this kludge + isn't necessary for GL programs, being resolution independent by + nature. */ -- (void) createBackbuffer +- (CGFloat) hackedContentScaleFactor { - CGContextRef ob = backbuffer; - NSSize osize = backbuffer_size; - - CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); - double s = self.contentScaleFactor; - backbuffer_size.width = (int) (s * rot_current_size.width); - backbuffer_size.height = (int) (s * rot_current_size.height); - backbuffer = CGBitmapContextCreate (NULL, - backbuffer_size.width, - backbuffer_size.height, - 8, - backbuffer_size.width * 4, - cs, - kCGImageAlphaPremultipliedLast); - NSAssert (backbuffer, @"unable to allocate back buffer"); - CGColorSpaceRelease (cs); + NSSize ssize = [[[UIScreen mainScreen] currentMode] size]; + NSSize bsize = [self bounds].size; - // Clear it. - CGContextSetGrayFillColor (backbuffer, 0, 1); - CGRect r = CGRectZero; - r.size = backbuffer_size; - CGContextFillRect (backbuffer, r); + CGFloat + max_ssize = ssize.width > ssize.height ? ssize.width : ssize.height, + max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height; - if (ob) { - // Restore old bits, as much as possible, to the X11 upper left origin. - NSRect rect; - rect.origin.x = 0; - rect.origin.y = (backbuffer_size.height - osize.height); - rect.size = osize; - CGImageRef img = CGBitmapContextCreateImage (ob); - CGContextDrawImage (backbuffer, rect, img); - CGImageRelease (img); - CGContextRelease (ob); - } + // Ratio of screen size in pixels to view size in points. + CGFloat s = max_ssize / max_bsize; + + // Two constraints: + + // 1. Don't exceed -- let's say 1280 pixels in either direction. + // (Otherwise the frame rate gets bad.) + CGFloat mag0 = ceil(max_ssize / 1280); + + // 2. Don't let the pixel size get too small. + // (Otherwise pixels in IFS and similar are too fine.) + // So don't let the result be > 2 pixels per point. + CGFloat mag1 = ceil(s / 2); + + // As of iPhone 6, mag0 is always >= mag1. This may not be true in the future. + // (desired scale factor) = s / (desired magnification factor) + return s / (mag0 > mag1 ? mag0 : mag1); } + static GLfloat _global_rot_current_angle_kludge; double current_device_rotation (void) @@ -584,22 +910,28 @@ double current_device_rotation (void) rot_current_angle = f - rotation_ratio * dist; // Intermediate frame size. - rot_current_size.width = rot_from.width + - rotation_ratio * (rot_to.width - rot_from.width); - rot_current_size.height = rot_from.height + - rotation_ratio * (rot_to.height - rot_from.height); + rot_current_size.width = floor(rot_from.width + + rotation_ratio * (rot_to.width - rot_from.width)); + rot_current_size.height = floor(rot_from.height + + rotation_ratio * (rot_to.height - rot_from.height)); // Tick animation. Complete rotation in 1/6th sec. double now = double_time(); double duration = 1/6.0; rotation_ratio = 1 - ((rot_start_time + duration - now) / duration); - if (rotation_ratio > 1) { // Done animating. + if (rotation_ratio > 1 || ignore_rotation_p) { // Done animating. orientation = new_orientation; rot_current_angle = angle_to; rot_current_size = rot_to; rotation_ratio = -1; +# if TARGET_IPHONE_SIMULATOR + NSLog (@"rotation ended: %s %d, %d x %d", + orientname(orientation), (int) rot_current_angle, + (int) rot_current_size.width, (int) rot_current_size.height); +# endif + // Check orientation again in case we rotated again while rotating: // this is a no-op if nothing has changed. [self didRotate:nil]; @@ -614,18 +946,19 @@ double current_device_rotation (void) # undef CLAMP180 - double s = self.contentScaleFactor; - if (((int) backbuffer_size.width != (int) (s * rot_current_size.width) || - (int) backbuffer_size.height != (int) (s * rot_current_size.height)) -/* && rotation_ratio == -1*/) - [self setFrame:[self frame]]; + CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow]) + ? initial_bounds + : rot_current_size); + if ((int) backbuffer_size.width != (int) rotsize.width || + (int) backbuffer_size.height != (int) rotsize.height) + [self resize_x11]; } - (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i { if (i == 0) exit (-1); // Cancel - [self stopAndClose]; // Keep going + [self stopAndClose:NO]; // Keep going } - (void) handleException: (NSException *)e @@ -651,102 +984,637 @@ double current_device_rotation (void) #endif // USE_IPHONE -- (void) render_x11 -{ -# ifdef USE_IPHONE - @try { +#ifdef USE_BACKBUFFER - if (orientation == UIDeviceOrientationUnknown) - [self didRotate:nil]; - [self hackRotation]; -# endif +# ifndef USE_IPHONE - if (!initted_p) { +struct gl_version +{ + // iOS always uses OpenGL ES 1.1. + unsigned major; + unsigned minor; +}; + +static GLboolean +gl_check_ver (const struct gl_version *caps, + unsigned gl_major, + unsigned gl_minor) +{ + return caps->major > gl_major || + (caps->major == gl_major && caps->minor >= gl_minor); +} - if (! xdpy) { -# ifdef USE_IPHONE - NSAssert (backbuffer, @"no back buffer"); - xdpy = jwxyz_make_display (self, backbuffer); # else - xdpy = jwxyz_make_display (self, 0); -# endif - xwindow = XRootWindow (xdpy, 0); -# ifdef USE_IPHONE - jwxyz_window_resized (xdpy, xwindow, - 0, 0, - backbuffer_size.width, backbuffer_size.height, - backbuffer); -# else - NSRect r = [self frame]; - jwxyz_window_resized (xdpy, xwindow, - r.origin.x, r.origin.y, - r.size.width, r.size.height, - 0); -# endif - } +static GLboolean +gluCheckExtension (const GLubyte *ext_name, const GLubyte *ext_string) +{ + size_t ext_len = strlen ((const char *)ext_name); - if (!setup_p) { - setup_p = YES; - if (xsft->setup_cb) - xsft->setup_cb (xsft, xsft->setup_arg); + for (;;) { + const GLubyte *found = (const GLubyte *)strstr ((const char *)ext_string, + (const char *)ext_name); + if (!found) + break; + + char last_ch = found[ext_len]; + if ((found == ext_string || found[-1] == ' ') && + (last_ch == ' ' || !last_ch)) { + return GL_TRUE; } - initted_p = YES; - resized_p = NO; - NSAssert(!xdata, @"xdata already initialized"); - -# undef ya_rand_init - ya_rand_init (0); - - XSetWindowBackground (xdpy, xwindow, - get_pixel_resource (xdpy, 0, - "background", "Background")); - XClearWindow (xdpy, xwindow); - -# ifndef USE_IPHONE - [[self window] setAcceptsMouseMovedEvents:YES]; -# endif - /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz - drawing primitives will run on the GPU instead of the CPU. - It seems like it might make things worse rather than better, - though... Plus it makes us binary-incompatible with 10.4. + ext_string = found + ext_len; + } + + return GL_FALSE; +} -# if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 - [[self window] setPreferredBackingLocation: - NSWindowBackingLocationVideoMemory]; # endif - */ - /* Kludge: even though the init_cb functions are declared to take 2 args, - actually call them with 3, for the benefit of xlockmore_init() and - xlockmore_setup(). - */ - void *(*init_cb) (Display *, Window, void *) = - (void *(*) (Display *, Window, void *)) xsft->init_cb; - - xdata = init_cb (xdpy, xwindow, xsft->setup_arg); +/* Called during startAnimation before the first call to createBackbuffer. */ +- (void) enableBackbuffer:(CGSize)new_backbuffer_size +{ +# ifndef USE_IPHONE + struct gl_version version; - if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) { - fpst = fps_init (xdpy, xwindow); - if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps; + { + const char *version_str = (const char *)glGetString (GL_VERSION); + + /* iPhone is always OpenGL ES 1.1. */ + if (sscanf ((const char *)version_str, "%u.%u", + &version.major, &version.minor) < 2) + { + version.major = 1; + version.minor = 1; } } +# endif + // The OpenGL extensions in use in here are pretty are pretty much ubiquitous + // on OS X, but it's still good form to check. + const GLubyte *extensions = glGetString (GL_EXTENSIONS); - /* I don't understand why we have to do this *every frame*, but we do, - or else the cursor comes back on. - */ + glGenTextures (1, &backbuffer_texture); + + // On really old systems, it would make sense to split the texture + // into subsections # ifndef USE_IPHONE - if (![self isPreview]) - [NSCursor setHiddenUntilMouseMoves:YES]; + gl_texture_target = (gluCheckExtension ((const GLubyte *) + "GL_ARB_texture_rectangle", + extensions) + ? GL_TEXTURE_RECTANGLE_EXT : GL_TEXTURE_2D); +# else + // OES_texture_npot also provides this, but iOS never provides it. + gl_limited_npot_p = gluCheckExtension ((const GLubyte *) + "GL_APPLE_texture_2D_limited_npot", + extensions); + gl_texture_target = GL_TEXTURE_2D; # endif + glBindTexture (gl_texture_target, &backbuffer_texture); + glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + // GL_LINEAR might make sense on Retina iPads. + glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri (gl_texture_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - if (fpst) - { - /* This is just a guess, but the -fps code wants to know how long - we were sleeping between frames. +# ifndef USE_IPHONE + // There isn't much sense in supporting one of these if the other + // isn't present. + gl_apple_client_storage_p = + gluCheckExtension ((const GLubyte *)"GL_APPLE_client_storage", + extensions) && + gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_range", extensions); + + if (gl_apple_client_storage_p) { + glTexParameteri (gl_texture_target, GL_TEXTURE_STORAGE_HINT_APPLE, + GL_STORAGE_SHARED_APPLE); + glPixelStorei (GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE); + } +# endif + + // If a video adapter suports BGRA textures, then that's probably as fast as + // you're gonna get for getting a texture onto the screen. +# ifdef USE_IPHONE + gl_pixel_format = + gluCheckExtension ((const GLubyte *)"GL_APPLE_texture_format_BGRA8888", + extensions) ? GL_BGRA : GL_RGBA; + gl_pixel_type = GL_UNSIGNED_BYTE; + // See also OES_read_format. +# else + if (gl_check_ver (&version, 1, 2) || + (gluCheckExtension ((const GLubyte *)"GL_EXT_bgra", extensions) && + gluCheckExtension ((const GLubyte *)"GL_APPLE_packed_pixels", + extensions))) { + gl_pixel_format = GL_BGRA; + // Both Intel and PowerPC-era docs say to use GL_UNSIGNED_INT_8_8_8_8_REV. + gl_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV; + } else { + gl_pixel_format = GL_RGBA; + gl_pixel_type = GL_UNSIGNED_BYTE; + } + // GL_ABGR_EXT/GL_UNSIGNED_BYTE is another possibilty that may have made more + // sense on PowerPC. +# endif + + glEnable (gl_texture_target); + glEnableClientState (GL_VERTEX_ARRAY); + glEnableClientState (GL_TEXTURE_COORD_ARRAY); + +# ifdef USE_IPHONE + glMatrixMode (GL_PROJECTION); + glLoadIdentity(); + NSAssert (new_backbuffer_size.width != 0 && new_backbuffer_size.height != 0, + @"initial_bounds never got set"); + // This is pretty similar to the glOrtho in createBackbuffer for OS X. + glOrthof (-new_backbuffer_size.width, new_backbuffer_size.width, + -new_backbuffer_size.height, new_backbuffer_size.height, -1, 1); +# endif // USE_IPHONE + + check_gl_error ("enableBackbuffer"); +} + + +static GLsizei +to_pow2 (size_t x) +{ + if (x <= 1) + return 1; + + size_t mask = (size_t)-1; + unsigned bits = sizeof(x) * CHAR_BIT; + unsigned log2 = bits; + + --x; + while (bits) { + if (!(x & mask)) { + log2 -= bits; + x <<= bits; + } + + bits >>= 1; + mask <<= bits; + } + + return 1 << log2; +} + + +/* Create a bitmap context into which we render everything. + If the desired size has changed, re-created it. + new_size is in rotated pixels, not points: the same size + and shape as the X11 window as seen by the hacks. + */ +- (void) createBackbuffer:(CGSize)new_size +{ + // Colorspaces and CGContexts only happen with non-GL hacks. + if (colorspace) + CGColorSpaceRelease (colorspace); + +# ifdef BACKBUFFER_OPENGL + NSAssert ([NSOpenGLContext currentContext] == + ogl_ctx, @"invalid GL context"); + + // This almost isn't necessary, except for the ugly aliasing artifacts. +# ifndef USE_IPHONE + glViewport (0, 0, new_size.width, new_size.height); + + glMatrixMode (GL_PROJECTION); + glLoadIdentity(); + // This is pretty similar to the glOrthof in enableBackbuffer for iPhone. + glOrtho (-new_size.width, new_size.width, -new_size.height, new_size.height, + -1, 1); +# endif // !USE_IPHONE +# endif // BACKBUFFER_OPENGL + + NSWindow *window = [self window]; + + if (window && xdpy) { + [self lockFocus]; + +# ifdef BACKBUFFER_OPENGL + // Was apparently faster until 10.9. + colorspace = CGColorSpaceCreateDeviceRGB (); +# endif // BACKBUFFER_OPENGL + + [self unlockFocus]; + } else { + colorspace = CGColorSpaceCreateDeviceRGB(); + } + + if (backbuffer && + (int)backbuffer_size.width == (int)new_size.width && + (int)backbuffer_size.height == (int)new_size.height) + return; + + CGContextRef ob = backbuffer; + void *odata = backbuffer_data; + size_t olen = backbuffer_len; + + CGSize osize = backbuffer_size; // pixels, not points. + backbuffer_size = new_size; // pixels, not points. + +# if TARGET_IPHONE_SIMULATOR + NSLog(@"backbuffer %.0fx%.0f", + backbuffer_size.width, backbuffer_size.height); +# endif + + /* OS X uses APPLE_client_storage and APPLE_texture_range, as described in + . + + iOS uses bog-standard glTexImage2D (for now). + + glMapBuffer is the standard way to get data from system RAM to video + memory asynchronously and without a memcpy, but support for + APPLE_client_storage is ubiquitous on OS X (not so for glMapBuffer), + and on iOS GL_PIXEL_UNPACK_BUFFER is only available on OpenGL ES 3 + (iPhone 5S or newer). Plus, glMapBuffer doesn't work well with + CGBitmapContext: glMapBuffer can return a different pointer on each + call, but a CGBitmapContext doesn't allow its data pointer to be + changed -- and recreating the context for a new pointer can be + expensive (glyph caches get dumped, for instance). + + glMapBufferRange has MAP_FLUSH_EXPLICIT_BIT and MAP_UNSYNCHRONIZED_BIT, + and these seem to allow mapping the buffer and leaving it where it is + in client address space while OpenGL works with the buffer, but it + requires OpenGL 3 Core profile on OS X (and ES 3 on iOS for + GL_PIXEL_UNPACK_BUFFER), so point goes to APPLE_client_storage. + + AMD_pinned_buffer provides the same advantage as glMapBufferRange, but + Apple never implemented that one for OS X. + */ + + backbuffer_data = NULL; + gl_texture_w = (int)backbuffer_size.width; + gl_texture_h = (int)backbuffer_size.height; + + NSAssert (gl_texture_target == GL_TEXTURE_2D +# ifndef USE_IPHONE + || gl_texture_target == GL_TEXTURE_RECTANGLE_EXT +# endif + , @"unexpected GL texture target"); + +# ifndef USE_IPHONE + if (gl_texture_target != GL_TEXTURE_RECTANGLE_EXT) +# else + if (!gl_limited_npot_p) +# endif + { + gl_texture_w = to_pow2 (gl_texture_w); + gl_texture_h = to_pow2 (gl_texture_h); + } + + size_t bytes_per_row = gl_texture_w * 4; + +# if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE) + // APPLE_client_storage requires texture width to be aligned to 32 bytes, or + // it will fall back to a memcpy. + // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW24 + bytes_per_row = (bytes_per_row + 31) & ~31; +# endif // BACKBUFFER_OPENGL && !USE_IPHONE + + backbuffer_len = bytes_per_row * gl_texture_h; + if (backbuffer_len) // mmap requires this to be non-zero. + backbuffer_data = mmap (NULL, backbuffer_len, + PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, + -1, 0); + + BOOL alpha_first_p, order_little_p; + + if (gl_pixel_format == GL_BGRA) { + alpha_first_p = YES; + order_little_p = YES; +/* + } else if (gl_pixel_format == GL_ABGR_EXT) { + alpha_first_p = NO; + order_little_p = YES; */ + } else { + NSAssert (gl_pixel_format == GL_RGBA, @"unknown GL pixel format"); + alpha_first_p = NO; + order_little_p = NO; + } + +#ifdef USE_IPHONE + NSAssert (gl_pixel_type == GL_UNSIGNED_BYTE, @"unknown GL pixel type"); +#else + NSAssert (gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8 || + gl_pixel_type == GL_UNSIGNED_INT_8_8_8_8_REV || + gl_pixel_type == GL_UNSIGNED_BYTE, + @"unknown GL pixel type"); + +#if defined __LITTLE_ENDIAN__ + const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8; +#elif defined __BIG_ENDIAN__ + const GLenum backwards_pixel_type = GL_UNSIGNED_INT_8_8_8_8_REV; +#else +# error Unknown byte order. +#endif + + if (gl_pixel_type == backwards_pixel_type) + order_little_p ^= YES; +#endif + + CGBitmapInfo bitmap_info = + (alpha_first_p ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaNoneSkipLast) | + (order_little_p ? kCGBitmapByteOrder32Little : kCGBitmapByteOrder32Big); + + backbuffer = CGBitmapContextCreate (backbuffer_data, + (int)backbuffer_size.width, + (int)backbuffer_size.height, + 8, + bytes_per_row, + colorspace, + bitmap_info); + NSAssert (backbuffer, @"unable to allocate back buffer"); + + // Clear it. + CGRect r; + r.origin.x = r.origin.y = 0; + r.size = backbuffer_size; + CGContextSetGrayFillColor (backbuffer, 0, 1); + CGContextFillRect (backbuffer, r); + +# if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE) + if (gl_apple_client_storage_p) + glTextureRangeAPPLE (gl_texture_target, backbuffer_len, backbuffer_data); +# endif // BACKBUFFER_OPENGL && !USE_IPHONE + + if (ob) { + // Restore old bits, as much as possible, to the X11 upper left origin. + + CGRect rect; // pixels, not points + rect.origin.x = 0; + rect.origin.y = (backbuffer_size.height - osize.height); + rect.size = osize; + + CGImageRef img = CGBitmapContextCreateImage (ob); + CGContextDrawImage (backbuffer, rect, img); + CGImageRelease (img); + CGContextRelease (ob); + + if (olen) + // munmap should round len up to the nearest page. + munmap (odata, olen); + } + + check_gl_error ("createBackbuffer"); +} + + +- (void) drawBackbuffer +{ +# ifdef BACKBUFFER_OPENGL + + NSAssert ([ogl_ctx isKindOfClass:[NSOpenGLContext class]], + @"ogl_ctx is not an NSOpenGLContext"); + + NSAssert (! (CGBitmapContextGetBytesPerRow (backbuffer) % 4), + @"improperly-aligned backbuffer"); + + // This gets width and height from the backbuffer in case + // APPLE_client_storage is in use. See the note in createBackbuffer. + // This still has to happen every frame even when APPLE_client_storage has + // the video adapter pulling texture data straight from + // XScreenSaverView-owned memory. + glTexImage2D (gl_texture_target, 0, GL_RGBA, + (GLsizei)(CGBitmapContextGetBytesPerRow (backbuffer) / 4), + gl_texture_h, 0, gl_pixel_format, gl_pixel_type, + backbuffer_data); + + GLfloat vertices[4][2] = + { + {-backbuffer_size.width, backbuffer_size.height}, + { backbuffer_size.width, backbuffer_size.height}, + { backbuffer_size.width, -backbuffer_size.height}, + {-backbuffer_size.width, -backbuffer_size.height} + }; + + GLfloat tex_coords[4][2]; + +# ifndef USE_IPHONE + if (gl_texture_target == GL_TEXTURE_RECTANGLE_EXT) { + tex_coords[0][0] = 0; + tex_coords[0][1] = 0; + tex_coords[1][0] = backbuffer_size.width; + tex_coords[1][1] = 0; + tex_coords[2][0] = backbuffer_size.width; + tex_coords[2][1] = backbuffer_size.height; + tex_coords[3][0] = 0; + tex_coords[3][1] = backbuffer_size.height; + } else +# endif // USE_IPHONE + { + GLfloat x = backbuffer_size.width / gl_texture_w; + GLfloat y = backbuffer_size.height / gl_texture_h; + tex_coords[0][0] = 0; + tex_coords[0][1] = 0; + tex_coords[1][0] = x; + tex_coords[1][1] = 0; + tex_coords[2][0] = x; + tex_coords[2][1] = y; + tex_coords[3][0] = 0; + tex_coords[3][1] = y; + } + +# ifdef USE_IPHONE + if (!ignore_rotation_p) { + glMatrixMode (GL_MODELVIEW); + glLoadIdentity(); + glRotatef (rot_current_angle, 0, 0, -1); + + if (rotation_ratio >= 0) + glClear (GL_COLOR_BUFFER_BIT); + } +# endif // USE_IPHONE + + glVertexPointer (2, GL_FLOAT, 0, vertices); + glTexCoordPointer (2, GL_FLOAT, 0, tex_coords); + glDrawArrays (GL_TRIANGLE_FAN, 0, 4); + +# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR + check_gl_error ("drawBackbuffer"); +# endif + + // This can also happen near the beginning of render_x11. + [self flushBackbuffer]; + +# endif // BACKBUFFER_OPENGL +} + + +- (void)flushBackbuffer +{ +# ifndef USE_IPHONE + // The OpenGL pipeline is not automatically synchronized with the contents + // of the backbuffer, so without glFinish, OpenGL can start rendering from + // the backbuffer texture at the same time that JWXYZ is clearing and + // drawing the next frame in the backing store for the backbuffer texture. + glFinish(); + + if (double_buffered_p) + [ogl_ctx flushBuffer]; // despite name, this actually swaps +# else + glBindRenderbufferOES (GL_RENDERBUFFER_OES, gl_renderbuffer); + [ogl_ctx presentRenderbuffer:GL_RENDERBUFFER_OES]; +# endif + +# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR + // glGetError waits for the OpenGL command pipe to flush, so skip it in + // release builds. + // OpenGL Programming Guide for Mac -> OpenGL Application Design + // Strategies -> Allow OpenGL to Manage Your Resources + // https://developer.apple.com/library/mac/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_designstrategies/opengl_designstrategies.html#//apple_ref/doc/uid/TP40001987-CH2-SW7 + check_gl_error ("flushBackbuffer"); +# endif +} + + +#endif // USE_BACKBUFFER + + +/* Inform X11 that the size of our window has changed. + */ +- (void) resize_x11 +{ + if (!xwindow) return; // early + + CGSize new_size; // pixels, not points + +# ifdef USE_BACKBUFFER + + [self prepareContext]; + +# if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE) + [ogl_ctx update]; +# endif // BACKBUFFER_OPENGL && !USE_IPHONE + +# ifdef USE_IPHONE + CGSize rotsize = ((ignore_rotation_p || ![self reshapeRotatedWindow]) + ? initial_bounds + : rot_current_size); + new_size.width = rotsize.width; + new_size.height = rotsize.height; +# else // !USE_IPHONE + new_size = NSSizeToCGSize([self bounds].size); +# endif // !USE_IPHONE + + [self createBackbuffer:new_size]; + jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height, + backbuffer); +# else // !USE_BACKBUFFER + new_size = [self bounds].size; + jwxyz_window_resized (xdpy, xwindow, 0, 0, new_size.width, new_size.height, + 0); +# endif // !USE_BACKBUFFER + +# if TARGET_IPHONE_SIMULATOR + NSLog(@"reshape %.0fx%.0f", new_size.width, new_size.height); +# endif + + // Next time render_x11 is called, run the saver's reshape_cb. + resized_p = YES; +} + + +- (void) render_x11 +{ +# ifdef USE_IPHONE + @try { + + if (orientation == UIDeviceOrientationUnknown) + [self didRotate:nil]; + [self hackRotation]; +# endif + + if (!initted_p) { + + if (! xdpy) { +# ifdef USE_BACKBUFFER + NSAssert (backbuffer, @"no back buffer"); + xdpy = jwxyz_make_display (self, backbuffer); +# else + xdpy = jwxyz_make_display (self, 0); +# endif + xwindow = XRootWindow (xdpy, 0); + +# ifdef USE_IPHONE + /* Some X11 hacks (fluidballs) want to ignore all rotation events. */ + ignore_rotation_p = + get_boolean_resource (xdpy, "ignoreRotation", "IgnoreRotation"); +# endif // USE_IPHONE + + [self resize_x11]; + } + + if (!setup_p) { + setup_p = YES; + if (xsft->setup_cb) + xsft->setup_cb (xsft, xsft->setup_arg); + } + initted_p = YES; + resized_p = NO; + NSAssert(!xdata, @"xdata already initialized"); + + +# undef ya_rand_init + ya_rand_init (0); + + XSetWindowBackground (xdpy, xwindow, + get_pixel_resource (xdpy, 0, + "background", "Background")); + XClearWindow (xdpy, xwindow); + +# ifndef USE_IPHONE + [[self window] setAcceptsMouseMovedEvents:YES]; +# endif + + /* In MacOS 10.5, this enables "QuartzGL", meaning that the Quartz + drawing primitives will run on the GPU instead of the CPU. + It seems like it might make things worse rather than better, + though... Plus it makes us binary-incompatible with 10.4. + +# if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 + [[self window] setPreferredBackingLocation: + NSWindowBackingLocationVideoMemory]; +# endif + */ + + /* Kludge: even though the init_cb functions are declared to take 2 args, + actually call them with 3, for the benefit of xlockmore_init() and + xlockmore_setup(). + */ + void *(*init_cb) (Display *, Window, void *) = + (void *(*) (Display *, Window, void *)) xsft->init_cb; + + xdata = init_cb (xdpy, xwindow, xsft->setup_arg); + // NSAssert(xdata, @"no xdata from init"); + if (! xdata) abort(); + + if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) { + fpst = fps_init (xdpy, xwindow); + if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps; + } else { + fpst = NULL; + xsft->fps_cb = 0; + } + + [self checkForUpdates]; + } + + + /* I don't understand why we have to do this *every frame*, but we do, + or else the cursor comes back on. + */ +# ifndef USE_IPHONE + if (![self isPreview]) + [NSCursor setHiddenUntilMouseMoves:YES]; +# endif + + + if (fpst) + { + /* This is just a guess, but the -fps code wants to know how long + we were sleeping between frames. */ long usecs = 1000000 * [self animationTimeInterval]; usecs -= 200; // caller apparently sleeps for slightly less sometimes... @@ -755,45 +1623,66 @@ double current_device_rotation (void) } - /* It turns out that [ScreenSaverView setAnimationTimeInterval] does nothing. - This is bad, because some of the screen hacks want to delay for long - periods (like 5 seconds or a minute!) between frames, and running them - all at 60 FPS is no good. - - So, we don't use setAnimationTimeInterval, and just let the framework call - us whenever. But, we only invoke the screen hack's "draw frame" method - when enough time has expired. + /* It turns out that on some systems (possibly only 10.5 and older?) + [ScreenSaverView setAnimationTimeInterval] does nothing. This means + that we cannot rely on it. + + Some of the screen hacks want to delay for long periods, and letting the + framework run the update function at 30 FPS when it really wanted half a + minute between frames would be bad. So instead, we assume that the + framework's animation timer might fire whenever, but we only invoke the + screen hack's "draw frame" method when enough time has expired. This means two extra calls to gettimeofday() per frame. For fast-cycling screen savers, that might actually slow them down. Oh well. - #### Also, we do not run the draw callback faster than the system's - animationTimeInterval, so if any savers are pickier about timing - than that, this may slow them down too much. If that's a problem, - then we could call draw_cb in a loop here (with usleep) until the - next call would put us past animationTimeInterval... But a better - approach would probably be to just change the saver to not do that. + A side-effect of this is that it's not possible for a saver to request + an animation interval that is faster than animationTimeInterval. + + HOWEVER! On modern systems where setAnimationTimeInterval is *not* + ignored, it's important that it be faster than 30 FPS. 240 FPS is good. + + An NSTimer won't fire if the timer is already running the invocation + function from a previous firing. So, if we use a 30 FPS + animationTimeInterval (33333 µs) and a screenhack takes 40000 µs for a + frame, there will be a 26666 µs delay until the next frame, 66666 µs + after the beginning of the current frame. In other words, 25 FPS + becomes 15 FPS. + + Frame rates tend to snap to values of 30/N, where N is a positive + integer, i.e. 30 FPS, 15 FPS, 10, 7.5, 6. And the 'snapped' frame rate + is rounded down from what it would normally be. + + So if we set animationTimeInterval to 1/240 instead of 1/30, frame rates + become values of 60/N, 120/N, or 240/N, with coarser or finer frame rate + steps for higher or lower animation time intervals respectively. */ struct timeval tv; gettimeofday (&tv, 0); double now = tv.tv_sec + (tv.tv_usec / 1000000.0); if (now < next_frame_time) return; - - [self prepareContext]; + + [self prepareContext]; // resize_x11 also calls this. + // [self flushBackbuffer]; if (resized_p) { // We do this here instead of in setFrame so that all the // Xlib drawing takes place under the animation timer. - [self resizeContext]; - NSRect r; + # ifndef USE_IPHONE - r = [self frame]; -# else // USE_IPHONE + if (ogl_ctx) + [ogl_ctx setView:self]; +# endif // !USE_IPHONE + + NSRect r; +# ifndef USE_BACKBUFFER + r = [self bounds]; +# else // USE_BACKBUFFER r.origin.x = 0; r.origin.y = 0; r.size.width = backbuffer_size.width; r.size.height = backbuffer_size.height; -# endif // USE_IPHONE +# endif // USE_BACKBUFFER xsft->reshape_cb (xdpy, xwindow, xdata, r.size.width, r.size.height); resized_p = NO; @@ -808,19 +1697,18 @@ double current_device_rotation (void) // And finally: // -# ifndef USE_IPHONE - NSDisableScreenUpdates(); -# endif + // NSAssert(xdata, @"no xdata when drawing"); + if (! xdata) abort(); unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata); - if (fpst) xsft->fps_cb (xdpy, xwindow, fpst, xdata); -# ifndef USE_IPHONE - NSEnableScreenUpdates(); -# endif + if (fpst && xsft->fps_cb) + xsft->fps_cb (xdpy, xwindow, fpst, xdata); gettimeofday (&tv, 0); now = tv.tv_sec + (tv.tv_usec / 1000000.0); next_frame_time = now + (delay / 1000000.0); + [self drawBackbuffer]; + # ifdef USE_IPHONE // Allow savers on the iPhone to run full-tilt. if (delay < [self animationTimeInterval]) [self setAnimationTimeInterval:(delay / 1000000.0)]; @@ -865,92 +1753,34 @@ double current_device_rotation (void) } -/* On MacOS: drawRect does nothing, and animateOneFrame renders. - On iOS GL: drawRect does nothing, and animateOneFrame renders. - On iOS X11: drawRect renders, and animateOneFrame marks the view dirty. - */ -#ifndef USE_IPHONE - -- (void)drawRect:(NSRect)rect -{ - if (xwindow) // clear to the X window's bg color, not necessarily black. - XClearWindow (xdpy, xwindow); - else - [super drawRect:rect]; // early: black. -} +#ifndef USE_BACKBUFFER - (void) animateOneFrame { [self render_x11]; + jwxyz_flush_context(xdpy); } -#else // USE_IPHONE +#else // USE_BACKBUFFER -- (void)drawRect:(NSRect)rect +- (void) animateOneFrame { // Render X11 into the backing store bitmap... NSAssert (backbuffer, @"no back buffer"); - UIGraphicsPushContext (backbuffer); - [self render_x11]; - UIGraphicsPopContext(); - - // Then copy that bitmap to the screen. - - CGContextRef cgc = UIGraphicsGetCurrentContext(); - - // Mask it to only update the parts that are exposed. -// CGContextClipToRect (cgc, rect); - - double s = self.contentScaleFactor; - CGRect frame = [self frame]; - - NSRect target; - target.size.width = backbuffer_size.width; - target.size.height = backbuffer_size.height; - target.origin.x = (s * frame.size.width - target.size.width) / 2; - target.origin.y = (s * frame.size.height - target.size.height) / 2; - - target.origin.x /= s; - target.origin.y /= s; - target.size.width /= s; - target.size.height /= s; - CGAffineTransform t = CGAffineTransformIdentity; - - // Rotate around center - float cx = frame.size.width / 2; - float cy = frame.size.height / 2; - t = CGAffineTransformTranslate (t, cx, cy); - t = CGAffineTransformRotate (t, -rot_current_angle / (180.0 / M_PI)); - t = CGAffineTransformTranslate (t, -cx, -cy); - - // Flip Y axis - t = CGAffineTransformConcat (t, - CGAffineTransformMake ( 1, 0, 0, - -1, 0, frame.size.height)); - - // Clear background (visible in corners of screen during rotation) - if (rotation_ratio != -1) { - CGContextSetGrayFillColor (cgc, 0, 1); - CGContextFillRect (cgc, frame); - } - - CGContextConcatCTM (cgc, t); +# ifdef USE_IPHONE + UIGraphicsPushContext (backbuffer); +# endif - // Copy the backbuffer to the screen. - // Note that CGContextDrawImage measures in "points", not "pixels". - CGImageRef img = CGBitmapContextCreateImage (backbuffer); - CGContextDrawImage (cgc, target, img); - CGImageRelease (img); -} + [self render_x11]; -- (void) animateOneFrame -{ - [self setNeedsDisplay]; +# if defined USE_IPHONE && defined USE_BACKBUFFER + UIGraphicsPopContext(); +# endif } -#endif // !USE_IPHONE +#endif // USE_BACKBUFFER @@ -958,26 +1788,8 @@ double current_device_rotation (void) { [super setFrame:newRect]; -# ifdef USE_IPHONE - [self createBackbuffer]; -# endif - - resized_p = YES; // The reshape_cb runs in render_x11 - if (xwindow) { // inform Xlib that the window has changed now. -# ifdef USE_IPHONE - NSAssert (backbuffer, @"no back buffer"); - // The backbuffer is the rotated size, and so is the xwindow. - jwxyz_window_resized (xdpy, xwindow, - 0, 0, - backbuffer_size.width, backbuffer_size.height, - backbuffer); -# else - jwxyz_window_resized (xdpy, xwindow, - newRect.origin.x, newRect.origin.y, - newRect.size.width, newRect.size.height, - 0); -# endif - } + if (xwindow) // inform Xlib that the window has changed now. + [self resize_x11]; } @@ -985,13 +1797,8 @@ double current_device_rotation (void) - (void) setFrameSize:(NSSize) newSize { [super setFrameSize:newSize]; - resized_p = YES; if (xwindow) - jwxyz_window_resized (xdpy, xwindow, - [self frame].origin.x, - [self frame].origin.y, - newSize.width, newSize.height, - 0); // backbuffer only on iPhone + [self resize_x11]; } # endif // !USE_IPHONE @@ -1006,6 +1813,40 @@ double current_device_rotation (void) return YES; } ++ (NSString *) decompressXML: (NSData *)data +{ + if (! data) return 0; + BOOL compressed_p = !!strncmp ((const char *) data.bytes, "")); + } + + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + + #ifndef USE_IPHONE - (NSWindow *) configureSheet #else @@ -1029,15 +1870,19 @@ double current_device_rotation (void) NSWindow *sheet; # endif // !USE_IPHONE + NSData *xmld = [NSData dataWithContentsOfFile:path]; + NSString *xml = [[self class] decompressXML: xmld]; sheet = [[XScreenSaverConfigSheet alloc] - initWithXMLFile:path - options:xsft->options - controller:[prefsReader userDefaultsController] - defaults:[prefsReader defaultOptions]]; + initWithXML:[xml dataUsingEncoding:NSUTF8StringEncoding] + options:xsft->options + controller:[prefsReader userDefaultsController] + globalController:[prefsReader globalDefaultsController] + defaults:[prefsReader defaultOptions]]; // #### am I expected to retain this, or not? wtf. // I thought not, but if I don't do this, we (sometimes) crash. - [sheet retain]; + // #### Analyze says "potential leak of an object stored into sheet" + // [sheet retain]; return sheet; } @@ -1050,26 +1895,62 @@ double current_device_rotation (void) /* Announce our willingness to accept keyboard input. -*/ + */ - (BOOL)acceptsFirstResponder { return YES; } +- (void) beep +{ +# ifndef USE_IPHONE + NSBeep(); +# else // USE_IPHONE + + // There's no way to play a standard system alert sound! + // We'd have to include our own WAV for that. + // + // Or we could vibrate: + // #import + // AudioServicesPlaySystemSound (kSystemSoundID_Vibrate); + // + // Instead, just flash the screen white, then fade. + // + UIView *v = [[UIView alloc] initWithFrame: [self frame]]; + [v setBackgroundColor: [UIColor whiteColor]]; + [[self window] addSubview:v]; + [UIView animateWithDuration: 0.1 + animations:^{ [v setAlpha: 0.0]; } + completion:^(BOOL finished) { [v removeFromSuperview]; } ]; + +# endif // USE_IPHONE +} + + +/* Send an XEvent to the hack. Returns YES if it was handled. + */ +- (BOOL) sendEvent: (XEvent *) e +{ + if (!initted_p || ![self isAnimating]) // no event handling unless running. + return NO; + + [self lockFocus]; + [self prepareContext]; + BOOL result = xsft->event_cb (xdpy, xwindow, xdata, e); + [self unlockFocus]; + return result; +} + + #ifndef USE_IPHONE /* Convert an NSEvent into an XEvent, and pass it along. Returns YES if it was handled. */ -- (BOOL) doEvent: (NSEvent *) e +- (BOOL) convertEvent: (NSEvent *) e type: (int) type { - if (![self isPreview] || // no event handling if actually screen-saving! - ![self isAnimating] || - !initted_p) - return NO; - XEvent xe; memset (&xe, 0, sizeof(xe)); @@ -1085,12 +1966,12 @@ double current_device_rotation (void) NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow] toView:self]; # ifdef USE_IPHONE - double s = self.contentScaleFactor; + double s = [self hackedContentScaleFactor]; # else int s = 1; # endif int x = s * p.x; - int y = s * ([self frame].size.height - p.y); + int y = s * ([self bounds].size.height - p.y); xe.xany.type = type; switch (type) { @@ -1146,6 +2027,18 @@ double current_device_rotation (void) case NSNextFunctionKey: k = XK_Next; break; case NSBeginFunctionKey: k = XK_Begin; break; case NSEndFunctionKey: k = XK_End; break; + case NSF1FunctionKey: k = XK_F1; break; + case NSF2FunctionKey: k = XK_F2; break; + case NSF3FunctionKey: k = XK_F3; break; + case NSF4FunctionKey: k = XK_F4; break; + case NSF5FunctionKey: k = XK_F5; break; + case NSF6FunctionKey: k = XK_F6; break; + case NSF7FunctionKey: k = XK_F7; break; + case NSF8FunctionKey: k = XK_F8; break; + case NSF9FunctionKey: k = XK_F9; break; + case NSF10FunctionKey: k = XK_F10; break; + case NSF11FunctionKey: k = XK_F11; break; + case NSF12FunctionKey: k = XK_F12; break; default: { const char *s = @@ -1156,93 +2049,108 @@ double current_device_rotation (void) } } + if (! k) return YES; // E.g., "KeyRelease XK_Shift_L" + xe.xkey.keycode = k; xe.xkey.state = state; break; } default: - NSAssert (0, @"unknown X11 event type: %d", type); + NSAssert1 (0, @"unknown X11 event type: %d", type); break; } - [self lockFocus]; - [self prepareContext]; - BOOL result = xsft->event_cb (xdpy, xwindow, xdata, &xe); - [self unlockFocus]; - return result; + return [self sendEvent: &xe]; } - (void) mouseDown: (NSEvent *) e { - if (! [self doEvent:e type:ButtonPress]) + if (! [self convertEvent:e type:ButtonPress]) [super mouseDown:e]; } - (void) mouseUp: (NSEvent *) e { - if (! [self doEvent:e type:ButtonRelease]) + if (! [self convertEvent:e type:ButtonRelease]) [super mouseUp:e]; } - (void) otherMouseDown: (NSEvent *) e { - if (! [self doEvent:e type:ButtonPress]) + if (! [self convertEvent:e type:ButtonPress]) [super otherMouseDown:e]; } - (void) otherMouseUp: (NSEvent *) e { - if (! [self doEvent:e type:ButtonRelease]) + if (! [self convertEvent:e type:ButtonRelease]) [super otherMouseUp:e]; } - (void) mouseMoved: (NSEvent *) e { - if (! [self doEvent:e type:MotionNotify]) + if (! [self convertEvent:e type:MotionNotify]) [super mouseMoved:e]; } - (void) mouseDragged: (NSEvent *) e { - if (! [self doEvent:e type:MotionNotify]) + if (! [self convertEvent:e type:MotionNotify]) [super mouseDragged:e]; } - (void) otherMouseDragged: (NSEvent *) e { - if (! [self doEvent:e type:MotionNotify]) + if (! [self convertEvent:e type:MotionNotify]) [super otherMouseDragged:e]; } - (void) scrollWheel: (NSEvent *) e { - if (! [self doEvent:e type:ButtonPress]) + if (! [self convertEvent:e type:ButtonPress]) [super scrollWheel:e]; } - (void) keyDown: (NSEvent *) e { - if (! [self doEvent:e type:KeyPress]) + if (! [self convertEvent:e type:KeyPress]) [super keyDown:e]; } - (void) keyUp: (NSEvent *) e { - if (! [self doEvent:e type:KeyRelease]) + if (! [self convertEvent:e type:KeyRelease]) [super keyUp:e]; } - (void) flagsChanged: (NSEvent *) e { - if (! [self doEvent:e type:KeyPress]) + if (! [self convertEvent:e type:KeyPress]) [super flagsChanged:e]; } + +- (NSOpenGLPixelFormat *) getGLPixelFormat +{ + NSAssert (prefsReader, @"no prefsReader for getGLPixelFormat"); + + NSOpenGLPixelFormatAttribute attrs[40]; + int i = 0; + attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24; + + if (double_buffered_p) + attrs[i++] = NSOpenGLPFADoubleBuffer; + + attrs[i] = 0; + + return [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; +} + #else // USE_IPHONE -- (void) stopAndClose +- (void) stopAndClose:(Bool)relaunch_p { if ([self isAnimating]) [self stopAnimation]; @@ -1252,80 +2160,87 @@ double current_device_rotation (void) suppose that this abstraction-breakage means that I'm adding XScreenSaverView to the UINavigationController wrong... */ - UIViewController *v = [[self window] rootViewController]; - if ([v isKindOfClass: [UINavigationController class]]) { - UINavigationController *n = (UINavigationController *) v; - [[n topViewController] becomeFirstResponder]; - } +// UIViewController *v = [[self window] rootViewController]; +// if ([v isKindOfClass: [UINavigationController class]]) { +// UINavigationController *n = (UINavigationController *) v; +// [[n topViewController] becomeFirstResponder]; +// } + [self resignFirstResponder]; - // [self removeFromSuperview]; - [UIView animateWithDuration: 0.5 - animations:^{ self.alpha = 0.0; } - completion:^(BOOL finished) { - [self removeFromSuperview]; - self.alpha = 1.0; - }]; + if (relaunch_p) { // Fake a shake on the SaverListController. + [_delegate didShake:self]; + } else { // Not launching another, animate our return to the list. +# if TARGET_IPHONE_SIMULATOR + NSLog (@"fading back to saver list"); +# endif + [_delegate wantsFadeOut:self]; + } } -- (void) stopAndClose:(Bool)relaunch_p +/* Whether the shape of the X11 Window should be changed to HxW when the + device is in a landscape orientation. X11 hacks want this, but OpenGL + hacks do not. + */ +- (BOOL)reshapeRotatedWindow { - [self stopAndClose]; - - if (relaunch_p) { // Fake a shake on the SaverListController. - UIViewController *v = [[self window] rootViewController]; - if ([v isKindOfClass: [UINavigationController class]]) { - UINavigationController *n = (UINavigationController *) v; - [[n topViewController] motionEnded: UIEventSubtypeMotionShake - withEvent: nil]; - } - } + return YES; } /* Called after the device's orientation has changed. + + Rotation is complicated: the UI, X11 and OpenGL work in 3 different ways. + + The UI (list of savers, preferences panels) is rotated by the system, + because its UIWindow is under a UINavigationController that does + automatic rotation, using Core Animation. + + The savers are under a different UIWindow and a UINavigationController + that does not do automatic rotation. + + We have to do it this way because using Core Animation on an EAGLContext + causes the OpenGL pipeline used on both X11 and GL savers to fall back on + software rendering and performance goes to hell. + + During and after rotation, the size/shape of the X11 window changes, + and ConfigureNotify events are generated. - Note: we could include a subclass of UIViewController which - contains a shouldAutorotateToInterfaceOrientation method that - returns YES, in which case Core Animation would auto-rotate our - View for us in response to rotation events... but, that interacts - badly with the EAGLContext -- if you introduce Core Animation into - the path, the OpenGL pipeline probably falls back on software - rendering and performance goes to hell. Also, the scaling and - rotation that Core Animation does interacts incorrectly with the GL - context anyway. - - So, we have to hack the rotation animation manually, in the GL world. - - Possibly XScreenSaverView should use Core Animation, and - XScreenSaverGLView should override that. -*/ + X11 code (jwxyz) continues to draw into the (reshaped) backbuffer, which is + rendered onto a rotating OpenGL quad. + + GL code always recieves a portrait-oriented X11 Window whose size never + changes. The GL COLOR_BUFFER is displayed on the hardware directly and + unrotated, so the GL hacks themselves are responsible for rotating the + GL scene to match current_device_rotation(). + + Touch events are converted to mouse clicks, and those XEvent coordinates + are reported in the coordinate system currently in use by the X11 window. + Again, GL must convert those. + */ - (void)didRotate:(NSNotification *)notification { UIDeviceOrientation current = [[UIDevice currentDevice] orientation]; - /* If the simulator starts up in the rotated position, sometimes - the UIDevice says we're in Portrait when we're not -- but it - turns out that the UINavigationController knows what's up! - So get it from there. + /* Sometimes UIDevice doesn't know the proper orientation, or the device is + face up/face down, so in those cases fall back to the status bar + orientation. The SaverViewController tries to set the status bar to the + proper orientation before it creates the XScreenSaverView; see + _storedOrientation in SaverViewController. */ - if (current == UIDeviceOrientationUnknown) { - switch ([[[self window] rootViewController] interfaceOrientation]) { - case UIInterfaceOrientationPortrait: - current = UIDeviceOrientationPortrait; - break; - case UIInterfaceOrientationPortraitUpsideDown: - current = UIDeviceOrientationPortraitUpsideDown; - break; - case UIInterfaceOrientationLandscapeLeft: // It's opposite day - current = UIDeviceOrientationLandscapeRight; - break; - case UIInterfaceOrientationLandscapeRight: - current = UIDeviceOrientationLandscapeLeft; - break; - default: - break; - } + if (current == UIDeviceOrientationUnknown || + current == UIDeviceOrientationFaceUp || + current == UIDeviceOrientationFaceDown) { + /* Mind the differences between UIInterfaceOrientation and + UIDeviceOrientaiton: + 1. UIInterfaceOrientation does not include FaceUp and FaceDown. + 2. LandscapeLeft and LandscapeRight are swapped between the two. But + converting between device and interface orientation doesn't need to + take this into account, because (from the UIInterfaceOrientation + description): "rotating the device requires rotating the content in + the opposite direction." + */ + current = [UIApplication sharedApplication].statusBarOrientation; } /* On the iPad (but not iPhone 3GS, or the simulator) sometimes we get @@ -1340,11 +2255,6 @@ double current_device_rotation (void) if (rotation_ratio >= 0) return; // in the midst of rotation animation if (orientation == current) return; // no change - // When transitioning to FaceUp or FaceDown, pretend there was no change. - if (current == UIDeviceOrientationFaceUp || - current == UIDeviceOrientationFaceDown) - return; - new_orientation = current; // current animation target rotation_ratio = 0; // start animating rot_start_time = double_time(); @@ -1363,32 +2273,45 @@ double current_device_rotation (void) default: angle_to = 0; break; } - NSRect ff = [self frame]; - switch (orientation) { case UIDeviceOrientationLandscapeRight: // from landscape case UIDeviceOrientationLandscapeLeft: - rot_from.width = ff.size.height; - rot_from.height = ff.size.width; + rot_from.width = initial_bounds.height; + rot_from.height = initial_bounds.width; break; default: // from portrait - rot_from.width = ff.size.width; - rot_from.height = ff.size.height; + rot_from.width = initial_bounds.width; + rot_from.height = initial_bounds.height; break; } switch (new_orientation) { case UIDeviceOrientationLandscapeRight: // to landscape case UIDeviceOrientationLandscapeLeft: - rot_to.width = ff.size.height; - rot_to.height = ff.size.width; + rot_to.width = initial_bounds.height; + rot_to.height = initial_bounds.width; break; default: // to portrait - rot_to.width = ff.size.width; - rot_to.height = ff.size.height; + rot_to.width = initial_bounds.width; + rot_to.height = initial_bounds.height; break; } +# if TARGET_IPHONE_SIMULATOR + NSLog (@"%srotation begun: %s %d -> %s %d; %d x %d", + initted_p ? "" : "initial ", + orientname(orientation), (int) rot_current_angle, + orientname(new_orientation), (int) angle_to, + (int) rot_current_size.width, (int) rot_current_size.height); +# endif + + // Even though the status bar isn't on the screen, this still does two things: + // 1. It fixes the orientation of the iOS simulator. + // 2. It places the iOS notification center on the expected edge. + // 3. It prevents the notification center from causing rotation events. + [[UIApplication sharedApplication] setStatusBarOrientation:new_orientation + animated:NO]; + if (! initted_p) { // If we've done a rotation but the saver hasn't been initialized yet, // don't bother going through an X11 resize, but just do it now. @@ -1398,156 +2321,278 @@ double current_device_rotation (void) } -/* I believe we can't use UIGestureRecognizer for tracking touches - because UIPanGestureRecognizer doesn't give us enough detail in its - callbacks. +/* We distinguish between taps and drags. - In the simulator, multi-touch sequences look like this: + - Drags/pans (down, motion, up) are sent to the saver to handle. + - Single-taps exit the saver. + - Double-taps are sent to the saver as a "Space" keypress. + - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys. - touchesBegan [touchA, touchB] - touchesEnd [touchA, touchB] + This means a saver cannot respond to a single-tap. Only a few try to. + */ - But on real devices, sometimes you get that, but sometimes you get: +- (void)initGestures +{ + UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleDoubleTap)]; + dtap.numberOfTapsRequired = 2; + dtap.numberOfTouchesRequired = 1; + + UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleTap)]; + stap.numberOfTapsRequired = 1; + stap.numberOfTouchesRequired = 1; + + UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] + initWithTarget:self + action:@selector(handlePan:)]; + pan.maximumNumberOfTouches = 1; + pan.minimumNumberOfTouches = 1; + + // I couldn't get Swipe to work, but using a second Pan recognizer works. + UIPanGestureRecognizer *pan2 = [[UIPanGestureRecognizer alloc] + initWithTarget:self + action:@selector(handlePan2:)]; + pan2.maximumNumberOfTouches = 2; + pan2.minimumNumberOfTouches = 2; + + // Also handle long-touch, and treat that the same as Pan. + // Without this, panning doesn't start until there's motion, so the trick + // of holding down your finger to freeze the scene doesn't work. + // + UILongPressGestureRecognizer *hold = [[UILongPressGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleLongPress:)]; + hold.numberOfTapsRequired = 0; + hold.numberOfTouchesRequired = 1; + hold.minimumPressDuration = 0.25; /* 1/4th second */ + + [stap requireGestureRecognizerToFail: dtap]; + [stap requireGestureRecognizerToFail: hold]; + [dtap requireGestureRecognizerToFail: hold]; + [pan requireGestureRecognizerToFail: hold]; - touchesBegan [touchA, touchB] - touchesEnd [touchB] - touchesEnd [touchA] + [self setMultipleTouchEnabled:YES]; - Or even + [self addGestureRecognizer: dtap]; + [self addGestureRecognizer: stap]; + [self addGestureRecognizer: pan]; + [self addGestureRecognizer: pan2]; + [self addGestureRecognizer: hold]; + + [dtap release]; + [stap release]; + [pan release]; + [pan2 release]; + [hold release]; +} + + +/* Given a mouse (touch) coordinate in unrotated, unscaled view coordinates, + convert it to what X11 and OpenGL expect. + + Getting this crap right is tricky, given the confusion of the various + scale factors, so here's a checklist that I think covers all of the X11 + and OpenGL cases. For each of these: rotate to all 4 orientations; + ensure the mouse tracks properly to all 4 corners. + + Test it in Xcode 6, because Xcode 5.0.2 can't run the iPhone6+ simulator. + + Test hacks must cover: + X11 ignoreRotation = true + X11 ignoreRotation = false + OpenGL (rotation is handled manually, so they never ignoreRotation) + + Test devices must cover: + contentScaleFactor = 1, hackedContentScaleFactor = 1 (iPad 2) + contentScaleFactor = 2, hackedContentScaleFactor = 1 (iPad Retina Air) + contentScaleFactor = 2, hackedContentScaleFactor = 2 (iPhone 5 5s 6 6+) + + iPad 2: 768x1024 / 1 = 768x1024 + iPad Air: 1536x2048 / 2 = 768x1024 (iPad Retina is identical) + iPhone 4s: 640x960 / 2 = 320x480 + iPhone 5: 640x1136 / 2 = 320x568 (iPhone 5s and iPhone 6 are identical) + iPhone 6+: 640x1136 / 2 = 320x568 (nativeBounds 960x1704 nativeScale 3) + + Tests: + iPad2 iPadAir iPhone4s iPhone5 iPhone6+ + Attraction X yes Y Y Y Y Y + Fireworkx X no Y Y Y Y Y + Carousel GL yes Y Y Y Y Y + Voronoi GL no Y Y Y Y Y + */ +- (void) convertMouse:(int)rot x:(int*)x y:(int *)y +{ + int xx = *x, yy = *y; - touchesBegan [touchA] - touchesBegan [touchB] - touchesEnd [touchA] - touchesEnd [touchB] +# if TARGET_IPHONE_SIMULATOR + { + XWindowAttributes xgwa; + XGetWindowAttributes (xdpy, xwindow, &xgwa); + NSLog (@"TOUCH %4d, %-4d in %4d x %-4d ig=%d rr=%d cs=%.0f hcs=%.0f\n", + *x, *y, + xgwa.width, xgwa.height, + ignore_rotation_p, [self reshapeRotatedWindow], + [self contentScaleFactor], + [self hackedContentScaleFactor]); + } +# endif // TARGET_IPHONE_SIMULATOR + + if (!ignore_rotation_p && [self reshapeRotatedWindow]) { + // + // For X11 hacks with ignoreRotation == false, we need to rotate the + // coordinates to match the unrotated X11 window. We do not do this + // for GL hacks, or for X11 hacks with ignoreRotation == true. + // + int w = [self frame].size.width; + int h = [self frame].size.height; + int swap; + switch (orientation) { + case UIDeviceOrientationLandscapeRight: + swap = xx; xx = h-yy; yy = swap; + break; + case UIDeviceOrientationLandscapeLeft: + swap = xx; xx = yy; yy = w-swap; + break; + case UIDeviceOrientationPortraitUpsideDown: + xx = w-xx; yy = h-yy; + default: + break; + } + } - So the only way to properly detect a "pinch" gesture is to remember - the start-point of each touch as it comes in; and the end-point of - each touch as those come in; and only process the gesture once the - number of touchEnds matches the number of touchBegins. - */ + double s = [self hackedContentScaleFactor]; + *x = xx * s; + *y = yy * s; -static void -rotate_mouse (int *x, int *y, int w, int h, int rot) -{ - int ox = *x, oy = *y; - if (rot > 45 && rot < 135) { *x = oy; *y = w-ox; } - else if (rot < -45 && rot > -135) { *x = h-oy; *y = ox; } - else if (rot > 135 || rot < -135) { *x = w-ox; *y = h-oy; } +# if TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__ + { + XWindowAttributes xgwa; + XGetWindowAttributes (xdpy, xwindow, &xgwa); + NSLog (@"touch %4d, %-4d in %4d x %-4d ig=%d rr=%d cs=%.0f hcs=%.0f\n", + *x, *y, + xgwa.width, xgwa.height, + ignore_rotation_p, [self reshapeRotatedWindow], + [self contentScaleFactor], + [self hackedContentScaleFactor]); + if (*x < 0 || *y < 0 || *x > xgwa.width || *y > xgwa.height) + abort(); + } +# endif // TARGET_IPHONE_SIMULATOR } -#if 0 // AudioToolbox/AudioToolbox.h -- (void) beep +/* Single click exits saver. + */ +- (void) handleTap { - // There's no way to play a standard system alert sound! - // We'd have to include our own WAV for that. Eh, fuck it. - AudioServicesPlaySystemSound (kSystemSoundID_Vibrate); -# if TARGET_IPHONE_SIMULATOR - NSLog(@"BEEP"); // The sim doesn't vibrate. -# endif + [self stopAndClose:NO]; } -#endif -/* We distinguish between taps and drags. - - Drags (down, motion, up) are sent to the saver to handle. - - Single-taps exit the saver. - This means a saver cannot respond to a single-tap. Only a few try to. +/* Double click sends Space KeyPress. */ +- (void) handleDoubleTap +{ + if (!xsft->event_cb || !xwindow) return; -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event -{ - tap_time = 0; - - if (xsft->event_cb && xwindow) { - double s = self.contentScaleFactor; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - int w = s * [self frame].size.width; - int h = s * [self frame].size.height; - for (UITouch *touch in touches) { - CGPoint p = [touch locationInView:self]; - xe.xany.type = ButtonPress; - xe.xbutton.button = i + 1; - xe.xbutton.button = i + 1; - xe.xbutton.x = s * p.x; - xe.xbutton.y = s * p.y; - rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle); - jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); - - // Ignore return code: don't care whether the hack handled it. - xsft->event_cb (xdpy, xwindow, xdata, &xe); - - // Remember when/where this was, to determine tap versus drag or hold. - tap_time = double_time(); - tap_point = p; - - i++; - break; // No pinches: only look at the first touch. - } - } + XEvent xe; + memset (&xe, 0, sizeof(xe)); + xe.xkey.keycode = ' '; + xe.xany.type = KeyPress; + BOOL ok1 = [self sendEvent: &xe]; + xe.xany.type = KeyRelease; + BOOL ok2 = [self sendEvent: &xe]; + if (!(ok1 || ok2)) + [self beep]; } -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event -{ - if (xsft->event_cb && xwindow) { - double s = self.contentScaleFactor; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - int w = s * [self frame].size.width; - int h = s * [self frame].size.height; - for (UITouch *touch in touches) { - CGPoint p = [touch locationInView:self]; - - // If the ButtonRelease came less than half a second after ButtonPress, - // and didn't move far, then this was a tap, not a drag or a hold. - // Interpret it as "exit". - // - double dist = sqrt (((p.x - tap_point.x) * (p.x - tap_point.x)) + - ((p.y - tap_point.y) * (p.y - tap_point.y))); - if (tap_time + 0.5 >= double_time() && dist < 20) { - [self stopAndClose]; - return; - } +/* Drag with one finger down: send MotionNotify. + */ +- (void) handlePan:(UIGestureRecognizer *)sender +{ + if (!xsft->event_cb || !xwindow) return; - xe.xany.type = ButtonRelease; - xe.xbutton.button = i + 1; - xe.xbutton.x = s * p.x; - xe.xbutton.y = s * p.y; - rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle); - jwxyz_mouse_moved (xdpy, xwindow, xe.xbutton.x, xe.xbutton.y); - xsft->event_cb (xdpy, xwindow, xdata, &xe); - i++; - break; // No pinches: only look at the first touch. - } + XEvent xe; + memset (&xe, 0, sizeof(xe)); + + CGPoint p = [sender locationInView:self]; // this is in points, not pixels + int x = p.x; + int y = p.y; + [self convertMouse: rot_current_angle x:&x y:&y]; + jwxyz_mouse_moved (xdpy, xwindow, x, y); + + switch (sender.state) { + case UIGestureRecognizerStateBegan: + xe.xany.type = ButtonPress; + xe.xbutton.button = 1; + xe.xbutton.x = x; + xe.xbutton.y = y; + break; + + case UIGestureRecognizerStateEnded: + xe.xany.type = ButtonRelease; + xe.xbutton.button = 1; + xe.xbutton.x = x; + xe.xbutton.y = y; + break; + + case UIGestureRecognizerStateChanged: + xe.xany.type = MotionNotify; + xe.xmotion.x = x; + xe.xmotion.y = y; + break; + + default: + break; } + + BOOL ok = [self sendEvent: &xe]; + if (!ok && xe.xany.type == ButtonRelease) + [self beep]; } -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event -{ - if (xsft->event_cb && xwindow) { - double s = self.contentScaleFactor; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - int w = s * [self frame].size.width; - int h = s * [self frame].size.height; - for (UITouch *touch in touches) { - CGPoint p = [touch locationInView:self]; - xe.xany.type = MotionNotify; - xe.xmotion.x = s * p.x; - xe.xmotion.y = s * p.y; - rotate_mouse (&xe.xbutton.x, &xe.xbutton.y, w, h, rot_current_angle); - jwxyz_mouse_moved (xdpy, xwindow, xe.xmotion.x, xe.xmotion.y); - xsft->event_cb (xdpy, xwindow, xdata, &xe); - i++; - break; // No pinches: only look at the first touch. - } - } +/* Hold one finger down: assume we're about to start dragging. + Treat the same as Pan. + */ +- (void) handleLongPress:(UIGestureRecognizer *)sender +{ + [self handlePan:sender]; +} + + + +/* Drag with 2 fingers down: send arrow keys. + */ +- (void) handlePan2:(UIPanGestureRecognizer *)sender +{ + if (!xsft->event_cb || !xwindow) return; + + if (sender.state != UIGestureRecognizerStateEnded) + return; + + XEvent xe; + memset (&xe, 0, sizeof(xe)); + + CGPoint p = [sender locationInView:self]; // this is in points, not pixels + int x = p.x; + int y = p.y; + [self convertMouse: rot_current_angle x:&x y:&y]; + + if (abs(x) > abs(y)) + xe.xkey.keycode = (x > 0 ? XK_Right : XK_Left); + else + xe.xkey.keycode = (y > 0 ? XK_Down : XK_Up); + + BOOL ok1 = [self sendEvent: &xe]; + xe.xany.type = KeyRelease; + BOOL ok2 = [self sendEvent: &xe]; + if (!(ok1 || ok2)) + [self beep]; } @@ -1588,10 +2633,88 @@ rotate_mouse (int *x, int *y, int w, int h, int rot) } } +- (NSDictionary *)getGLProperties +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, + nil]; +} + +- (void)addExtraRenderbuffers:(CGSize)size +{ + // No extra renderbuffers are needed for 2D screenhacks. +} #endif // USE_IPHONE +- (void) checkForUpdates +{ +# ifndef USE_IPHONE + // We only check once at startup, even if there are multiple screens, + // and even if this saver is running for many days. + // (Uh, except this doesn't work because this static isn't shared, + // even if we make it an exported global. Not sure why. Oh well.) + static BOOL checked_p = NO; + if (checked_p) return; + checked_p = YES; + + // If it's off, don't bother running the updater. Otherwise, the + // updater will decide if it's time to hit the network. + if (! get_boolean_resource (xdpy, + SUSUEnableAutomaticChecksKey, + SUSUEnableAutomaticChecksKey)) + return; + + NSString *updater = @"XScreenSaverUpdater.app"; + + // There may be multiple copies of the updater: e.g., one in /Applications + // and one in the mounted installer DMG! It's important that we run the + // one from the disk and not the DMG, so search for the right one. + // + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSArray *search = + @[[[bundle bundlePath] stringByDeletingLastPathComponent], + [@"~/Library/Screen Savers" stringByExpandingTildeInPath], + @"/Library/Screen Savers", + @"/System/Library/Screen Savers", + @"/Applications", + @"/Applications/Utilities"]; + NSString *app_path = nil; + for (NSString *dir in search) { + NSString *p = [dir stringByAppendingPathComponent:updater]; + if ([[NSFileManager defaultManager] fileExistsAtPath:p]) { + app_path = p; + break; + } + } + + if (! app_path) + app_path = [workspace fullPathForApplication:updater]; + + if (app_path && [app_path hasPrefix:@"/Volumes/XScreenSaver "]) + app_path = 0; // The DMG version will not do. + + if (!app_path) { + NSLog(@"Unable to find %@", updater); + return; + } + + NSError *err = nil; + if (! [workspace launchApplicationAtURL:[NSURL fileURLWithPath:app_path] + options:(NSWorkspaceLaunchWithoutAddingToRecents | + NSWorkspaceLaunchWithoutActivation | + NSWorkspaceLaunchAndHide) + configuration:nil + error:&err]) { + NSLog(@"Unable to launch %@: %@", app_path, err); + } + +# endif // !USE_IPHONE +} + + @end /* Utility functions...