From http://www.jwz.org/xscreensaver/xscreensaver-5.31.tar.gz
[xscreensaver] / hacks / glx / glslideshow.c
1 /* glslideshow, Copyright (c) 2003-2014 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 "texfont.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   texture_font_data *font_data; /* for printing image file names */
164
165   int sprite_id, image_id;      /* debugging id counters */
166
167   double time_elapsed;
168   int frames_elapsed;
169
170 } slideshow_state;
171
172 static slideshow_state *sss = NULL;
173
174
175 /* Command-line arguments
176  */
177 static int fade_seconds;    /* Duration in seconds of fade transitions.
178                                If 0, jump-cut instead of fading. */
179 static int pan_seconds;     /* Duration of each pan through an image. */
180 static int image_seconds;   /* How many seconds until loading a new image. */
181 static int zoom;            /* How far in to zoom when panning, in percent of
182                                image size: that is, 75 means "when zoomed all
183                                the way in, 75% of the image will be visible."
184                              */
185 static int fps_cutoff;      /* If the frame-rate falls below this, turn off
186                                zooming.*/
187 static Bool letterbox_p;    /* When a loaded image is not the same aspect
188                                ratio as the window, whether to display black
189                                bars.
190                              */
191 static Bool mipmap_p;       /* Use mipmaps instead of single textures. */
192 static Bool do_titles;      /* Display image titles. */
193 static Bool debug_p;        /* Be loud and do weird things. */
194
195
196 static XrmOptionDescRec opts[] = {
197   {"-fade",         ".fadeDuration",  XrmoptionSepArg, 0      },
198   {"-pan",          ".panDuration",   XrmoptionSepArg, 0      },
199   {"-duration",     ".imageDuration", XrmoptionSepArg, 0      },
200   {"-zoom",         ".zoom",          XrmoptionSepArg, 0      },
201   {"-cutoff",       ".FPScutoff",     XrmoptionSepArg, 0      },
202   {"-titles",       ".titles",        XrmoptionNoArg, "True"  },
203   {"-letterbox",    ".letterbox",     XrmoptionNoArg, "True"  },
204   {"-no-letterbox", ".letterbox",     XrmoptionNoArg, "False" },
205   {"-clip",         ".letterbox",     XrmoptionNoArg, "False" },
206   {"-mipmaps",      ".mipmap",        XrmoptionNoArg, "True"  },
207   {"-no-mipmaps",   ".mipmap",        XrmoptionNoArg, "False" },
208   {"-debug",        ".debug",         XrmoptionNoArg, "True"  },
209 };
210
211 static argtype vars[] = {
212   { &fade_seconds,  "fadeDuration", "FadeDuration", DEF_FADE_DURATION,  t_Int},
213   { &pan_seconds,   "panDuration",  "PanDuration",  DEF_PAN_DURATION,   t_Int},
214   { &image_seconds, "imageDuration","ImageDuration",DEF_IMAGE_DURATION, t_Int},
215   { &zoom,          "zoom",         "Zoom",         DEF_ZOOM,           t_Int},
216   { &mipmap_p,      "mipmap",       "Mipmap",       DEF_MIPMAP,        t_Bool},
217   { &letterbox_p,   "letterbox",    "Letterbox",    DEF_LETTERBOX,     t_Bool},
218   { &fps_cutoff,    "FPScutoff",    "FPSCutoff",    DEF_FPS_CUTOFF,     t_Int},
219   { &debug_p,       "debug",        "Debug",        DEF_DEBUG,         t_Bool},
220   { &do_titles,     "titles",       "Titles",       DEF_TITLES,        t_Bool},
221 };
222
223 ENTRYPOINT ModeSpecOpt slideshow_opts = {countof(opts), opts, countof(vars), vars, NULL};
224
225
226 static const char *
227 blurb (void)
228 {
229 # ifdef HAVE_COCOA
230   return "GLSlideshow";
231 # else
232   static char buf[255];
233   time_t now = time ((time_t *) 0);
234   char *ct = (char *) ctime (&now);
235   int n = strlen(progname);
236   if (n > 100) n = 99;
237   strncpy(buf, progname, n);
238   buf[n++] = ':';
239   buf[n++] = ' ';
240   strncpy(buf+n, ct+11, 8);
241   strcpy(buf+n+9, ": ");
242   return buf;
243 # endif
244 }
245
246
247 /* Returns the current time in seconds as a double.
248  */
249 static double
250 double_time (void)
251 {
252   struct timeval now;
253 # ifdef GETTIMEOFDAY_TWO_ARGS
254   struct timezone tzp;
255   gettimeofday(&now, &tzp);
256 # else
257   gettimeofday(&now);
258 # endif
259
260   return (now.tv_sec + ((double) now.tv_usec * 0.000001));
261 }
262
263
264 static void image_loaded_cb (const char *filename, XRectangle *geom,
265                              int image_width, int image_height,
266                              int texture_width, int texture_height,
267                              void *closure);
268
269
270 /* Allocate an image structure and start a file loading in the background.
271  */
272 static image *
273 alloc_image (ModeInfo *mi)
274 {
275   slideshow_state *ss = &sss[MI_SCREEN(mi)];
276   int wire = MI_IS_WIREFRAME(mi);
277   image *img = (image *) calloc (1, sizeof (*img));
278
279   img->id = ++ss->image_id;
280   img->loaded_p = False;
281   img->used_p = False;
282   img->mi = mi;
283
284   glGenTextures (1, &img->texid);
285   if (img->texid <= 0) abort();
286
287   ss->image_load_time = ss->now;
288
289   if (wire)
290     image_loaded_cb (0, 0, 0, 0, 0, 0, img);
291   else
292     load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context,
293                         0, 0, mipmap_p, img->texid, image_loaded_cb, img);
294
295   ss->images[ss->nimages++] = img;
296   if (ss->nimages >= countof(ss->images)) abort();
297
298   return img;
299 }
300
301
302 /* Callback that tells us that the texture has been loaded.
303  */
304 static void
305 image_loaded_cb (const char *filename, XRectangle *geom,
306                  int image_width, int image_height,
307                  int texture_width, int texture_height,
308                  void *closure)
309 {
310   image *img = (image *) closure;
311   ModeInfo *mi = img->mi;
312   /* slideshow_state *ss = &sss[MI_SCREEN(mi)]; */
313
314   int wire = MI_IS_WIREFRAME(mi);
315
316   if (wire)
317     {
318       img->w = MI_WIDTH (mi) * (0.5 + frand (1.0));
319       img->h = MI_HEIGHT (mi);
320       img->geom.width  = img->w;
321       img->geom.height = img->h;
322       goto DONE;
323     }
324
325   if (image_width == 0 || image_height == 0)
326     exit (1);
327
328   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
329   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
330                    mipmap_p ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
331
332   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
333   glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
334
335   img->w  = image_width;
336   img->h  = image_height;
337   img->tw = texture_width;
338   img->th = texture_height;
339   img->geom = *geom;
340   img->title = (filename ? strdup (filename) : 0);
341
342   /* If the image's width doesn't come back as the width of the screen,
343      then the image must have been scaled down (due to insufficient
344      texture memory.)  Scale up the coordinates to stretch the image
345      to fill the window.
346    */
347   if (img->w != MI_WIDTH(mi))
348     {
349       double scale = (double) MI_WIDTH(mi) / img->w;
350       img->w  *= scale;
351       img->h  *= scale;
352       img->tw *= scale;
353       img->th *= scale;
354       img->geom.x      *= scale;
355       img->geom.y      *= scale;
356       img->geom.width  *= scale;
357       img->geom.height *= scale;
358     }
359
360   /* xscreensaver-getimage returns paths relative to the image directory
361      now, so leave the sub-directory part in.  Unless it's an absolute path.
362   */
363   if (img->title && img->title[0] == '/')
364     {
365       /* strip filename to part between last "/" and last ".". */
366       char *s = strrchr (img->title, '/');
367       if (s) strcpy (img->title, s+1);
368       s = strrchr (img->title, '.');
369       if (s) *s = 0;
370     }
371
372   if (debug_p)
373     fprintf (stderr, "%s: loaded   img %2d: \"%s\"\n",
374              blurb(), img->id, (img->title ? img->title : "(null)"));
375  DONE:
376
377   img->loaded_p = True;
378 }
379
380
381
382 /* Free the image and texture, after nobody is referencing it.
383  */
384 static void
385 destroy_image (ModeInfo *mi, image *img)
386 {
387   slideshow_state *ss = &sss[MI_SCREEN(mi)];
388   Bool freed_p = False;
389   int i;
390
391   if (!img) abort();
392   if (!img->loaded_p) abort();
393   if (!img->used_p) abort();
394   if (img->texid <= 0) abort();
395   if (img->refcount != 0) abort();
396
397   for (i = 0; i < ss->nimages; i++)             /* unlink it from the list */
398     if (ss->images[i] == img)
399       {
400         int j;
401         for (j = i; j < ss->nimages-1; j++)     /* pull remainder forward */
402           ss->images[j] = ss->images[j+1];
403         ss->images[j] = 0;
404         ss->nimages--;
405         freed_p = True;
406         break;
407       }
408
409   if (!freed_p) abort();
410
411   if (debug_p)
412     fprintf (stderr, "%s: unloaded img %2d: \"%s\"\n",
413              blurb(), img->id, (img->title ? img->title : "(null)"));
414
415   if (img->title) free (img->title);
416   glDeleteTextures (1, &img->texid);
417   free (img);
418 }
419
420
421 /* Return an image to use for a sprite.
422    If it's time for a new one, get a new one.
423    Otherwise, use an old one.
424    Might return 0 if the machine is really slow.
425  */
426 static image *
427 get_image (ModeInfo *mi)
428 {
429   slideshow_state *ss = &sss[MI_SCREEN(mi)];
430   image *img = 0;
431   double now = ss->now;
432   Bool want_new_p = (ss->change_now_p ||
433                      ss->image_load_time + image_seconds <= now);
434   image *new_img = 0;
435   image *old_img = 0;
436   image *loading_img = 0;
437   int i;
438
439   for (i = 0; i < ss->nimages; i++)
440     {
441       image *img2 = ss->images[i];
442       if (!img2) abort();
443       if (!img2->loaded_p)
444         loading_img = img2;
445       else if (!img2->used_p)
446         new_img = img2;
447       else
448         old_img = img2;
449     }
450
451   if (want_new_p && new_img)
452     img = new_img, new_img = 0, ss->change_now_p = False;
453   else if (old_img)
454     img = old_img, old_img = 0;
455   else if (new_img)
456     img = new_img, new_img = 0, ss->change_now_p = False;
457
458   /* Make sure that there is always one unused image in the pipe.
459    */
460   if (!new_img && !loading_img)
461     alloc_image (mi);
462
463   return img;
464 }
465
466
467 /* Pick random starting and ending positions for the given sprite.
468  */
469 static void
470 randomize_sprite (ModeInfo *mi, sprite *sp)
471 {
472   int vp_w = MI_WIDTH(mi);
473   int vp_h = MI_HEIGHT(mi);
474   int img_w = sp->img->geom.width;
475   int img_h = sp->img->geom.height;
476   int min_w, max_w;
477   double ratio = (double) img_h / img_w;
478
479   if (letterbox_p)
480     {
481       min_w = img_w;
482     }
483   else
484     {
485       if (img_w < vp_w)
486         min_w = vp_w;
487       else
488         min_w = img_w * (float) vp_h / img_h;
489     }
490
491   max_w = min_w * 100 / zoom;
492
493   sp->from.w = min_w + frand ((max_w - min_w) * 0.4);
494   sp->to.w   = max_w - frand ((max_w - min_w) * 0.4);
495   sp->from.h = sp->from.w * ratio;
496   sp->to.h   = sp->to.w   * ratio;
497
498   if (zoom == 100)      /* only one box, and it is centered */
499     {
500       sp->from.x = (sp->from.w > vp_w
501                     ? -(sp->from.w - vp_w) / 2
502                     :  (vp_w - sp->from.w) / 2);
503       sp->from.y = (sp->from.h > vp_h
504                     ? -(sp->from.h - vp_h) / 2
505                     :  (vp_h - sp->from.h) / 2);
506       sp->to = sp->from;
507     }
508   else                  /* position both boxes randomly */
509     {
510       sp->from.x = (sp->from.w > vp_w
511                     ? -frand (sp->from.w - vp_w)
512                     :  frand (vp_w - sp->from.w));
513       sp->from.y = (sp->from.h > vp_h
514                     ? -frand (sp->from.h - vp_h)
515                     :  frand (vp_h - sp->from.h));
516       sp->to.x   = (sp->to.w > vp_w
517                     ? -frand (sp->to.w - vp_w)
518                     :  frand (vp_w - sp->to.w));
519       sp->to.y   = (sp->to.h > vp_h
520                     ? -frand (sp->to.h - vp_h)
521                     :  frand (vp_h - sp->to.h));
522     }
523
524   if (random() & 1)
525     {
526       rect swap = sp->to;
527       sp->to = sp->from;
528       sp->from = swap;
529     }
530
531   /* Make sure the aspect ratios are within 0.001 of each other.
532    */
533   {
534     int r1 = 0.5 + (sp->from.w * 1000 / sp->from.h);
535     int r2 = 0.5 + (sp->to.w   * 1000 / sp->to.h);
536     if (r1 < r2-1 || r1 > r2+1)
537       {
538         fprintf (stderr,
539                  "%s: botched aspect: %f x %f (%d) vs  %f x %f (%d): %s\n",
540                  progname, 
541                  sp->from.w, sp->from.h, r1,
542                  sp->to.w, sp->to.h, r2,
543                  (sp->img->title ? sp->img->title : "[null]"));
544         abort();
545       }
546   }
547
548   sp->from.x /= vp_w;
549   sp->from.y /= vp_h;
550   sp->from.w /= vp_w;
551   sp->from.h /= vp_h;
552   sp->to.x   /= vp_w;
553   sp->to.y   /= vp_h;
554   sp->to.w   /= vp_w;
555   sp->to.h   /= vp_h;
556 }
557
558
559 /* Allocate a new sprite and start its animation going.
560  */
561 static sprite *
562 new_sprite (ModeInfo *mi)
563 {
564   slideshow_state *ss = &sss[MI_SCREEN(mi)];
565   image *img = get_image (mi);
566   sprite *sp;
567
568   if (!img)
569     {
570       /* Oops, no images yet!  The machine is probably hurting bad.
571          Let's give it some time before thrashing again. */
572       usleep (250000);
573       return 0;
574     }
575
576   sp = (sprite *) calloc (1, sizeof (*sp));
577   sp->id = ++ss->sprite_id;
578   sp->start_time = ss->now;
579   sp->state_time = sp->start_time;
580   sp->state = sp->prev_state = NEW;
581   sp->img = img;
582
583   sp->img->refcount++;
584   sp->img->used_p = True;
585
586   ss->sprites[ss->nsprites++] = sp;
587   if (ss->nsprites >= countof(ss->sprites)) abort();
588
589   randomize_sprite (mi, sp);
590
591   return sp;
592 }
593
594
595 /* Free the given sprite, and decrement the reference count on its image.
596  */
597 static void
598 destroy_sprite (ModeInfo *mi, sprite *sp)
599 {
600   slideshow_state *ss = &sss[MI_SCREEN(mi)];
601   Bool freed_p = False;
602   image *img;
603   int i;
604
605   if (!sp) abort();
606   if (sp->state != DEAD) abort();
607   img = sp->img;
608   if (!img) abort();
609   if (!img->loaded_p) abort();
610   if (!img->used_p) abort();
611   if (img->refcount <= 0) abort();
612
613   for (i = 0; i < ss->nsprites; i++)            /* unlink it from the list */
614     if (ss->sprites[i] == sp)
615       {
616         int j;
617         for (j = i; j < ss->nsprites-1; j++)    /* pull remainder forward */
618           ss->sprites[j] = ss->sprites[j+1];
619         ss->sprites[j] = 0;
620         ss->nsprites--;
621         freed_p = True;
622         break;
623       }
624
625   if (!freed_p) abort();
626   free (sp);
627   sp = 0;
628
629   img->refcount--;
630   if (img->refcount < 0) abort();
631   if (img->refcount == 0)
632     destroy_image (mi, img);
633 }
634
635
636 /* Updates the sprite for the current frame of the animation based on
637    its creation time compared to the current wall clock.
638  */
639 static void
640 tick_sprite (ModeInfo *mi, sprite *sp)
641 {
642   slideshow_state *ss = &sss[MI_SCREEN(mi)];
643   image *img = sp->img;
644   double now = ss->now;
645   double secs;
646   double ratio;
647   rect prev_rect = sp->current;
648   GLfloat prev_opacity = sp->opacity;
649
650   if (! sp->img) abort();
651   if (! img->loaded_p) abort();
652
653   secs = now - sp->start_time;
654   ratio = secs / (pan_seconds + fade_seconds);
655   if (ratio > 1) ratio = 1;
656
657   sp->current.x = sp->from.x + ratio * (sp->to.x - sp->from.x);
658   sp->current.y = sp->from.y + ratio * (sp->to.y - sp->from.y);
659   sp->current.w = sp->from.w + ratio * (sp->to.w - sp->from.w);
660   sp->current.h = sp->from.h + ratio * (sp->to.h - sp->from.h);
661
662   sp->prev_state = sp->state;
663
664   if (secs < fade_seconds)
665     {
666       sp->state = IN;
667       sp->opacity = secs / (GLfloat) fade_seconds;
668     }
669   else if (secs < pan_seconds)
670     {
671       sp->state = FULL;
672       sp->opacity = 1;
673     }
674   else if (secs < pan_seconds + fade_seconds)
675     {
676       sp->state = OUT;
677       sp->opacity = 1 - ((secs - pan_seconds) / (GLfloat) fade_seconds);
678     }
679   else
680     {
681       sp->state = DEAD;
682       sp->opacity = 0;
683     }
684
685   if (sp->state != sp->prev_state &&
686       (sp->prev_state == IN ||
687        sp->prev_state == FULL))
688     {
689       double secs = now - sp->state_time;
690
691       if (debug_p)
692         fprintf (stderr,
693                  "%s: %s %3d frames %2.0f sec %5.1f fps (%.1f fps?)\n",
694                  blurb(),
695                  (sp->prev_state == IN ? "fade" : "pan "),
696                  sp->frame_count,
697                  secs,
698                  sp->frame_count / secs,
699                  ss->theoretical_fps);
700
701       sp->state_time = now;
702       sp->frame_count = 0;
703     }
704
705   sp->frame_count++;
706
707   if (sp->state != DEAD &&
708       (prev_rect.x != sp->current.x ||
709        prev_rect.y != sp->current.y ||
710        prev_rect.w != sp->current.w ||
711        prev_rect.h != sp->current.h ||
712        prev_opacity != sp->opacity))
713     ss->redisplay_needed_p = True;
714 }
715
716
717 /* Draw the given sprite at the phase of its animation dictated by
718    its creation time compared to the current wall clock.
719  */
720 static void
721 draw_sprite (ModeInfo *mi, sprite *sp)
722 {
723   slideshow_state *ss = &sss[MI_SCREEN(mi)];
724   int wire = MI_IS_WIREFRAME(mi);
725   image *img = sp->img;
726
727   if (! sp->img) abort();
728   if (! img->loaded_p) abort();
729
730   glPushMatrix();
731   {
732     glTranslatef (sp->current.x, sp->current.y, 0);
733     glScalef (sp->current.w, sp->current.h, 1);
734
735     if (wire)                   /* Draw a grid inside the box */
736       {
737         GLfloat dy = 0.1;
738         GLfloat dx = dy * img->w / img->h;
739         GLfloat x, y;
740
741         if (sp->id & 1)
742           glColor4f (sp->opacity, 0, 0, 1);
743         else
744           glColor4f (0, 0, sp->opacity, 1);
745
746         glBegin(GL_LINES);
747         glVertex3f (0, 0, 0); glVertex3f (1, 1, 0);
748         glVertex3f (1, 0, 0); glVertex3f (0, 1, 0);
749
750         for (y = 0; y < 1+dy; y += dy)
751           {
752             GLfloat yy = (y > 1 ? 1 : y);
753             for (x = 0.5; x < 1+dx; x += dx)
754               {
755                 GLfloat xx = (x > 1 ? 1 : x);
756                 glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
757                 glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
758               }
759             for (x = 0.5; x > -dx; x -= dx)
760               {
761                 GLfloat xx = (x < 0 ? 0 : x);
762                 glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
763                 glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
764               }
765           }
766         glEnd();
767       }
768     else                        /* Draw the texture quad */
769       {
770         GLfloat texw  = img->geom.width  / (GLfloat) img->tw;
771         GLfloat texh  = img->geom.height / (GLfloat) img->th;
772         GLfloat texx1 = img->geom.x / (GLfloat) img->tw;
773         GLfloat texy1 = img->geom.y / (GLfloat) img->th;
774         GLfloat texx2 = texx1 + texw;
775         GLfloat texy2 = texy1 + texh;
776
777         glBindTexture (GL_TEXTURE_2D, img->texid);
778         glColor4f (1, 1, 1, sp->opacity);
779         glNormal3f (0, 0, 1);
780         glBegin (GL_QUADS);
781         glTexCoord2f (texx1, texy2); glVertex3f (0, 0, 0);
782         glTexCoord2f (texx2, texy2); glVertex3f (1, 0, 0);
783         glTexCoord2f (texx2, texy1); glVertex3f (1, 1, 0);
784         glTexCoord2f (texx1, texy1); glVertex3f (0, 1, 0);
785         glEnd();
786
787         if (debug_p)            /* Draw a border around the image */
788           {
789             if (!wire) glDisable (GL_TEXTURE_2D);
790
791             if (sp->id & 1)
792               glColor4f (sp->opacity, 0, 0, 1);
793             else
794               glColor4f (0, 0, sp->opacity, 1);
795
796             glBegin (GL_LINE_LOOP);
797             glVertex3f (0, 0, 0);
798             glVertex3f (0, 1, 0);
799             glVertex3f (1, 1, 0);
800             glVertex3f (1, 0, 0);
801             glEnd();
802
803             if (!wire) glEnable (GL_TEXTURE_2D);
804           }
805       }
806
807
808     if (do_titles &&
809         img->title && *img->title &&
810         (sp->state == IN || sp->state == FULL))
811       {
812         glColor4f (1, 1, 1, sp->opacity);
813         print_texture_label (mi->dpy, ss->font_data,
814                              mi->xgwa.width, mi->xgwa.height,
815                              1, img->title);
816       }
817   }
818   glPopMatrix();
819
820   if (debug_p)
821     {
822       if (!wire) glDisable (GL_TEXTURE_2D);
823
824       if (sp->id & 1)
825         glColor4f (1, 0, 0, 1);
826       else
827         glColor4f (0, 0, 1, 1);
828
829       /* Draw the "from" and "to" boxes
830        */
831       glBegin (GL_LINE_LOOP);
832       glVertex3f (sp->from.x,              sp->from.y,              0);
833       glVertex3f (sp->from.x + sp->from.w, sp->from.y,              0);
834       glVertex3f (sp->from.x + sp->from.w, sp->from.y + sp->from.h, 0);
835       glVertex3f (sp->from.x,              sp->from.y + sp->from.h, 0);
836       glEnd();
837
838       glBegin (GL_LINE_LOOP);
839       glVertex3f (sp->to.x,                sp->to.y,                0);
840       glVertex3f (sp->to.x + sp->to.w,     sp->to.y,                0);
841       glVertex3f (sp->to.x + sp->to.w,     sp->to.y + sp->to.h,     0);
842       glVertex3f (sp->to.x,                sp->to.y + sp->to.h,     0);
843       glEnd();
844
845       if (!wire) glEnable (GL_TEXTURE_2D);
846     }
847 }
848
849
850 static void
851 tick_sprites (ModeInfo *mi)
852 {
853   slideshow_state *ss = &sss[MI_SCREEN(mi)];
854   int i;
855   for (i = 0; i < ss->nsprites; i++)
856       tick_sprite (mi, ss->sprites[i]);
857 }
858
859
860 static void
861 draw_sprites (ModeInfo *mi)
862 {
863   slideshow_state *ss = &sss[MI_SCREEN(mi)];
864   int i;
865
866   glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
867
868   glPushMatrix();
869
870   {
871     GLfloat rot = current_device_rotation();
872     glTranslatef (0.5, 0.5, 0);
873     glRotatef(rot, 0, 0, 1);
874     if ((rot >  45 && rot <  135) ||
875         (rot < -45 && rot > -135))
876       {
877         GLfloat s = MI_WIDTH(mi) / (GLfloat) MI_HEIGHT(mi);
878         glScalef (s, 1/s, 1);
879       }
880     glTranslatef (-0.5, -0.5, 0);
881   }
882
883   for (i = 0; i < ss->nsprites; i++)
884     draw_sprite (mi, ss->sprites[i]);
885   glPopMatrix();
886
887   if (debug_p)                          /* draw a white box (the "screen") */
888     {
889       int wire = MI_IS_WIREFRAME(mi);
890
891       if (!wire) glDisable (GL_TEXTURE_2D);
892
893       glColor4f (1, 1, 1, 1);
894       glBegin (GL_LINE_LOOP);
895       glVertex3f (0, 0, 0);
896       glVertex3f (0, 1, 0);
897       glVertex3f (1, 1, 0);
898       glVertex3f (1, 0, 0);
899       glEnd();
900
901       if (!wire) glEnable (GL_TEXTURE_2D);
902     }
903 }
904
905
906 ENTRYPOINT void
907 reshape_slideshow (ModeInfo *mi, int width, int height)
908 {
909   slideshow_state *ss = &sss[MI_SCREEN(mi)];
910   GLfloat s;
911   glViewport (0, 0, width, height);
912   glMatrixMode (GL_PROJECTION);
913   glLoadIdentity();
914   glMatrixMode (GL_MODELVIEW);
915   glLoadIdentity();
916
917   s = 2;
918
919   if (debug_p)
920     {
921       s *= (zoom / 100.0) * 0.75;
922       if (s < 0.1) s = 0.1;
923     }
924
925   glScalef (s, s, s);
926   glTranslatef (-0.5, -0.5, 0);
927
928   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
929
930   ss->redisplay_needed_p = True;
931 }
932
933
934 ENTRYPOINT Bool
935 slideshow_handle_event (ModeInfo *mi, XEvent *event)
936 {
937   slideshow_state *ss = &sss[MI_SCREEN(mi)];
938
939   if (event->xany.type == Expose ||
940            event->xany.type == GraphicsExpose ||
941            event->xany.type == VisibilityNotify)
942     {
943       ss->redisplay_needed_p = True;
944       if (debug_p)
945         fprintf (stderr, "%s: exposure\n", blurb());
946       return False;
947     }
948   else if (screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event))
949     {
950       ss->change_now_p = True;
951       return True;
952     }
953
954   return False;
955 }
956
957
958 /* Do some sanity checking on various user-supplied values, and make
959    sure they are all internally consistent.
960  */
961 static void
962 sanity_check (ModeInfo *mi)
963 {
964   if (zoom < 1) zoom = 1;           /* zoom is a positive percentage */
965   else if (zoom > 100) zoom = 100;
966
967   if (zoom == 100)                  /* with no zooming, there is no panning */
968     pan_seconds = 0;
969
970   if (pan_seconds < fade_seconds)   /* pan is inclusive of fade */
971     pan_seconds = fade_seconds;
972
973   if (pan_seconds == 0)             /* no zero-length cycles, please... */
974     pan_seconds = 1;
975
976   if (image_seconds < pan_seconds)  /* we only change images at fade-time */
977     image_seconds = pan_seconds;
978
979   /* If we're not panning/zooming within the image, then there's no point
980      in crossfading the image with itself -- only do crossfades when changing
981      to a new image. */
982   if (zoom == 100 && pan_seconds < image_seconds)
983     pan_seconds = image_seconds;
984
985   /* No need to use mipmaps if we're not changing the image size much */
986   if (zoom >= 80) mipmap_p = False;
987
988   if      (fps_cutoff < 0)  fps_cutoff = 0;
989   else if (fps_cutoff > 30) fps_cutoff = 30;
990 }
991
992
993 static void
994 check_fps (ModeInfo *mi)
995 {
996 #ifndef HAVE_COCOA  /* always assume Cocoa is fast enough */
997
998   slideshow_state *ss = &sss[MI_SCREEN(mi)];
999
1000   double start_time, end_time, wall_elapsed, frame_duration, fps;
1001   int i;
1002
1003   start_time = ss->now;
1004   end_time = double_time();
1005   frame_duration = end_time - start_time;   /* time spent drawing this frame */
1006   ss->time_elapsed += frame_duration;       /* time spent drawing all frames */
1007   ss->frames_elapsed++;
1008
1009   wall_elapsed = end_time - ss->dawn_of_time;
1010   fps = ss->frames_elapsed / ss->time_elapsed;
1011   ss->theoretical_fps = fps;
1012
1013   if (ss->checked_fps_p) return;
1014
1015   if (wall_elapsed <= 8)    /* too early to be sure */
1016     return;
1017
1018   ss->checked_fps_p = True;
1019
1020   if (fps >= fps_cutoff)
1021     {
1022       if (debug_p)
1023         fprintf (stderr,
1024                  "%s: %.1f fps is fast enough (with %d frames in %.1f secs)\n",
1025                  blurb(), fps, ss->frames_elapsed, wall_elapsed);
1026       return;
1027     }
1028
1029   fprintf (stderr,
1030            "%s: only %.1f fps!  Turning off pan/fade to compensate...\n",
1031            blurb(), fps);
1032   zoom = 100;
1033   fade_seconds = 0;
1034
1035   sanity_check (mi);
1036
1037   for (i = 0; i < ss->nsprites; i++)
1038     {
1039       sprite *sp = ss->sprites[i];
1040       randomize_sprite (mi, sp);
1041       sp->state = FULL;
1042     }
1043
1044   ss->redisplay_needed_p = True;
1045
1046   /* Need this in case zoom changed. */
1047   reshape_slideshow (mi, mi->xgwa.width, mi->xgwa.height);
1048 #endif /* HAVE_COCOA */
1049 }
1050
1051
1052 /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
1053  */
1054 static void
1055 hack_resources (void)
1056 {
1057 #if 0
1058   char *res = "desktopGrabber";
1059   char *val = get_string_resource (res, "DesktopGrabber");
1060   char buf1[255];
1061   char buf2[255];
1062   XrmValue value;
1063   sprintf (buf1, "%.100s.%.100s", progclass, res);
1064   sprintf (buf2, "%.200s -v", val);
1065   value.addr = buf2;
1066   value.size = strlen(buf2);
1067   XrmPutResource (&db, buf1, "String", &value);
1068 #endif
1069 }
1070
1071
1072 ENTRYPOINT void
1073 init_slideshow (ModeInfo *mi)
1074 {
1075   int screen = MI_SCREEN(mi);
1076   slideshow_state *ss;
1077   int wire = MI_IS_WIREFRAME(mi);
1078   
1079   if (sss == NULL) {
1080     if ((sss = (slideshow_state *)
1081          calloc (MI_NUM_SCREENS(mi), sizeof(slideshow_state))) == NULL)
1082       return;
1083   }
1084   ss = &sss[screen];
1085
1086   if ((ss->glx_context = init_GL(mi)) != NULL) {
1087     reshape_slideshow (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
1088   } else {
1089     MI_CLEARWINDOW(mi);
1090   }
1091
1092   if (debug_p)
1093     fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
1094              blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
1095
1096   sanity_check(mi);
1097
1098   if (debug_p)
1099     fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n\n",
1100              blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
1101
1102   glDisable (GL_LIGHTING);
1103   glDisable (GL_DEPTH_TEST);
1104   glDepthMask (GL_FALSE);
1105   glEnable (GL_CULL_FACE);
1106   glCullFace (GL_BACK);
1107
1108   if (! wire)
1109     {
1110       glEnable (GL_TEXTURE_2D);
1111       glShadeModel (GL_SMOOTH);
1112       glEnable (GL_BLEND);
1113       glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1114     }
1115
1116   if (debug_p) glLineWidth (3);
1117
1118   ss->font_data = load_texture_font (mi->dpy, "titleFont");
1119
1120   if (debug_p)
1121     hack_resources();
1122
1123   ss->now = double_time();
1124   ss->dawn_of_time = ss->now;
1125   ss->prev_frame_time = ss->now;
1126
1127   ss->awaiting_first_image_p = True;
1128   alloc_image (mi);
1129 }
1130
1131
1132 ENTRYPOINT void
1133 draw_slideshow (ModeInfo *mi)
1134 {
1135   slideshow_state *ss = &sss[MI_SCREEN(mi)];
1136   int i;
1137
1138   if (!ss->glx_context)
1139     return;
1140
1141   glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
1142
1143   if (ss->awaiting_first_image_p)
1144     {
1145       image *img = ss->images[0];
1146       if (!img) abort();
1147       if (!img->loaded_p)
1148         return;
1149
1150       ss->awaiting_first_image_p = False;
1151       ss->dawn_of_time = double_time();
1152
1153       /* start the very first sprite fading in */
1154       new_sprite (mi);
1155     }
1156
1157   ss->now = double_time();
1158
1159   /* Each sprite has three states: fading in, full, fading out.
1160      The in/out states overlap like this:
1161
1162      iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . . . . . . . . . . . 
1163      . . . . . . . . . iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . .
1164      . . . . . . . . . . . . . . . . . . iiiiiiFFFFFFFFFFFFooooo
1165
1166      So as soon as a sprite goes into the "out" state, we create
1167      a new sprite (in the "in" state.)
1168    */
1169
1170   if (ss->nsprites > 2) abort();
1171
1172   /* If a sprite is just entering the fade-out state,
1173      then add a new sprite in the fade-in state.
1174    */
1175   for (i = 0; i < ss->nsprites; i++)
1176     {
1177       sprite *sp = ss->sprites[i];
1178       if (sp->state != sp->prev_state &&
1179           sp->state == (fade_seconds == 0 ? DEAD : OUT))
1180         new_sprite (mi);
1181     }
1182
1183   tick_sprites (mi);
1184
1185   /* Now garbage collect the dead sprites.
1186    */
1187   for (i = 0; i < ss->nsprites; i++)
1188     {
1189       sprite *sp = ss->sprites[i];
1190       if (sp->state == DEAD)
1191         {
1192           destroy_sprite (mi, sp);
1193           i--;
1194         }
1195     }
1196
1197   /* We can only ever end up with no sprites at all if the machine is
1198      being really slow and we hopped states directly from FULL to DEAD
1199      without passing OUT... */
1200   if (ss->nsprites == 0)
1201     new_sprite (mi);
1202
1203   if (!ss->redisplay_needed_p)
1204     return;
1205
1206   if (debug_p && ss->now - ss->prev_frame_time > 1)
1207     fprintf (stderr, "%s: static screen for %.1f secs\n",
1208              blurb(), ss->now - ss->prev_frame_time);
1209
1210   draw_sprites (mi);
1211
1212   if (mi->fps_p) do_fps (mi);
1213
1214   glFinish();
1215   glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
1216   ss->prev_frame_time = ss->now;
1217   ss->redisplay_needed_p = False;
1218   check_fps (mi);
1219 }
1220
1221 XSCREENSAVER_MODULE_2 ("GLSlideshow", glslideshow, slideshow)
1222
1223 #endif /* USE_GL */