0cdf14d34065700d992be9787586471a9ab4d5e9
[xscreensaver] / hacks / glx / glslideshow.c
1 /* glslideshow, Copyright (c) 2003-2008 Jamie Zawinski <jwz@jwz.org>
2  * Loads a sequence of images and smoothly pans around them; crossfades
3  * when loading new images.
4  *
5  * Originally written by Mike Oliphant <oliphant@gtk.org> (c) 2002, 2003.
6  * Rewritten by jwz, 21-Jun-2003.
7  * Rewritten by jwz again, 6-Feb-2005.
8  *
9  * Permission to use, copy, modify, distribute, and sell this software and its
10  * documentation for any purpose is hereby granted without fee, provided that
11  * the above copyright notice appear in all copies and that both that
12  * copyright notice and this permission notice appear in supporting
13  * documentation.  No representations are made about the suitability of this
14  * software for any purpose.  It is provided "as is" without express or
15  * implied warranty.
16  *
17  *****************************************************************************
18  *
19  * TODO:
20  *
21  * - When a new image is loaded, there is a glitch: animation pauses during
22  *   the period when we're loading the image-to-fade-in.  On fast (2GHz)
23  *   machines, this stutter is short but noticable (usually around 1/10th
24  *   second.)  On slower machines, it can be much more pronounced.
25  *   This turns out to be hard to fix...
26  *
27  *   Image loading happens in three stages:
28  *
29  *    1: Fork a process and run xscreensaver-getimage in the background.
30  *       This writes image data to a server-side X pixmap.
31  *
32  *    2: When that completes, a callback informs us that the pixmap is ready.
33  *       We must then download the pixmap data from the server with XGetImage
34  *       (or XShmGetImage.)
35  *
36  *    3: Once we have the bits, we must convert them from server-native bitmap
37  *       layout to 32 bit RGBA in client-endianness, to make them usable as
38  *       OpenGL textures.
39  *
40  *    4: We must actually construct a texture.
41  *
42  *   So, the speed of step 1 doesn't really matter, since that happens in
43  *   the background.  But steps 2, 3, and 4 happen in *this* process, and
44  *   cause the visible glitch.
45  *
46  *   Step 2 can't be moved to another process without opening a second
47  *   connection to the X server, which is pretty heavy-weight.  (That would
48  *   be possible, though; the other process could open an X connection,
49  *   retrieve the pixmap, and feed it back to us through a pipe or
50  *   something.)
51  *
52  *   Step 3 might be able to be optimized by coding tuned versions of
53  *   grab-ximage.c:copy_ximage() for the most common depths and bit orders.
54  *   (Or by moving it into the other process along with step 2.)
55  *
56  *   Step 4 is the hard one, though.  It might be possible to speed up this
57  *   step if there is some way to allow two GL processes share texture
58  *   data.  Unless, of course, all the time being consumed by step 4 is
59  *   because the graphics pipeline is flooded, in which case, that other
60  *   process would starve the screen anyway.
61  *
62  *   Is it possible to use a single GLX context in a multithreaded way?
63  *   Or use a second GLX context, but allow the two contexts to share data?
64  *   I can't find any documentation about this.
65  *
66  *   How does Apple do this with their MacOSX slideshow screen saver?
67  *   Perhaps it's easier for them because their OpenGL libraries have
68  *   thread support at a lower level?
69  */
70
71 #define DEFAULTS  "*delay:           20000                \n" \
72                   "*wireframe:       False                \n" \
73                   "*showFPS:         False                \n" \
74                   "*fpsSolid:        True                 \n" \
75                   "*useSHM:          True                 \n" \
76                   "*titleFont:       -*-helvetica-medium-r-normal-*-180-*\n" \
77                   "*desktopGrabber:  xscreensaver-getimage -no-desktop %s\n" \
78                   "*grabDesktopImages:   False \n" \
79                   "*chooseRandomImages:  True  \n"
80
81 # define refresh_slideshow 0
82 # define release_slideshow 0
83 # include "xlockmore.h"
84
85 #undef countof
86 #define countof(x) (sizeof((x))/sizeof((*x)))
87
88 #ifdef USE_GL
89
90
91 # define DEF_FADE_DURATION  "2"
92 # define DEF_PAN_DURATION   "6"
93 # define DEF_IMAGE_DURATION "30"
94 # define DEF_ZOOM           "75"
95 # define DEF_FPS_CUTOFF     "5"
96 # define DEF_TITLES         "False"
97 # define DEF_LETTERBOX      "True"
98 # define DEF_DEBUG          "False"
99 # define DEF_MIPMAP         "True"
100
101 #include "grab-ximage.h"
102 #include "glxfonts.h"
103
104 typedef struct {
105   double x, y, w, h;
106 } rect;
107
108 typedef struct {
109   ModeInfo *mi;
110   int id;                          /* unique number for debugging */
111   char *title;                     /* the filename of this image */
112   int w, h;                        /* size in pixels of the image */
113   int tw, th;                      /* size in pixels of the texture */
114   XRectangle geom;                 /* where in the image the bits are */
115   Bool loaded_p;                   /* whether the image has finished loading */
116   Bool used_p;                     /* whether the image has yet appeared
117                                       on screen */
118   GLuint texid;                    /* which texture contains the image */
119   int refcount;                    /* how many sprites refer to this image */
120 } image;
121
122
123 typedef enum { NEW, IN, FULL, OUT, DEAD } sprite_state;
124
125 typedef struct {
126   int id;                          /* unique number for debugging */
127   image *img;                      /* which image this animation displays */
128   GLfloat opacity;                 /* how to render it */
129   double start_time;               /* when this animation began */
130   rect from, to, current;          /* the journey this image is taking */
131   sprite_state state;              /* the state we're in right now */
132   sprite_state prev_state;         /* the state we were in previously */
133   double state_time;               /* time of last state change */
134   int frame_count;                 /* frames since last state change */
135 } sprite;
136
137
138 typedef struct {
139   GLXContext *glx_context;
140   int nimages;                  /* how many images are loaded or loading now */
141   image *images[10];            /* pointers to the images */
142
143   int nsprites;                 /* how many sprites are animating right now */
144   sprite *sprites[10];          /* pointers to the live sprites */
145
146   double now;                   /* current time in seconds */
147   double dawn_of_time;          /* when the program launched */
148   double image_load_time;       /* time when we last loaded a new image */
149   double prev_frame_time;       /* time when we last drew a frame */
150
151   Bool awaiting_first_image_p;  /* Early in startup: nothing to display yet */
152   Bool redisplay_needed_p;      /* Sometimes we can get away with not
153                                    re-painting.  Tick this if a redisplay
154                                    is required. */
155   Bool change_now_p;            /* Set when the user clicks to ask for a new
156                                    image right now. */
157
158   GLfloat fps;                  /* approximate frame rate we're achieving */
159   GLfloat theoretical_fps;      /* maximum frame rate that might be possible */
160   Bool checked_fps_p;           /* Whether we have checked for a low
161                                    frame rate. */
162
163   XFontStruct *xfont;           /* for printing image file names */
164   GLuint font_dlist;
165
166   int sprite_id, image_id;      /* debugging id counters */
167
168   double time_elapsed;
169   int frames_elapsed;
170
171 } slideshow_state;
172
173 static slideshow_state *sss = NULL;
174
175
176 /* Command-line arguments
177  */
178 static int fade_seconds;    /* Duration in seconds of fade transitions.
179                                If 0, jump-cut instead of fading. */
180 static int pan_seconds;     /* Duration of each pan through an image. */
181 static int image_seconds;   /* How many seconds until loading a new image. */
182 static int zoom;            /* How far in to zoom when panning, in percent of
183                                image size: that is, 75 means "when zoomed all
184                                the way in, 75% of the image will be visible."
185                              */
186 static int fps_cutoff;      /* If the frame-rate falls below this, turn off
187                                zooming.*/
188 static Bool letterbox_p;    /* When a loaded image is not the same aspect
189                                ratio as the window, whether to display black
190                                bars.
191                              */
192 static Bool mipmap_p;       /* Use mipmaps instead of single textures. */
193 static Bool do_titles;      /* Display image titles. */
194 static Bool debug_p;        /* Be loud and do weird things. */
195
196
197 static XrmOptionDescRec opts[] = {
198   {"-fade",         ".fadeDuration",  XrmoptionSepArg, 0      },
199   {"-pan",          ".panDuration",   XrmoptionSepArg, 0      },
200   {"-duration",     ".imageDuration", XrmoptionSepArg, 0      },
201   {"-zoom",         ".zoom",          XrmoptionSepArg, 0      },
202   {"-cutoff",       ".FPScutoff",     XrmoptionSepArg, 0      },
203   {"-titles",       ".titles",        XrmoptionNoArg, "True"  },
204   {"-letterbox",    ".letterbox",     XrmoptionNoArg, "True"  },
205   {"-no-letterbox", ".letterbox",     XrmoptionNoArg, "False" },
206   {"-clip",         ".letterbox",     XrmoptionNoArg, "False" },
207   {"-mipmaps",      ".mipmap",        XrmoptionNoArg, "True"  },
208   {"-no-mipmaps",   ".mipmap",        XrmoptionNoArg, "False" },
209   {"-debug",        ".debug",         XrmoptionNoArg, "True"  },
210 };
211
212 static argtype vars[] = {
213   { &fade_seconds,  "fadeDuration", "FadeDuration", DEF_FADE_DURATION,  t_Int},
214   { &pan_seconds,   "panDuration",  "PanDuration",  DEF_PAN_DURATION,   t_Int},
215   { &image_seconds, "imageDuration","ImageDuration",DEF_IMAGE_DURATION, t_Int},
216   { &zoom,          "zoom",         "Zoom",         DEF_ZOOM,           t_Int},
217   { &mipmap_p,      "mipmap",       "Mipmap",       DEF_MIPMAP,        t_Bool},
218   { &letterbox_p,   "letterbox",    "Letterbox",    DEF_LETTERBOX,     t_Bool},
219   { &fps_cutoff,    "FPScutoff",    "FPSCutoff",    DEF_FPS_CUTOFF,     t_Int},
220   { &debug_p,       "debug",        "Debug",        DEF_DEBUG,         t_Bool},
221   { &do_titles,     "titles",       "Titles",       DEF_TITLES,        t_Bool},
222 };
223
224 ENTRYPOINT ModeSpecOpt slideshow_opts = {countof(opts), opts, countof(vars), vars, NULL};
225
226
227 static const char *
228 blurb (void)
229 {
230 # ifdef HAVE_COCOA
231   return "GLSlideshow";
232 # else
233   static char buf[255];
234   time_t now = time ((time_t *) 0);
235   char *ct = (char *) ctime (&now);
236   int n = strlen(progname);
237   if (n > 100) n = 99;
238   strncpy(buf, progname, n);
239   buf[n++] = ':';
240   buf[n++] = ' ';
241   strncpy(buf+n, ct+11, 8);
242   strcpy(buf+n+9, ": ");
243   return buf;
244 # endif
245 }
246
247
248 /* Returns the current time in seconds as a double.
249  */
250 static double
251 double_time (void)
252 {
253   struct timeval now;
254 # ifdef GETTIMEOFDAY_TWO_ARGS
255   struct timezone tzp;
256   gettimeofday(&now, &tzp);
257 # else
258   gettimeofday(&now);
259 # endif
260
261   return (now.tv_sec + ((double) now.tv_usec * 0.000001));
262 }
263
264
265 static void image_loaded_cb (const char *filename, XRectangle *geom,
266                              int image_width, int image_height,
267                              int texture_width, int texture_height,
268                              void *closure);
269
270
271 /* Allocate an image structure and start a file loading in the background.
272  */
273 static image *
274 alloc_image (ModeInfo *mi)
275 {
276   slideshow_state *ss = &sss[MI_SCREEN(mi)];
277   int wire = MI_IS_WIREFRAME(mi);
278   image *img = (image *) calloc (1, sizeof (*img));
279
280   img->id = ++ss->image_id;
281   img->loaded_p = False;
282   img->used_p = False;
283   img->mi = mi;
284
285   glGenTextures (1, &img->texid);
286   if (img->texid <= 0) abort();
287
288   ss->image_load_time = ss->now;
289
290   if (wire)
291     image_loaded_cb (0, 0, 0, 0, 0, 0, img);
292   else
293     load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context,
294                         0, 0, mipmap_p, img->texid, image_loaded_cb, img);
295
296   ss->images[ss->nimages++] = img;
297   if (ss->nimages >= countof(ss->images)) abort();
298
299   return img;
300 }
301
302
303 /* Callback that tells us that the texture has been loaded.
304  */
305 static void
306 image_loaded_cb (const char *filename, XRectangle *geom,
307                  int image_width, int image_height,
308                  int texture_width, int texture_height,
309                  void *closure)
310 {
311   image *img = (image *) closure;
312   ModeInfo *mi = img->mi;
313   /* slideshow_state *ss = &sss[MI_SCREEN(mi)]; */
314
315   int wire = MI_IS_WIREFRAME(mi);
316
317   if (wire)
318     {
319       img->w = MI_WIDTH (mi) * (0.5 + frand (1.0));
320       img->h = MI_HEIGHT (mi);
321       img->geom.width  = img->w;
322       img->geom.height = img->h;
323       goto DONE;
324     }
325
326   if (image_width == 0 || image_height == 0)
327     exit (1);
328
329   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
330   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
331                    mipmap_p ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
332
333   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
334   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
335
336   img->w  = image_width;
337   img->h  = image_height;
338   img->tw = texture_width;
339   img->th = texture_height;
340   img->geom = *geom;
341   img->title = (filename ? strdup (filename) : 0);
342
343   /* If the image's width doesn't come back as the width of the screen,
344      then the image must have been scaled down (due to insufficient
345      texture memory.)  Scale up the coordinates to stretch the image
346      to fill the window.
347    */
348   if (img->w != MI_WIDTH(mi))
349     {
350       double scale = (double) MI_WIDTH(mi) / img->w;
351       img->w  *= scale;
352       img->h  *= scale;
353       img->tw *= scale;
354       img->th *= scale;
355       img->geom.x      *= scale;
356       img->geom.y      *= scale;
357       img->geom.width  *= scale;
358       img->geom.height *= scale;
359     }
360
361   if (img->title)   /* strip filename to part after last /. */
362     {
363       char *s = strrchr (img->title, '/');
364       if (s) strcpy (img->title, s+1);
365     }
366
367   if (debug_p)
368     fprintf (stderr, "%s: loaded   img %2d: \"%s\"\n",
369              blurb(), img->id, (img->title ? img->title : "(null)"));
370  DONE:
371
372   img->loaded_p = True;
373 }
374
375
376
377 /* Free the image and texture, after nobody is referencing it.
378  */
379 static void
380 destroy_image (ModeInfo *mi, image *img)
381 {
382   slideshow_state *ss = &sss[MI_SCREEN(mi)];
383   Bool freed_p = False;
384   int i;
385
386   if (!img) abort();
387   if (!img->loaded_p) abort();
388   if (!img->used_p) abort();
389   if (img->texid <= 0) abort();
390   if (img->refcount != 0) abort();
391
392   for (i = 0; i < ss->nimages; i++)             /* unlink it from the list */
393     if (ss->images[i] == img)
394       {
395         int j;
396         for (j = i; j < ss->nimages-1; j++)     /* pull remainder forward */
397           ss->images[j] = ss->images[j+1];
398         ss->images[j] = 0;
399         ss->nimages--;
400         freed_p = True;
401         break;
402       }
403
404   if (!freed_p) abort();
405
406   if (debug_p)
407     fprintf (stderr, "%s: unloaded img %2d: \"%s\"\n",
408              blurb(), img->id, (img->title ? img->title : "(null)"));
409
410   if (img->title) free (img->title);
411   glDeleteTextures (1, &img->texid);
412   free (img);
413 }
414
415
416 /* Return an image to use for a sprite.
417    If it's time for a new one, get a new one.
418    Otherwise, use an old one.
419    Might return 0 if the machine is really slow.
420  */
421 static image *
422 get_image (ModeInfo *mi)
423 {
424   slideshow_state *ss = &sss[MI_SCREEN(mi)];
425   image *img = 0;
426   double now = ss->now;
427   Bool want_new_p = (ss->change_now_p ||
428                      ss->image_load_time + image_seconds <= now);
429   image *new_img = 0;
430   image *old_img = 0;
431   image *loading_img = 0;
432   int i;
433
434   for (i = 0; i < ss->nimages; i++)
435     {
436       image *img2 = ss->images[i];
437       if (!img2) abort();
438       if (!img2->loaded_p)
439         loading_img = img2;
440       else if (!img2->used_p)
441         new_img = img2;
442       else
443         old_img = img2;
444     }
445
446   if (want_new_p && new_img)
447     img = new_img, new_img = 0, ss->change_now_p = False;
448   else if (old_img)
449     img = old_img, old_img = 0;
450   else if (new_img)
451     img = new_img, new_img = 0, ss->change_now_p = False;
452
453   /* Make sure that there is always one unused image in the pipe.
454    */
455   if (!new_img && !loading_img)
456     alloc_image (mi);
457
458   return img;
459 }
460
461
462 /* Pick random starting and ending positions for the given sprite.
463  */
464 static void
465 randomize_sprite (ModeInfo *mi, sprite *sp)
466 {
467   int vp_w = MI_WIDTH(mi);
468   int vp_h = MI_HEIGHT(mi);
469   int img_w = sp->img->geom.width;
470   int img_h = sp->img->geom.height;
471   int min_w, min_h, max_w, max_h;
472   double ratio = (double) img_h / img_w;
473
474   if (letterbox_p)
475     {
476       min_w = img_w;
477       min_h = img_h;
478     }
479   else
480     {
481       if (img_w < vp_w)
482         {
483           min_w = vp_w;
484           min_h = img_h * (float) vp_w / img_w;
485         }
486       else
487         {
488           min_w = img_w * (float) vp_h / img_h;
489           min_h = vp_h;
490         }
491     }
492
493   max_w = min_w * 100 / zoom;
494   max_h = min_h * 100 / zoom;
495
496   sp->from.w = min_w + frand ((max_w - min_w) * 0.4);
497   sp->to.w   = max_w - frand ((max_w - min_w) * 0.4);
498   sp->from.h = sp->from.w * ratio;
499   sp->to.h   = sp->to.w   * ratio;
500
501   if (zoom == 100)      /* only one box, and it is centered */
502     {
503       sp->from.x = (sp->from.w > vp_w
504                     ? -(sp->from.w - vp_w) / 2
505                     :  (vp_w - sp->from.w) / 2);
506       sp->from.y = (sp->from.h > vp_h
507                     ? -(sp->from.h - vp_h) / 2
508                     :  (vp_h - sp->from.h) / 2);
509       sp->to = sp->from;
510     }
511   else                  /* position both boxes randomly */
512     {
513       sp->from.x = (sp->from.w > vp_w
514                     ? -frand (sp->from.w - vp_w)
515                     :  frand (vp_w - sp->from.w));
516       sp->from.y = (sp->from.h > vp_h
517                     ? -frand (sp->from.h - vp_h)
518                     :  frand (vp_h - sp->from.h));
519       sp->to.x   = (sp->to.w > vp_w
520                     ? -frand (sp->to.w - vp_w)
521                     :  frand (vp_w - sp->to.w));
522       sp->to.y   = (sp->to.h > vp_h
523                     ? -frand (sp->to.h - vp_h)
524                     :  frand (vp_h - sp->to.h));
525     }
526
527   if (random() & 1)
528     {
529       rect swap = sp->to;
530       sp->to = sp->from;
531       sp->from = swap;
532     }
533
534   /* Make sure the aspect ratios are within 0.001 of each other.
535    */
536   {
537     int r1 = 0.5 + (sp->from.w * 1000 / sp->from.h);
538     int r2 = 0.5 + (sp->to.w   * 1000 / sp->to.h);
539     if (r1 < r2-1 || r1 > r2+1)
540       {
541         fprintf (stderr,
542                  "%s: botched aspect: %f x %f (%d) vs  %f x %f (%d): %s\n",
543                  progname, 
544                  sp->from.w, sp->from.h, r1,
545                  sp->to.w, sp->to.h, r2,
546                  (sp->img->title ? sp->img->title : "[null]"));
547         abort();
548       }
549   }
550
551   sp->from.x /= vp_w;
552   sp->from.y /= vp_h;
553   sp->from.w /= vp_w;
554   sp->from.h /= vp_h;
555   sp->to.x   /= vp_w;
556   sp->to.y   /= vp_h;
557   sp->to.w   /= vp_w;
558   sp->to.h   /= vp_h;
559 }
560
561
562 /* Allocate a new sprite and start its animation going.
563  */
564 static sprite *
565 new_sprite (ModeInfo *mi)
566 {
567   slideshow_state *ss = &sss[MI_SCREEN(mi)];
568   image *img = get_image (mi);
569   sprite *sp;
570
571   if (!img)
572     {
573       /* Oops, no images yet!  The machine is probably hurting bad.
574          Let's give it some time before thrashing again. */
575       usleep (250000);
576       return 0;
577     }
578
579   sp = (sprite *) calloc (1, sizeof (*sp));
580   sp->id = ++ss->sprite_id;
581   sp->start_time = ss->now;
582   sp->state_time = sp->start_time;
583   sp->state = sp->prev_state = NEW;
584   sp->img = img;
585
586   sp->img->refcount++;
587   sp->img->used_p = True;
588
589   ss->sprites[ss->nsprites++] = sp;
590   if (ss->nsprites >= countof(ss->sprites)) abort();
591
592   randomize_sprite (mi, sp);
593
594   return sp;
595 }
596
597
598 /* Free the given sprite, and decrement the reference count on its image.
599  */
600 static void
601 destroy_sprite (ModeInfo *mi, sprite *sp)
602 {
603   slideshow_state *ss = &sss[MI_SCREEN(mi)];
604   Bool freed_p = False;
605   image *img;
606   int i;
607
608   if (!sp) abort();
609   if (sp->state != DEAD) abort();
610   img = sp->img;
611   if (!img) abort();
612   if (!img->loaded_p) abort();
613   if (!img->used_p) abort();
614   if (img->refcount <= 0) abort();
615
616   for (i = 0; i < ss->nsprites; i++)            /* unlink it from the list */
617     if (ss->sprites[i] == sp)
618       {
619         int j;
620         for (j = i; j < ss->nsprites-1; j++)    /* pull remainder forward */
621           ss->sprites[j] = ss->sprites[j+1];
622         ss->sprites[j] = 0;
623         ss->nsprites--;
624         freed_p = True;
625         break;
626       }
627
628   if (!freed_p) abort();
629   free (sp);
630   sp = 0;
631
632   img->refcount--;
633   if (img->refcount < 0) abort();
634   if (img->refcount == 0)
635     destroy_image (mi, img);
636 }
637
638
639 /* Updates the sprite for the current frame of the animation based on
640    its creation time compared to the current wall clock.
641  */
642 static void
643 tick_sprite (ModeInfo *mi, sprite *sp)
644 {
645   slideshow_state *ss = &sss[MI_SCREEN(mi)];
646   image *img = sp->img;
647   double now = ss->now;
648   double secs;
649   double ratio;
650   rect prev_rect = sp->current;
651   GLfloat prev_opacity = sp->opacity;
652
653   if (! sp->img) abort();
654   if (! img->loaded_p) abort();
655
656   secs = now - sp->start_time;
657   ratio = secs / (pan_seconds + fade_seconds);
658   if (ratio > 1) ratio = 1;
659
660   sp->current.x = sp->from.x + ratio * (sp->to.x - sp->from.x);
661   sp->current.y = sp->from.y + ratio * (sp->to.y - sp->from.y);
662   sp->current.w = sp->from.w + ratio * (sp->to.w - sp->from.w);
663   sp->current.h = sp->from.h + ratio * (sp->to.h - sp->from.h);
664
665   sp->prev_state = sp->state;
666
667   if (secs < fade_seconds)
668     {
669       sp->state = IN;
670       sp->opacity = secs / (GLfloat) fade_seconds;
671     }
672   else if (secs < pan_seconds)
673     {
674       sp->state = FULL;
675       sp->opacity = 1;
676     }
677   else if (secs < pan_seconds + fade_seconds)
678     {
679       sp->state = OUT;
680       sp->opacity = 1 - ((secs - pan_seconds) / (GLfloat) fade_seconds);
681     }
682   else
683     {
684       sp->state = DEAD;
685       sp->opacity = 0;
686     }
687
688   if (sp->state != sp->prev_state &&
689       (sp->prev_state == IN ||
690        sp->prev_state == FULL))
691     {
692       double secs = now - sp->state_time;
693
694       if (debug_p)
695         fprintf (stderr,
696                  "%s: %s %3d frames %2.0f sec %5.1f fps (%.1f fps?)\n",
697                  blurb(),
698                  (sp->prev_state == IN ? "fade" : "pan "),
699                  sp->frame_count,
700                  secs,
701                  sp->frame_count / secs,
702                  ss->theoretical_fps);
703
704       sp->state_time = now;
705       sp->frame_count = 0;
706     }
707
708   sp->frame_count++;
709
710   if (sp->state != DEAD &&
711       (prev_rect.x != sp->current.x ||
712        prev_rect.y != sp->current.y ||
713        prev_rect.w != sp->current.w ||
714        prev_rect.h != sp->current.h ||
715        prev_opacity != sp->opacity))
716     ss->redisplay_needed_p = True;
717 }
718
719
720 /* Draw the given sprite at the phase of its animation dictated by
721    its creation time compared to the current wall clock.
722  */
723 static void
724 draw_sprite (ModeInfo *mi, sprite *sp)
725 {
726   slideshow_state *ss = &sss[MI_SCREEN(mi)];
727   int wire = MI_IS_WIREFRAME(mi);
728   image *img = sp->img;
729
730   if (! sp->img) abort();
731   if (! img->loaded_p) abort();
732
733   glPushMatrix();
734   {
735     glTranslatef (sp->current.x, sp->current.y, 0);
736     glScalef (sp->current.w, sp->current.h, 1);
737
738     if (wire)                   /* Draw a grid inside the box */
739       {
740         GLfloat dy = 0.1;
741         GLfloat dx = dy * img->w / img->h;
742         GLfloat x, y;
743
744         if (sp->id & 1)
745           glColor4f (sp->opacity, 0, 0, 1);
746         else
747           glColor4f (0, 0, sp->opacity, 1);
748
749         glBegin(GL_LINES);
750         glVertex3f (0, 0, 0); glVertex3f (1, 1, 0);
751         glVertex3f (1, 0, 0); glVertex3f (0, 1, 0);
752
753         for (y = 0; y < 1+dy; y += dy)
754           {
755             GLfloat yy = (y > 1 ? 1 : y);
756             for (x = 0.5; x < 1+dx; x += dx)
757               {
758                 GLfloat xx = (x > 1 ? 1 : x);
759                 glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
760                 glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
761               }
762             for (x = 0.5; x > -dx; x -= dx)
763               {
764                 GLfloat xx = (x < 0 ? 0 : x);
765                 glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
766                 glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
767               }
768           }
769         glEnd();
770       }
771     else                        /* Draw the texture quad */
772       {
773         GLfloat texw  = img->geom.width  / (GLfloat) img->tw;
774         GLfloat texh  = img->geom.height / (GLfloat) img->th;
775         GLfloat texx1 = img->geom.x / (GLfloat) img->tw;
776         GLfloat texy1 = img->geom.y / (GLfloat) img->th;
777         GLfloat texx2 = texx1 + texw;
778         GLfloat texy2 = texy1 + texh;
779
780         glBindTexture (GL_TEXTURE_2D, img->texid);
781         glColor4f (1, 1, 1, sp->opacity);
782         glNormal3f (0, 0, 1);
783         glBegin (GL_QUADS);
784         glTexCoord2f (texx1, texy2); glVertex3f (0, 0, 0);
785         glTexCoord2f (texx2, texy2); glVertex3f (1, 0, 0);
786         glTexCoord2f (texx2, texy1); glVertex3f (1, 1, 0);
787         glTexCoord2f (texx1, texy1); glVertex3f (0, 1, 0);
788         glEnd();
789
790         if (debug_p)            /* Draw a border around the image */
791           {
792             if (!wire) glDisable (GL_TEXTURE_2D);
793
794             if (sp->id & 1)
795               glColor4f (sp->opacity, 0, 0, 1);
796             else
797               glColor4f (0, 0, sp->opacity, 1);
798
799             glBegin (GL_LINE_LOOP);
800             glVertex3f (0, 0, 0);
801             glVertex3f (0, 1, 0);
802             glVertex3f (1, 1, 0);
803             glVertex3f (1, 0, 0);
804             glEnd();
805
806             if (!wire) glEnable (GL_TEXTURE_2D);
807           }
808       }
809
810
811     if (do_titles &&
812         img->title && *img->title)
813       {
814         int x = 10;
815         int y = mi->xgwa.height - 10;
816         glColor4f (0, 0, 0, sp->opacity);   /* cheap-assed dropshadow */
817         print_gl_string (mi->dpy, ss->xfont, ss->font_dlist,
818                          mi->xgwa.width, mi->xgwa.height, x, y,
819                          img->title, False);
820         x++; y++;
821         glColor4f (1, 1, 1, sp->opacity);
822         print_gl_string (mi->dpy, ss->xfont, ss->font_dlist,
823                          mi->xgwa.width, mi->xgwa.height, x, y,
824                          img->title, False);
825       }
826   }
827   glPopMatrix();
828
829   if (debug_p)
830     {
831       if (!wire) glDisable (GL_TEXTURE_2D);
832
833       if (sp->id & 1)
834         glColor4f (1, 0, 0, 1);
835       else
836         glColor4f (0, 0, 1, 1);
837
838       /* Draw the "from" and "to" boxes
839        */
840       glBegin (GL_LINE_LOOP);
841       glVertex3f (sp->from.x,              sp->from.y,              0);
842       glVertex3f (sp->from.x + sp->from.w, sp->from.y,              0);
843       glVertex3f (sp->from.x + sp->from.w, sp->from.y + sp->from.h, 0);
844       glVertex3f (sp->from.x,              sp->from.y + sp->from.h, 0);
845       glEnd();
846
847       glBegin (GL_LINE_LOOP);
848       glVertex3f (sp->to.x,                sp->to.y,                0);
849       glVertex3f (sp->to.x + sp->to.w,     sp->to.y,                0);
850       glVertex3f (sp->to.x + sp->to.w,     sp->to.y + sp->to.h,     0);
851       glVertex3f (sp->to.x,                sp->to.y + sp->to.h,     0);
852       glEnd();
853
854       if (!wire) glEnable (GL_TEXTURE_2D);
855     }
856 }
857
858
859 static void
860 tick_sprites (ModeInfo *mi)
861 {
862   slideshow_state *ss = &sss[MI_SCREEN(mi)];
863   int i;
864   for (i = 0; i < ss->nsprites; i++)
865       tick_sprite (mi, ss->sprites[i]);
866 }
867
868
869 static void
870 draw_sprites (ModeInfo *mi)
871 {
872   slideshow_state *ss = &sss[MI_SCREEN(mi)];
873   int i;
874
875   glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
876
877   glPushMatrix();
878   for (i = 0; i < ss->nsprites; i++)
879     draw_sprite (mi, ss->sprites[i]);
880   glPopMatrix();
881
882   if (debug_p)                          /* draw a white box (the "screen") */
883     {
884       int wire = MI_IS_WIREFRAME(mi);
885
886       if (!wire) glDisable (GL_TEXTURE_2D);
887
888       glColor4f (1, 1, 1, 1);
889       glBegin (GL_LINE_LOOP);
890       glVertex3f (0, 0, 0);
891       glVertex3f (0, 1, 0);
892       glVertex3f (1, 1, 0);
893       glVertex3f (1, 0, 0);
894       glEnd();
895
896       if (!wire) glEnable (GL_TEXTURE_2D);
897     }
898 }
899
900
901 ENTRYPOINT void
902 reshape_slideshow (ModeInfo *mi, int width, int height)
903 {
904   slideshow_state *ss = &sss[MI_SCREEN(mi)];
905   GLfloat s;
906   glViewport (0, 0, width, height);
907   glMatrixMode (GL_PROJECTION);
908   glLoadIdentity();
909   glMatrixMode (GL_MODELVIEW);
910   glLoadIdentity();
911
912   s = 2;
913
914   if (debug_p)
915     {
916       s *= (zoom / 100.0) * 0.75;
917       if (s < 0.1) s = 0.1;
918     }
919
920   glScalef (s, s, s);
921   glTranslatef (-0.5, -0.5, 0);
922
923   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
924
925   ss->redisplay_needed_p = True;
926 }
927
928
929 ENTRYPOINT Bool
930 slideshow_handle_event (ModeInfo *mi, XEvent *event)
931 {
932   slideshow_state *ss = &sss[MI_SCREEN(mi)];
933
934   if (event->xany.type == ButtonPress &&
935       event->xbutton.button == Button1)
936     {
937       ss->change_now_p = True;
938       return True;
939     }
940   else if (event->xany.type == KeyPress)
941     {
942       KeySym keysym;
943       char c = 0;
944       XLookupString (&event->xkey, &c, 1, &keysym, 0);
945       if (c == ' ' || c == '\r' || c == '\n' || c == '\t')
946         {
947           ss->change_now_p = True;
948           return True;
949         }
950     }
951   else if (event->xany.type == Expose ||
952            event->xany.type == GraphicsExpose ||
953            event->xany.type == VisibilityNotify)
954     {
955       ss->redisplay_needed_p = True;
956       if (debug_p)
957         fprintf (stderr, "%s: exposure\n", blurb());
958       return False;
959     }
960
961   return False;
962 }
963
964
965 /* Do some sanity checking on various user-supplied values, and make
966    sure they are all internally consistent.
967  */
968 static void
969 sanity_check (ModeInfo *mi)
970 {
971   if (zoom < 1) zoom = 1;           /* zoom is a positive percentage */
972   else if (zoom > 100) zoom = 100;
973
974   if (zoom == 100)                  /* with no zooming, there is no panning */
975     pan_seconds = 0;
976
977   if (pan_seconds < fade_seconds)   /* pan is inclusive of fade */
978     pan_seconds = fade_seconds;
979
980   if (pan_seconds == 0)             /* no zero-length cycles, please... */
981     pan_seconds = 1;
982
983   if (image_seconds < pan_seconds)  /* we only change images at fade-time */
984     image_seconds = pan_seconds;
985
986   /* If we're not panning/zooming within the image, then there's no point
987      in crossfading the image with itself -- only do crossfades when changing
988      to a new image. */
989   if (zoom == 100 && pan_seconds < image_seconds)
990     pan_seconds = image_seconds;
991
992   /* No need to use mipmaps if we're not changing the image size much */
993   if (zoom >= 80) mipmap_p = False;
994
995   if      (fps_cutoff < 0)  fps_cutoff = 0;
996   else if (fps_cutoff > 30) fps_cutoff = 30;
997 }
998
999
1000 static void
1001 check_fps (ModeInfo *mi)
1002 {
1003 #ifndef HAVE_COCOA  /* always assume Cocoa is fast enough */
1004
1005   slideshow_state *ss = &sss[MI_SCREEN(mi)];
1006
1007   double start_time, end_time, wall_elapsed, frame_duration, fps;
1008   int i;
1009
1010   start_time = ss->now;
1011   end_time = double_time();
1012   frame_duration = end_time - start_time;   /* time spent drawing this frame */
1013   ss->time_elapsed += frame_duration;       /* time spent drawing all frames */
1014   ss->frames_elapsed++;
1015
1016   wall_elapsed = end_time - ss->dawn_of_time;
1017   fps = ss->frames_elapsed / ss->time_elapsed;
1018   ss->theoretical_fps = fps;
1019
1020   if (ss->checked_fps_p) return;
1021
1022   if (wall_elapsed <= 8)    /* too early to be sure */
1023     return;
1024
1025   ss->checked_fps_p = True;
1026
1027   if (fps >= fps_cutoff)
1028     {
1029       if (debug_p)
1030         fprintf (stderr,
1031                  "%s: %.1f fps is fast enough (with %d frames in %.1f secs)\n",
1032                  blurb(), fps, ss->frames_elapsed, wall_elapsed);
1033       return;
1034     }
1035
1036   fprintf (stderr,
1037            "%s: only %.1f fps!  Turning off pan/fade to compensate...\n",
1038            blurb(), fps);
1039   zoom = 100;
1040   fade_seconds = 0;
1041
1042   sanity_check (mi);
1043
1044   for (i = 0; i < ss->nsprites; i++)
1045     {
1046       sprite *sp = ss->sprites[i];
1047       randomize_sprite (mi, sp);
1048       sp->state = FULL;
1049     }
1050
1051   ss->redisplay_needed_p = True;
1052
1053   /* Need this in case zoom changed. */
1054   reshape_slideshow (mi, mi->xgwa.width, mi->xgwa.height);
1055 #endif /* HAVE_COCOA */
1056 }
1057
1058
1059 /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
1060  */
1061 static void
1062 hack_resources (void)
1063 {
1064 #if 0
1065   char *res = "desktopGrabber";
1066   char *val = get_string_resource (res, "DesktopGrabber");
1067   char buf1[255];
1068   char buf2[255];
1069   XrmValue value;
1070   sprintf (buf1, "%.100s.%.100s", progclass, res);
1071   sprintf (buf2, "%.200s -v", val);
1072   value.addr = buf2;
1073   value.size = strlen(buf2);
1074   XrmPutResource (&db, buf1, "String", &value);
1075 #endif
1076 }
1077
1078
1079 ENTRYPOINT void
1080 init_slideshow (ModeInfo *mi)
1081 {
1082   int screen = MI_SCREEN(mi);
1083   slideshow_state *ss;
1084   int wire = MI_IS_WIREFRAME(mi);
1085   
1086   if (sss == NULL) {
1087     if ((sss = (slideshow_state *)
1088          calloc (MI_NUM_SCREENS(mi), sizeof(slideshow_state))) == NULL)
1089       return;
1090   }
1091   ss = &sss[screen];
1092
1093   if ((ss->glx_context = init_GL(mi)) != NULL) {
1094     reshape_slideshow (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
1095   } else {
1096     MI_CLEARWINDOW(mi);
1097   }
1098
1099   if (debug_p)
1100     fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
1101              blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
1102
1103   sanity_check(mi);
1104
1105   if (debug_p)
1106     fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n\n",
1107              blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
1108
1109   glDisable (GL_LIGHTING);
1110   glDisable (GL_DEPTH_TEST);
1111   glDepthMask (GL_FALSE);
1112   glEnable (GL_CULL_FACE);
1113   glCullFace (GL_BACK);
1114
1115   if (! wire)
1116     {
1117       glEnable (GL_TEXTURE_2D);
1118       glShadeModel (GL_SMOOTH);
1119       glEnable (GL_BLEND);
1120       glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1121     }
1122
1123   if (debug_p) glLineWidth (3);
1124
1125   load_font (mi->dpy, "titleFont", &ss->xfont, &ss->font_dlist);
1126
1127   if (debug_p)
1128     hack_resources();
1129
1130   ss->now = double_time();
1131   ss->dawn_of_time = ss->now;
1132   ss->prev_frame_time = ss->now;
1133
1134   ss->awaiting_first_image_p = True;
1135   alloc_image (mi);
1136 }
1137
1138
1139 ENTRYPOINT void
1140 draw_slideshow (ModeInfo *mi)
1141 {
1142   slideshow_state *ss = &sss[MI_SCREEN(mi)];
1143   int i;
1144
1145   if (!ss->glx_context)
1146     return;
1147
1148   glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
1149
1150   if (ss->awaiting_first_image_p)
1151     {
1152       image *img = ss->images[0];
1153       if (!img) abort();
1154       if (!img->loaded_p)
1155         return;
1156
1157       ss->awaiting_first_image_p = False;
1158       ss->dawn_of_time = double_time();
1159
1160       /* start the very first sprite fading in */
1161       new_sprite (mi);
1162     }
1163
1164   ss->now = double_time();
1165
1166   /* Each sprite has three states: fading in, full, fading out.
1167      The in/out states overlap like this:
1168
1169      iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . . . . . . . . . . . 
1170      . . . . . . . . . iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . .
1171      . . . . . . . . . . . . . . . . . . iiiiiiFFFFFFFFFFFFooooo
1172
1173      So as soon as a sprite goes into the "out" state, we create
1174      a new sprite (in the "in" state.)
1175    */
1176
1177   if (ss->nsprites > 2) abort();
1178
1179   /* If a sprite is just entering the fade-out state,
1180      then add a new sprite in the fade-in state.
1181    */
1182   for (i = 0; i < ss->nsprites; i++)
1183     {
1184       sprite *sp = ss->sprites[i];
1185       if (sp->state != sp->prev_state &&
1186           sp->state == (fade_seconds == 0 ? DEAD : OUT))
1187         new_sprite (mi);
1188     }
1189
1190   tick_sprites (mi);
1191
1192   /* Now garbage collect the dead sprites.
1193    */
1194   for (i = 0; i < ss->nsprites; i++)
1195     {
1196       sprite *sp = ss->sprites[i];
1197       if (sp->state == DEAD)
1198         {
1199           destroy_sprite (mi, sp);
1200           i--;
1201         }
1202     }
1203
1204   /* We can only ever end up with no sprites at all if the machine is
1205      being really slow and we hopped states directly from FULL to DEAD
1206      without passing OUT... */
1207   if (ss->nsprites == 0)
1208     new_sprite (mi);
1209
1210   if (!ss->redisplay_needed_p)
1211     return;
1212
1213   if (debug_p && ss->now - ss->prev_frame_time > 1)
1214     fprintf (stderr, "%s: static screen for %.1f secs\n",
1215              blurb(), ss->now - ss->prev_frame_time);
1216
1217   draw_sprites (mi);
1218
1219   ss->fps = fps_compute (mi->fpst, 0);
1220   if (mi->fps_p) do_fps (mi);
1221
1222   glFinish();
1223   glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
1224   ss->prev_frame_time = ss->now;
1225   ss->redisplay_needed_p = False;
1226   check_fps (mi);
1227 }
1228
1229 XSCREENSAVER_MODULE_2 ("GLSlideshow", glslideshow, slideshow)
1230
1231 #endif /* USE_GL */