From http://www.jwz.org/xscreensaver/xscreensaver-5.36.tar.gz
[xscreensaver] / jwxyz / jwxyz-cocoa.m
1 /* xscreensaver, Copyright (c) 1991-2016 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or 
9  * implied warranty.
10  */
11
12 /* JWXYZ Is Not Xlib.
13
14    But it's a bunch of function definitions that bear some resemblance to
15    Xlib and that do Cocoa-ish or OpenGL-ish things that bear some resemblance
16    to the things that Xlib might have done.
17
18    This code is used by both the original jwxyz.m and the new jwxyz-gl.c.
19  */
20
21 #import "jwxyzI.h"
22 #import "jwxyz-cocoa.h"
23
24 #include <stdarg.h>
25
26 #ifdef USE_IPHONE
27 # import <OpenGLES/ES1/gl.h>
28 # import <OpenGLES/ES1/glext.h>
29 # define NSOpenGLContext EAGLContext
30 #endif
31
32 /* OS X/iOS-specific JWXYZ implementation. */
33
34 void
35 jwxyz_logv (Bool error, const char *fmt, va_list args)
36 {
37   vfprintf (stderr, fmt, args);
38   fputc ('\n', stderr);
39 }
40
41 /* Instead of calling abort(), throw a real exception, so that
42    XScreenSaverView can catch it and display a dialog.
43  */
44 void
45 jwxyz_abort (const char *fmt, ...)
46 {
47   char s[10240];
48   if (!fmt || !*fmt)
49     strcpy (s, "abort");
50   else
51     {
52       va_list args;
53       va_start (args, fmt);
54       vsprintf (s, fmt, args);
55       va_end (args);
56     }
57   [[NSException exceptionWithName: NSInternalInconsistencyException
58                 reason: [NSString stringWithCString: s
59                                   encoding:NSUTF8StringEncoding]
60                 userInfo: nil]
61     raise];
62   abort();  // not reached
63 }
64
65
66 const XRectangle *
67 jwxyz_frame (Drawable d)
68 {
69   return &d->frame;
70 }
71
72
73 unsigned int
74 jwxyz_drawable_depth (Drawable d)
75 {
76   return (d->type == WINDOW
77           ? visual_depth (NULL, NULL)
78           : d->pixmap.depth);
79 }
80
81
82 void
83 jwxyz_get_pos (Window w, XPoint *xvpos, XPoint *xp)
84 {
85 # ifdef USE_IPHONE
86
87   xvpos->x = 0;
88   xvpos->y = 0;
89
90   if (xp) {
91     xp->x = w->window.last_mouse_x;
92     xp->y = w->window.last_mouse_y;
93   }
94
95 # else  // !USE_IPHONE
96
97   NSWindow *nsw = [w->window.view window];
98
99   // get bottom left of window on screen, from bottom left
100
101 # if (MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_6)
102   NSRect rr1 = [w->window.view convertRect: NSMakeRect(0,0,0,0) toView:nil];
103   NSRect rr2 = [nsw convertRectToScreen: rr1];
104   NSPoint wpos = NSMakePoint (rr2.origin.x - rr1.origin.x,
105                               rr2.origin.y - rr1.origin.y);
106 # else
107   // deprecated as of 10.7
108   NSPoint wpos = [nsw convertBaseToScreen: NSMakePoint(0,0)];
109 # endif
110
111
112   // get bottom left of view on window, from bottom left
113   NSPoint vpos;
114   vpos.x = vpos.y = 0;
115   vpos = [w->window.view convertPoint:vpos toView:[nsw contentView]];
116
117   // get bottom left of view on screen, from bottom left
118   vpos.x += wpos.x;
119   vpos.y += wpos.y;
120
121   // get top left of view on screen, from bottom left
122   vpos.y += w->frame.height;
123
124   // get top left of view on screen, from top left
125   NSArray *screens = [NSScreen screens];
126   NSScreen *screen = (screens && [screens count] > 0
127                       ? [screens objectAtIndex:0]
128                       : [NSScreen mainScreen]);
129   NSRect srect = [screen frame];
130   vpos.y = srect.size.height - vpos.y;
131
132   xvpos->x = vpos.x;
133   xvpos->y = vpos.y;
134
135   if (xp) {
136     // get the mouse position on window, from bottom left
137     NSEvent *e = [NSApp currentEvent];
138     NSPoint p = [e locationInWindow];
139
140     // get mouse position on screen, from bottom left
141     p.x += wpos.x;
142     p.y += wpos.y;
143
144     // get mouse position on screen, from top left
145     p.y = srect.size.height - p.y;
146
147     xp->x = (int) p.x;
148     xp->y = (int) p.y;
149   }
150
151 # endif // !USE_IPHONE
152 }
153
154
155 #ifdef USE_IPHONE
156
157 void
158 check_framebuffer_status (void)
159 {
160   int err = glCheckFramebufferStatusOES (GL_FRAMEBUFFER_OES);
161   switch (err) {
162   case GL_FRAMEBUFFER_COMPLETE_OES:
163     break;
164   case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
165     Assert (0, "framebuffer incomplete attachment");
166     break;
167   case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
168     Assert (0, "framebuffer incomplete missing attachment");
169     break;
170   case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
171     Assert (0, "framebuffer incomplete dimensions");
172     break;
173   case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
174     Assert (0, "framebuffer incomplete formats");
175     break;
176   case GL_FRAMEBUFFER_UNSUPPORTED_OES:
177     Assert (0, "framebuffer unsupported");
178     break;
179 /*
180   case GL_FRAMEBUFFER_UNDEFINED:
181     Assert (0, "framebuffer undefined");
182     break;
183   case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
184     Assert (0, "framebuffer incomplete draw buffer");
185     break;
186   case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
187     Assert (0, "framebuffer incomplete read buffer");
188     break;
189   case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE:
190     Assert (0, "framebuffer incomplete multisample");
191     break;
192   case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS:
193     Assert (0, "framebuffer incomplete layer targets");
194     break;
195  */
196   default:
197     NSCAssert (0, @"framebuffer incomplete, unknown error 0x%04X", err);
198     break;
199   }
200 }
201
202
203 void
204 create_framebuffer (GLuint *gl_framebuffer, GLuint *gl_renderbuffer)
205 {
206   glGenFramebuffersOES  (1, gl_framebuffer);
207   glBindFramebufferOES  (GL_FRAMEBUFFER_OES,  *gl_framebuffer);
208
209   glGenRenderbuffersOES (1, gl_renderbuffer);
210   glBindRenderbufferOES (GL_RENDERBUFFER_OES, *gl_renderbuffer);
211 }
212
213 #endif // USE_IPHONE
214
215
216 #if defined JWXYZ_QUARTZ
217
218 /* Pushes a GC context; sets Fill and Stroke colors to the background color.
219  */
220 static void
221 push_bg_gc (Display *dpy, Drawable d, GC gc, Bool fill_p)
222 {
223   XGCValues *gcv = jwxyz_gc_gcv (gc);
224   push_color_gc (dpy, d, gc, gcv->background, gcv->antialias_p, fill_p);
225 }
226
227
228 void
229 jwxyz_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc,
230                  int src_x, int src_y,
231                  unsigned int width, unsigned int height,
232                  int dst_x, int dst_y)
233 {
234   XRectangle src_frame = src->frame, dst_frame = dst->frame;
235   XGCValues *gcv = jwxyz_gc_gcv (gc);
236
237   BOOL mask_p = src->type == PIXMAP && src->pixmap.depth == 1;
238
239
240   /* If we're copying from a bitmap to a bitmap, and there's nothing funny
241      going on with clipping masks or depths or anything, optimize it by
242      just doing a memcpy instead of going through a CGI.
243    */
244   if (gcv->function == GXcopy &&
245       !gcv->clip_mask &&
246       jwxyz_drawable_depth (src) == jwxyz_drawable_depth (dst)) {
247
248     Assert(!src_frame.x &&
249            !src_frame.y &&
250            !dst_frame.x &&
251            !dst_frame.y,
252            "unexpected non-zero origin");
253
254     ptrdiff_t src_pitch = CGBitmapContextGetBytesPerRow(src->cgc);
255     ptrdiff_t dst_pitch = CGBitmapContextGetBytesPerRow(dst->cgc);
256     char *src_data = seek_xy (CGBitmapContextGetData (src->cgc), src_pitch,
257                               src_x, src_y);
258     char *dst_data = seek_xy (CGBitmapContextGetData (dst->cgc), dst_pitch,
259                               dst_x, dst_y);
260
261     size_t bytes = width * 4;
262
263     if (src == dst && dst_y > src_y) {
264       // Copy upwards if the areas might overlap.
265       src_data += src_pitch * (height - 1);
266       dst_data += dst_pitch * (height - 1);
267       src_pitch = -src_pitch;
268       dst_pitch = -dst_pitch;
269     }
270
271     while (height) {
272       // memcpy is an alias for memmove on OS X.
273       memmove(dst_data, src_data, bytes);
274       src_data += src_pitch;
275       dst_data += dst_pitch;
276       --height;
277     }
278   } else {
279     CGRect
280     src_rect = CGRectMake(src_x, src_y, width, height),
281     dst_rect = CGRectMake(dst_x, dst_y, width, height);
282
283     src_rect.origin = map_point (src, src_rect.origin.x,
284                                  src_rect.origin.y + src_rect.size.height);
285     dst_rect.origin = map_point (dst, dst_rect.origin.x,
286                                  dst_rect.origin.y + src_rect.size.height);
287
288     NSObject *releaseme = 0;
289     CGImageRef cgi;
290     BOOL free_cgi_p = NO;
291
292     // We must first copy the bits to an intermediary CGImage object, then
293     // copy that to the destination drawable's CGContext.
294     //
295     // First we get a CGImage out of the pixmap CGContext -- it's the whole
296     // pixmap, but it presumably shares the data pointer instead of copying
297     // it.  We then cache that CGImage it inside the Pixmap object.  Note:
298     // invalidate_drawable_cache() must be called to discard this any time a
299     // modification is made to the pixmap, or we'll end up re-using old bits.
300     //
301     if (!src->cgi)
302       src->cgi = CGBitmapContextCreateImage (src->cgc);
303     cgi = src->cgi;
304
305     // if doing a sub-rect, trim it down.
306     if (src_rect.origin.x    != src_frame.x   ||
307         src_rect.origin.y    != src_frame.y   ||
308         src_rect.size.width  != src_frame.width ||
309         src_rect.size.height != src_frame.height) {
310       // #### I don't understand why this is needed...
311       src_rect.origin.y = (src_frame.height -
312                            src_rect.size.height - src_rect.origin.y);
313       // This does not copy image data, so it should be fast.
314       cgi = CGImageCreateWithImageInRect (cgi, src_rect);
315       free_cgi_p = YES;
316     }
317
318     CGContextRef cgc = dst->cgc;
319
320     if (mask_p) {               // src depth == 1
321
322       push_bg_gc (dpy, dst, gc, YES);
323
324       // fill the destination rectangle with solid background...
325       CGContextFillRect (cgc, dst_rect);
326
327       Assert (cgc, "no CGC with 1-bit XCopyArea");
328
329       // then fill in a solid rectangle of the fg color, using the image as an
330       // alpha mask.  (the image has only values of BlackPixel or WhitePixel.)
331       set_color (dpy, cgc, gcv->foreground, jwxyz_gc_depth (gc),
332                  gcv->alpha_allowed_p, YES);
333       CGContextClipToMask (cgc, dst_rect, cgi);
334       CGContextFillRect (cgc, dst_rect);
335
336       pop_gc (dst, gc);
337
338     } else {            // src depth > 1
339
340       push_gc (dst, gc);
341
342       // copy the CGImage onto the destination CGContext
343       //Assert(CGImageGetColorSpace(cgi) == dpy->colorspace, "bad colorspace");
344       CGContextDrawImage (cgc, dst_rect, cgi);
345
346       pop_gc (dst, gc);
347     }
348
349     if (free_cgi_p) CGImageRelease (cgi);
350     
351     if (releaseme) [releaseme release];
352   }
353   
354   invalidate_drawable_cache (dst);
355 }
356
357 #elif defined JWXYZ_GL
358
359 /* Warning! The JWXYZ_GL code here is experimental and provisional and not at
360    all ready for prime time. Please be careful.
361  */
362
363 void
364 jwxyz_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc,
365                  int src_x, int src_y,
366                  unsigned int width, unsigned int height,
367                  int dst_x, int dst_y)
368 {
369   /* TODO:
370    - glCopyPixels if src == dst
371    - Pixel buffer copying
372    - APPLE_framebuffer_multisample has ResolveMultisampleFramebufferAPPLE,
373      which is like a blit.
374    */
375
376   /* Strange and ugly flickering when going the glCopyTexImage2D route on
377      OS X. (Early 2009 Mac mini, OS X 10.10)
378    */
379 # ifdef USE_IPHONE
380   jwxyz_gl_copy_area_copy_tex_image (dpy, src, dst, gc, src_x, src_y,
381                                      width, height, dst_x, dst_y);
382 # else // !USE_IPHONE
383   jwxyz_gl_copy_area_read_pixels (dpy, src, dst, gc,
384                                   src_x, src_y, width, height, dst_x, dst_y);
385 # endif // !USE_IPHONE
386   jwxyz_assert_gl ();
387 }
388
389
390 void
391 jwxyz_assert_gl ()
392 {
393   // This is like check_gl_error, except this happens for debug builds only.
394 #ifndef __OPTIMIZE__
395   if([NSOpenGLContext currentContext])
396   {
397     // glFinish here drops FPS into the toilet. It might need to be on if
398     // something goes wrong.
399     // glFinish();
400     GLenum error = glGetError();
401     Assert (!error, "jwxyz_assert_gl: OpenGL error");
402   }
403 #endif // !__OPTIMIZE__
404 }
405
406 void
407 jwxyz_assert_drawable (Window main_window, Drawable d)
408 {
409 #if !defined USE_IPHONE && !defined __OPTIMIZE__
410   XScreenSaverView *view = main_window->window.view;
411   NSOpenGLContext *ogl_ctx = [view oglContext];
412
413   if (d->type == WINDOW) {
414     Assert([ogl_ctx view] == view,
415            "jwxyz_assert_display: ogl_ctx view not set!");
416   }
417
418   @try {
419     /* Assert([d->ctx isKindOfClass:[NSOpenGLContext class]], "Not a context."); */
420     Class c = [ogl_ctx class];
421     Assert([c isSubclassOfClass:[NSOpenGLContext class]], "Not a context.");
422     // [d->ctx makeCurrentContext];
423   }
424   @catch (NSException *exception) {
425     perror([[exception reason] UTF8String]);
426     abort();
427   }
428 #endif // !USE_IPHONE && !__OPTIMIZE__
429 }
430
431
432 void
433 jwxyz_bind_drawable (Window main_window, Drawable d)
434 {
435   /* Windows and Pixmaps need to use different contexts with OpenGL
436      screenhacks, because an OpenGL screenhack sets state in an arbitrary
437      fashion, but jwxyz-gl.c has its own ideas w.r.t. OpenGL state.
438
439      On iOS, all pixmaps can use the same context with different FBOs. Which
440      is nice.
441    */
442
443   /* OpenGL screenhacks in general won't be drawing on the Window, but they
444      can and will draw on a Pixmap -- but an OpenGL call following an Xlib
445      call won't be able to fix the fact that it's drawing offscreen.
446    */
447
448   /* EXT_direct_state_access might be appropriate, but it's apparently not
449      available on Mac OS X.
450    */
451
452   // jwxyz_assert_display (dpy);
453   jwxyz_assert_drawable (main_window, main_window);
454   jwxyz_assert_gl ();
455   jwxyz_assert_drawable (main_window, d);
456
457 #if defined USE_IPHONE && !defined __OPTIMIZE__
458   Drawable current_drawable = main_window->window.current_drawable;
459   Assert (!current_drawable
460             || current_drawable->ogl_ctx == [EAGLContext currentContext],
461           "bind_drawable: Wrong context.");
462   if (current_drawable) {
463     GLint framebuffer;
464     glGetIntegerv (GL_FRAMEBUFFER_BINDING_OES, &framebuffer);
465     Assert (framebuffer == current_drawable->gl_framebuffer,
466             "bind_drawable: Wrong framebuffer.");
467   }
468 #endif
469
470   if (main_window->window.current_drawable != d) {
471     main_window->window.current_drawable = d;
472
473     /* Doing this repeatedly is probably not OK performance-wise. Probably. */
474 #ifndef USE_IPHONE
475     [d->ogl_ctx makeCurrentContext];
476 #else
477     [EAGLContext setCurrentContext:d->ogl_ctx];
478     glBindFramebufferOES(GL_FRAMEBUFFER_OES, d->gl_framebuffer);
479     if (d->type == PIXMAP) {
480       glViewport (0, 0, d->frame.width, d->frame.height);
481       jwxyz_set_matrices (d->frame.width, d->frame.height);
482     }
483 #endif
484   }
485
486   jwxyz_assert_gl ();
487 }
488
489
490 Pixmap
491 XCreatePixmap (Display *dpy, Drawable d,
492                unsigned int width, unsigned int height, unsigned int depth)
493 {
494   Pixmap p = (Pixmap) calloc (1, sizeof(*p));
495   p->type = PIXMAP;
496   p->frame.width  = width;
497   p->frame.height = height;
498   p->pixmap.depth = depth;
499
500   Assert (depth == 1 || depth == 32, "XCreatePixmap: depth must be 32");
501
502   /* TODO: If Pixel buffers are not supported, do something about it. */
503   Window w = XRootWindow (dpy, 0);
504
505 # ifndef USE_IPHONE
506
507   p->ogl_ctx = [[NSOpenGLContext alloc]
508                 initWithFormat:w->window.pixfmt
509                 shareContext:w->ogl_ctx];
510   CFRetain (p->ogl_ctx);
511
512   [p->ogl_ctx makeCurrentContext]; // This is indeed necessary.
513
514   p->pixmap.gl_pbuffer = [[NSOpenGLPixelBuffer alloc]
515                           /* TODO: Only if there are rectangluar textures. */
516                           initWithTextureTarget:GL_TEXTURE_RECTANGLE_EXT
517                           /* TODO: Make sure GL_RGBA isn't better. */
518                           textureInternalFormat:GL_RGB
519                           textureMaxMipMapLevel:0
520                           pixelsWide:width
521                           pixelsHigh:height];
522   CFRetain (p->pixmap.gl_pbuffer);
523
524   [p->ogl_ctx setPixelBuffer:p->pixmap.gl_pbuffer
525                  cubeMapFace:0
526                  mipMapLevel:0
527         currentVirtualScreen:w->window.virtual_screen];
528
529 # else // USE_IPHONE
530
531   p->ogl_ctx = w->window.ogl_ctx_pixmap;
532
533   [EAGLContext setCurrentContext:p->ogl_ctx];
534   create_framebuffer (&p->gl_framebuffer, &p->gl_renderbuffer);
535
536   glRenderbufferStorageOES (GL_RENDERBUFFER_OES, GL_RGBA8_OES, width, height);
537   glFramebufferRenderbufferOES (GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES,
538                                 GL_RENDERBUFFER_OES, p->gl_renderbuffer);
539
540   check_framebuffer_status ();
541
542   glBindFramebufferOES(GL_FRAMEBUFFER_OES, p->gl_framebuffer);
543
544 # endif // USE_IPHONE
545
546   w->window.current_drawable = p;
547   glViewport (0, 0, width, height);
548   jwxyz_set_matrices (width, height);
549
550 # ifndef __OPTIMIZE__
551   glClearColor (frand(1), frand(1), frand(1), 0);
552   glClear (GL_COLOR_BUFFER_BIT);
553 # endif
554
555   return p;
556 }
557
558 int
559 XFreePixmap (Display *d, Pixmap p)
560 {
561   Assert (p && p->type == PIXMAP, "not a pixmap");
562
563   Window w = RootWindow (d, 0);
564
565 # ifndef USE_IPHONE
566   CFRelease (p->ogl_ctx);
567   [p->ogl_ctx release];
568
569   CFRelease (p->pixmap.gl_pbuffer);
570   [p->pixmap.gl_pbuffer release];
571 # else  // USE_IPHONE
572   glDeleteRenderbuffersOES (1, &p->gl_renderbuffer);
573   glDeleteFramebuffersOES (1, &p->gl_framebuffer);
574 # endif // USE_IPHONE
575
576   if (w->window.current_drawable == p) {
577     w->window.current_drawable = NULL;
578   }
579
580   free (p);
581   return 0;
582 }
583
584 #endif // JWXYZ_GL