X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=OSX%2FXScreenSaverView.m;h=657835b9c9b1e5ebe9eeeb4e045cd616b2c75f6a;hb=88cfe534a698a0562e81345957a50714af1453bc;hp=a02a5067d3bf9da8515a5b24bcc457abb3263953;hpb=019de959b265701cd0c3fccbb61f2b69f06bf9ee;p=xscreensaver diff --git a/OSX/XScreenSaverView.m b/OSX/XScreenSaverView.m index a02a5067..657835b9 100644 --- a/OSX/XScreenSaverView.m +++ b/OSX/XScreenSaverView.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 2006-2013 Jamie Zawinski +/* 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 @@ -16,6 +16,7 @@ */ #import +#import #import #import "XScreenSaverView.h" #import "XScreenSaverConfigSheet.h" @@ -24,6 +25,9 @@ #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. @@ -36,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 @@ -47,6 +55,8 @@ 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. @@ -139,8 +149,7 @@ extern NSDictionary *make_function_table_dict(void); // ios-function-table.m NSLog (@"no symbol \"%@\" for \"%@\"", table_name, path); # else // USE_IPHONE - // Remember: any time you add a new saver to the iOS app, - // manually run "make ios-function-table.m"! + // 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]; @@ -258,7 +267,7 @@ add_default_options (const XrmOptionDescRec *opts, # endif // ".textLiteral: ", // ".textFile: ", - ".textURL: http://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss", + ".textURL: https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss", // ".textProgram: ", ".grabDesktopImages: yes", # ifndef USE_IPHONE @@ -268,6 +277,7 @@ add_default_options (const XrmOptionDescRec *opts, # endif ".imageDirectory: ~/Pictures", ".relaunchDelay: 2", + ".texFontCacheSize: 30", # ifndef USE_IPHONE # define STR1(S) #S @@ -354,19 +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 - initial_bounds = frame.size; - 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; @@ -378,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); @@ -410,29 +423,53 @@ double_time (void) progname = progclass = xsft->progclass; next_frame_time = 0; - -# ifdef USE_BACKBUFFER - [self createBackbuffer]; - [self initLayer]; + +# 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 + 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; } -- (void) initLayer + +#ifdef USE_IPHONE ++ (Class) layerClass { -# if !defined(USE_IPHONE) && defined(USE_CALAYER) - [self setLayer: [CALayer layer]]; - self.layer.delegate = self; - self.layer.opaque = YES; - [self setWantsLayer: YES]; -# endif // !USE_IPHONE && USE_CALAYER + return [CAEAGLLayer class]; } +#endif - (id) initWithFrame:(NSRect)frame isPreview:(BOOL)p @@ -443,23 +480,26 @@ 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 + [[NSNotificationCenter defaultCenter] removeObserver:self]; +# endif # ifdef USE_BACKBUFFER - if (backbuffer) - CGContextRelease (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); -# ifndef USE_CALAYER - if (window_ctx) - CGContextRelease (window_ctx); -# endif // !USE_CALAYER - # endif // USE_BACKBUFFER [prefsReader release]; @@ -504,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 @@ -511,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]; @@ -535,8 +589,165 @@ double_time (void) [[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 { @@ -556,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; @@ -580,19 +797,38 @@ double_time (void) [[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 + } } @@ -623,11 +859,30 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure) */ - (CGFloat) hackedContentScaleFactor { - GLfloat s = [self contentScaleFactor]; - if (initial_bounds.width >= 1024 || - initial_bounds.height >= 1024) - s = 1; - return s; + NSSize ssize = [[[UIScreen mainScreen] currentMode] size]; + NSSize bsize = [self bounds].size; + + CGFloat + max_ssize = ssize.width > ssize.height ? ssize.width : ssize.height, + max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height; + + // 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); } @@ -655,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]; @@ -685,11 +946,11 @@ double current_device_rotation (void) # undef CLAMP180 - double s = [self hackedContentScaleFactor]; - if (!ignore_rotation_p && - /* rotation_ratio && */ - ((int) backbuffer_size.width != (int) (s * rot_current_size.width) || - (int) backbuffer_size.height != (int) (s * rot_current_size.height))) + 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]; } @@ -725,110 +986,345 @@ double current_device_rotation (void) #ifdef USE_BACKBUFFER -/* Create a bitmap context into which we render everything. - If the desired size has changed, re-created it. - */ -- (void) createBackbuffer +# ifndef USE_IPHONE + +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); +} + +# else + +static GLboolean +gluCheckExtension (const GLubyte *ext_name, const GLubyte *ext_string) +{ + size_t ext_len = strlen ((const char *)ext_name); + + 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; + } + + ext_string = found + ext_len; + } + + return GL_FALSE; +} + +# endif + +/* Called during startAnimation before the first call to createBackbuffer. */ +- (void) enableBackbuffer:(CGSize)new_backbuffer_size { +# ifndef USE_IPHONE + struct gl_version version; + + { + 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); + + glGenTextures (1, &backbuffer_texture); + + // On really old systems, it would make sense to split the texture + // into subsections +# ifndef USE_IPHONE + 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); + +# 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 - double s = [self hackedContentScaleFactor]; - CGSize rotsize = ignore_rotation_p ? initial_bounds : rot_current_size; - int new_w = s * rotsize.width; - int new_h = s * rotsize.height; + 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 - int new_w = [self bounds].size.width; - int new_h = [self bounds].size.height; + 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); -# ifndef USE_CALAYER - if (window_ctx) - CGContextRelease (window_ctx); -# endif + +# 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]; -# ifndef USE_CALAYER - // TODO: This was borrowed from jwxyz_window_resized, and should - // probably be refactored. - - // Figure out which screen the window is currently on. - CGDirectDisplayID cgdpy = 0; - - { -// int wx, wy; -// TODO: XTranslateCoordinates is returning (0,1200) on my system. -// Is this right? -// In any case, those weren't valid coordinates for CGGetDisplaysWithPoint. -// XTranslateCoordinates (xdpy, xwindow, NULL, 0, 0, &wx, &wy, NULL); -// p.x = wx; -// p.y = wy; - - NSPoint p0 = {0, 0}; - p0 = [window convertBaseToScreen:p0]; - CGPoint p = {p0.x, p0.y}; - CGDisplayCount n; - CGGetDisplaysWithPoint (p, 1, &cgdpy, &n); - NSAssert (cgdpy, @"unable to find CGDisplay"); - } - - { - // Figure out this screen's colorspace, and use that for every CGImage. - // - CMProfileRef profile = 0; - - // CMGetProfileByAVID is deprecated as of OS X 10.6, but there's no - // documented replacement as of OS X 10.9. - // http://lists.apple.com/archives/colorsync-dev/2012/Nov/msg00001.html - CMGetProfileByAVID ((CMDisplayIDType) cgdpy, &profile); - NSAssert (profile, @"unable to find colorspace profile"); - colorspace = CGColorSpaceCreateWithPlatformColorSpace (profile); - NSAssert (colorspace, @"unable to find colorspace"); - } -# else // USE_CALAYER +# ifdef BACKBUFFER_OPENGL // Was apparently faster until 10.9. colorspace = CGColorSpaceCreateDeviceRGB (); -# endif // USE_CALAYER +# endif // BACKBUFFER_OPENGL -# ifndef USE_CALAYER - window_ctx = [[window graphicsContext] graphicsPort]; - CGContextRetain (window_ctx); -# endif // !USE_CALAYER - [self unlockFocus]; } else { -# ifndef USE_CALAYER - window_ctx = NULL; -# endif // !USE_CALAYER colorspace = CGColorSpaceCreateDeviceRGB(); } if (backbuffer && - backbuffer_size.width == new_w && - backbuffer_size.height == new_h) + (int)backbuffer_size.width == (int)new_size.width && + (int)backbuffer_size.height == (int)new_size.height) return; - CGSize osize = backbuffer_size; 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_size.width = new_w; - backbuffer_size.height = new_h; + backbuffer_data = NULL; + gl_texture_w = (int)backbuffer_size.width; + gl_texture_h = (int)backbuffer_size.height; - backbuffer = CGBitmapContextCreate (NULL, - backbuffer_size.width, - backbuffer_size.height, - 8, - backbuffer_size.width * 4, + 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, - // kCGImageAlphaPremultipliedLast - (kCGImageAlphaNoneSkipFirst | - kCGBitmapByteOrder32Host) - ); + bitmap_info); NSAssert (backbuffer, @"unable to allocate back buffer"); // Clear it. @@ -838,19 +1334,141 @@ double current_device_rotation (void) 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; + + CGRect rect; // pixels, not points rect.origin.x = 0; rect.origin.y = (backbuffer_size.height - osize.height); - rect.size = osize; + 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 @@ -860,21 +1478,39 @@ double current_device_rotation (void) { if (!xwindow) return; // early + CGSize new_size; // pixels, not points + # ifdef USE_BACKBUFFER - [self createBackbuffer]; - jwxyz_window_resized (xdpy, xwindow, - 0, 0, - backbuffer_size.width, backbuffer_size.height, + + [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 - NSRect r = [self frame]; // ignoring rotation is closer - r.size = [self bounds].size; // to what XGetGeometry expects. - jwxyz_window_resized (xdpy, xwindow, - r.origin.x, r.origin.y, - r.size.width, r.size.height, + 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; } @@ -951,11 +1587,14 @@ double current_device_rotation (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; } @@ -984,36 +1623,57 @@ 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]; + +# ifndef USE_IPHONE + if (ogl_ctx) + [ogl_ctx setView:self]; +# endif // !USE_IPHONE + NSRect r; # ifndef USE_BACKBUFFER r = [self bounds]; @@ -1037,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)]; @@ -1094,19 +1753,6 @@ double current_device_rotation (void) } -/* drawRect always does nothing, and animateOneFrame renders bits to the - screen. This is (now) true of both X11 and GL on both MacOS and iOS. - */ - -- (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 @@ -1129,121 +1775,10 @@ double current_device_rotation (void) [self render_x11]; -# ifdef USE_IPHONE +# if defined USE_IPHONE && defined USE_BACKBUFFER UIGraphicsPopContext(); # endif - -# ifdef USE_IPHONE - // Then compute the transformations for rotation. - double hs = [self hackedContentScaleFactor]; - double s = [self contentScaleFactor]; - - // The rotation origin for layer.affineTransform is in the center already. - CGAffineTransform t = ignore_rotation_p ? - CGAffineTransformIdentity : - CGAffineTransformMakeRotation (rot_current_angle / (180.0 / M_PI)); - - CGFloat f = s / hs; - self.layer.affineTransform = CGAffineTransformScale(t, f, f); - - CGRect bounds; - bounds.origin.x = 0; - bounds.origin.y = 0; - bounds.size.width = backbuffer_size.width / s; - bounds.size.height = backbuffer_size.height / s; - self.layer.bounds = bounds; -# endif // USE_IPHONE - -# ifdef USE_CALAYER - [self.layer setNeedsDisplay]; -# else // !USE_CALAYER - size_t - w = CGBitmapContextGetWidth (backbuffer), - h = CGBitmapContextGetHeight (backbuffer); - - size_t bpl = CGBitmapContextGetBytesPerRow (backbuffer); - CGDataProviderRef prov = CGDataProviderCreateWithData (NULL, - CGBitmapContextGetData(backbuffer), - bpl * h, - NULL); - - - CGImageRef img = CGImageCreate (w, h, - 8, 32, - CGBitmapContextGetBytesPerRow(backbuffer), - colorspace, - CGBitmapContextGetBitmapInfo(backbuffer), - prov, NULL, NO, - kCGRenderingIntentDefault); - - CGDataProviderRelease (prov); - - CGRect rect; - rect.origin.x = 0; - rect.origin.y = 0; - rect.size = backbuffer_size; - CGContextDrawImage (window_ctx, rect, img); - - CGImageRelease (img); - - CGContextFlush (window_ctx); -# endif // !USE_CALAYER -} - -# ifdef USE_CALAYER - -- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx -{ - // This "isn't safe" if NULL is passed to CGBitmapCreateContext before iOS 4. - char *dest_data = (char *)CGBitmapContextGetData (ctx); - - // The CGContext here is normally upside-down on iOS. - if (dest_data && - CGBitmapContextGetBitmapInfo (ctx) == - (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host) -# ifdef USE_IPHONE - && CGContextGetCTM (ctx).d < 0 -# endif // USE_IPHONE - ) - { - size_t dest_height = CGBitmapContextGetHeight (ctx); - size_t dest_bpr = CGBitmapContextGetBytesPerRow (ctx); - size_t src_height = CGBitmapContextGetHeight (backbuffer); - size_t src_bpr = CGBitmapContextGetBytesPerRow (backbuffer); - char *src_data = (char *)CGBitmapContextGetData (backbuffer); - - size_t height = src_height < dest_height ? src_height : dest_height; - - if (src_bpr == dest_bpr) { - // iPad 1: 4.0 ms, iPad 2: 6.7 ms - memcpy (dest_data, src_data, src_bpr * height); - } else { - // iPad 1: 4.6 ms, iPad 2: 7.2 ms - size_t bpr = src_bpr < dest_bpr ? src_bpr : dest_bpr; - while (height) { - memcpy (dest_data, src_data, bpr); - --height; - src_data += src_bpr; - dest_data += dest_bpr; - } - } - } else { - - // iPad 1: 9.6 ms, iPad 2: 12.1 ms - -# ifdef USE_IPHONE - CGContextScaleCTM (ctx, 1, -1); - CGFloat s = [self contentScaleFactor]; - CGFloat hs = [self hackedContentScaleFactor]; - CGContextTranslateCTM (ctx, 0, -backbuffer_size.height * hs / s); -# endif // USE_IPHONE - - CGImageRef img = CGBitmapContextCreateImage (backbuffer); - CGContextDrawImage (ctx, self.layer.bounds, img); - CGImageRelease (img); - } } -# endif // USE_CALAYER #endif // USE_BACKBUFFER @@ -1295,9 +1830,9 @@ double current_device_rotation (void) UInt32 usize = * (UInt32 *) (data.bytes + data.length - 4); data2 = [NSMutableData dataWithLength: usize]; zs.next_in = (Bytef *) data.bytes; - zs.avail_in = data.length; + zs.avail_in = (uint) data.length; zs.next_out = (Bytef *) data2.bytes; - zs.avail_out = data2.length; + zs.avail_out = (uint) data2.length; ret = inflate (&zs, Z_FINISH); inflateEnd (&zs); } @@ -1347,7 +1882,7 @@ double current_device_rotation (void) // #### am I expected to retain this, or not? wtf. // I thought not, but if I don't do this, we (sometimes) crash. // #### Analyze says "potential leak of an object stored into sheet" - [sheet retain]; + // [sheet retain]; return sheet; } @@ -1367,19 +1902,55 @@ double current_device_rotation (void) } +- (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)); @@ -1456,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 = @@ -1477,80 +2060,93 @@ double current_device_rotation (void) 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 @@ -1564,77 +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]; - } - - UIView *fader = [self superview]; // the "backgroundView" view is our parent +// UIViewController *v = [[self window] rootViewController]; +// if ([v isKindOfClass: [UINavigationController class]]) { +// UINavigationController *n = (UINavigationController *) v; +// [[n topViewController] becomeFirstResponder]; +// } + [self resignFirstResponder]; if (relaunch_p) { // Fake a shake on the SaverListController. - // Why is [self window] sometimes null here? - UIWindow *w = [[UIApplication sharedApplication] keyWindow]; - UIViewController *v = [w rootViewController]; - if ([v isKindOfClass: [UINavigationController class]]) { - UINavigationController *n = (UINavigationController *) v; - [[n topViewController] motionEnded: UIEventSubtypeMotionShake - withEvent: nil]; - } + [_delegate didShake:self]; } else { // Not launching another, animate our return to the list. - [UIView animateWithDuration: 0.5 - animations:^{ fader.alpha = 0.0; } - completion:^(BOOL finished) { - [fader removeFromSuperview]; - fader.alpha = 1.0; - }]; +# if TARGET_IPHONE_SIMULATOR + NSLog (@"fading back to saver list"); +# endif + [_delegate wantsFadeOut:self]; } } +/* 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 +{ + 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. - 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. + The savers are under a different UIWindow and a UINavigationController + that does not do automatic rotation. - So, we have to hack the rotation animation manually, in the GL world. + 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. - Possibly XScreenSaverView should use Core Animation, and - XScreenSaverGLView should override that. + During and after rotation, the size/shape of the X11 window changes, + and ConfigureNotify events are generated. + + 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 @@ -1649,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(); @@ -1696,6 +2297,21 @@ double current_device_rotation (void) 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. @@ -1705,178 +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. - - Currently we don't handle multi-touches (just the first touch) but - I'm leaving this comment here for future reference: - - In the simulator, multi-touch sequences look like this: +/* We distinguish between taps and drags. - touchesBegan [touchA, touchB] - touchesEnd [touchA, touchB] + - 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. - But on real devices, sometimes you get that, but sometimes you get: + This means a saver cannot respond to a single-tap. Only a few try to. + */ - touchesBegan [touchA, touchB] - touchesEnd [touchB] - touchesEnd [touchA] +- (void)initGestures +{ + UITapGestureRecognizer *dtap = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(handleDoubleTap)]; + dtap.numberOfTapsRequired = 2; + dtap.numberOfTouchesRequired = 1; - Or even + 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] - touchesBegan [touchB] - touchesEnd [touchA] - touchesEnd [touchB] + [self setMultipleTouchEnabled:YES]; - 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. + [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) rotateMouse:(int)rot x:(int*)x y:(int *)y w:(int)w h:(int)h +- (void) convertMouse:(int)rot x:(int*)x y:(int *)y { - // This is a no-op unless contentScaleFactor != hackedContentScaleFactor. - // Currently, this is the iPad Retina only. - CGRect frame = [self bounds]; // Scale. + int xx = *x, yy = *y; + +# 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; + } + } + double s = [self hackedContentScaleFactor]; - *x *= (backbuffer_size.width / frame.size.width) / s; - *y *= (backbuffer_size.height / frame.size.height) / s; + *x = xx * s; + *y = yy * s; + +# 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)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +- (void) handleDoubleTap { - // If they are trying to pinch, just do nothing. - if ([[event allTouches] count] > 1) - return; + if (!xsft->event_cb || !xwindow) return; - tap_time = 0; - - if (xsft->event_cb && xwindow) { - double s = [self hackedContentScaleFactor]; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - // #### 'frame' here or 'bounds'? - 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; - [self rotateMouse: rot_current_angle - x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; - 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 +/* Drag with one finger down: send MotionNotify. + */ +- (void) handlePan:(UIGestureRecognizer *)sender { - // If they are trying to pinch, just do nothing. - if ([[event allTouches] count] > 1) - return; + if (!xsft->event_cb || !xwindow) return; - if (xsft->event_cb && xwindow) { - double s = [self hackedContentScaleFactor]; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - // #### 'frame' here or 'bounds'? - 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:NO]; - return; - } + XEvent xe; + memset (&xe, 0, sizeof(xe)); - xe.xany.type = ButtonRelease; - xe.xbutton.button = i + 1; - xe.xbutton.x = s * p.x; - xe.xbutton.y = s * p.y; - [self rotateMouse: rot_current_angle - x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; - 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. - } + 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]; +} + + +/* Hold one finger down: assume we're about to start dragging. + Treat the same as Pan. + */ +- (void) handleLongPress:(UIGestureRecognizer *)sender +{ + [self handlePan:sender]; } -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event + +/* Drag with 2 fingers down: send arrow keys. + */ +- (void) handlePan2:(UIPanGestureRecognizer *)sender { - // If they are trying to pinch, just do nothing. - if ([[event allTouches] count] > 1) + if (!xsft->event_cb || !xwindow) return; + + if (sender.state != UIGestureRecognizerStateEnded) return; - if (xsft->event_cb && xwindow) { - double s = [self hackedContentScaleFactor]; - XEvent xe; - memset (&xe, 0, sizeof(xe)); - int i = 0; - // #### 'frame' here or 'bounds'? - 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; - [self rotateMouse: rot_current_angle - x: &xe.xbutton.x y: &xe.xbutton.y w: w h: h]; - 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. - } - } + 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]; } @@ -1917,6 +2633,18 @@ double current_device_rotation (void) } } +- (NSDictionary *)getGLProperties +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, + nil]; +} + +- (void)addExtraRenderbuffers:(CGSize)size +{ + // No extra renderbuffers are needed for 2D screenhacks. +} + #endif // USE_IPHONE @@ -1930,18 +2658,60 @@ double current_device_rotation (void) 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; // If it's off, don't bother running the updater. + return; - // Otherwise, the updater will decide if it's time to hit the network. - NSString *updater = @"XScreenSaverUpdater"; - if (! [[NSWorkspace sharedWorkspace] - launchApplication:updater showIcon:NO autolaunch:NO]) { - NSLog(@"Unable to launch %@", updater); + 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; + } } -# endif // USE_IPHONE + + 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 }