http://www.jwz.org/xscreensaver/xscreensaver-5.13.tar.gz
[xscreensaver] / hacks / glx / glslideshow.c
1 /* glslideshow, Copyright (c) 2003-2011 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 0 /* xscreensaver-getimage returns paths relative to the image directory
362           now, so leave the sub-directory part in.
363         */
364   if (img->title)  /* strip filename to part between last "/" and last ".". */
365     {
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 # endif /* 0 */
372
373   if (debug_p)
374     fprintf (stderr, "%s: loaded   img %2d: \"%s\"\n",
375              blurb(), img->id, (img->title ? img->title : "(null)"));
376  DONE:
377
378   img->loaded_p = True;
379 }
380
381
382
383 /* Free the image and texture, after nobody is referencing it.
384  */
385 static void
386 destroy_image (ModeInfo *mi, image *img)
387 {
388   slideshow_state *ss = &sss[MI_SCREEN(mi)];
389   Bool freed_p = False;
390   int i;
391
392   if (!img) abort();
393   if (!img->loaded_p) abort();
394   if (!img->used_p) abort();
395   if (img->texid <= 0) abort();
396   if (img->refcount != 0) abort();
397
398   for (i = 0; i < ss->nimages; i++)             /* unlink it from the list */
399     if (ss->images[i] == img)
400       {
401         int j;
402         for (j = i; j < ss->nimages-1; j++)     /* pull remainder forward */
403           ss->images[j] = ss->images[j+1];
404         ss->images[j] = 0;
405         ss->nimages--;
406         freed_p = True;
407         break;
408       }
409
410   if (!freed_p) abort();
411
412   if (debug_p)
413     fprintf (stderr, "%s: unloaded img %2d: \"%s\"\n",
414              blurb(), img->id, (img->title ? img->title : "(null)"));
415
416   if (img->title) free (img->title);
417   glDeleteTextures (1, &img->texid);
418   free (img);
419 }
420
421
422 /* Return an image to use for a sprite.
423    If it's time for a new one, get a new one.
424    Otherwise, use an old one.
425    Might return 0 if the machine is really slow.
426  */
427 static image *
428 get_image (ModeInfo *mi)
429 {
430   slideshow_state *ss = &sss[MI_SCREEN(mi)];
431   image *img = 0;
432   double now = ss->now;
433   Bool want_new_p = (ss->change_now_p ||
434                      ss->image_load_time + image_seconds <= now);
435   image *new_img = 0;
436   image *old_img = 0;
437   image *loading_img = 0;
438   int i;
439
440   for (i = 0; i < ss->nimages; i++)
441     {
442       image *img2 = ss->images[i];
443       if (!img2) abort();
444       if (!img2->loaded_p)
445         loading_img = img2;
446       else if (!img2->used_p)
447         new_img = img2;
448       else
449         old_img = img2;
450     }
451
452   if (want_new_p && new_img)
453     img = new_img, new_img = 0, ss->change_now_p = False;
454   else if (old_img)
455     img = old_img, old_img = 0;
456   else if (new_img)
457     img = new_img, new_img = 0, ss->change_now_p = False;
458
459   /* Make sure that there is always one unused image in the pipe.
460    */
461   if (!new_img && !loading_img)
462     alloc_image (mi);
463
464   return img;
465 }
466
467
468 /* Pick random starting and ending positions for the given sprite.
469  */
470 static void
471 randomize_sprite (ModeInfo *mi, sprite *sp)
472 {
473   int vp_w = MI_WIDTH(mi);
474   int vp_h = MI_HEIGHT(mi);
475   int img_w = sp->img->geom.width;
476   int img_h = sp->img->geom.height;
477   int min_w, max_w;
478   double ratio = (double) img_h / img_w;
479
480   if (letterbox_p)
481     {
482       min_w = img_w;
483     }
484   else
485     {
486       if (img_w < vp_w)
487         min_w = vp_w;
488       else
489         min_w = img_w * (float) vp_h / img_h;
490     }
491
492   max_w = min_w * 100 / zoom;
493
494   sp->from.w = min_w + frand ((max_w - min_w) * 0.4);
495   sp->to.w   = max_w - frand ((max_w - min_w) * 0.4);
496   sp->from.h = sp->from.w * ratio;
497   sp->to.h   = sp->to.w   * ratio;
498
499   if (zoom == 100)      /* only one box, and it is centered */
500     {
501       sp->from.x = (sp->from.w > vp_w
502                     ? -(sp->from.w - vp_w) / 2
503                     :  (vp_w - sp->from.w) / 2);
504       sp->from.y = (sp->from.h > vp_h
505                     ? -(sp->from.h - vp_h) / 2
506                     :  (vp_h - sp->from.h) / 2);
507       sp->to = sp->from;
508     }
509   else                  /* position both boxes randomly */
510     {
511       sp->from.x = (sp->from.w > vp_w
512                     ? -frand (sp->from.w - vp_w)
513                     :  frand (vp_w - sp->from.w));
514       sp->from.y = (sp->from.h > vp_h
515                     ? -frand (sp->from.h - vp_h)
516                     :  frand (vp_h - sp->from.h));
517       sp->to.x   = (sp->to.w > vp_w
518                     ? -frand (sp->to.w - vp_w)
519                     :  frand (vp_w - sp->to.w));
520       sp->to.y   = (sp->to.h > vp_h
521                     ? -frand (sp->to.h - vp_h)
522                     :  frand (vp_h - sp->to.h));
523     }
524
525   if (random() & 1)
526     {
527       rect swap = sp->to;
528       sp->to = sp->from;
529       sp->from = swap;
530     }
531
532   /* Make sure the aspect ratios are within 0.001 of each other.
533    */
534   {
535     int r1 = 0.5 + (sp->from.w * 1000 / sp->from.h);
536     int r2 = 0.5 + (sp->to.w   * 1000 / sp->to.h);
537     if (r1 < r2-1 || r1 > r2+1)
538       {
539         fprintf (stderr,
540                  "%s: botched aspect: %f x %f (%d) vs  %f x %f (%d): %s\n",
541                  progname, 
542                  sp->from.w, sp->from.h, r1,
543                  sp->to.w, sp->to.h, r2,
544                  (sp->img->title ? sp->img->title : "[null]"));
545         abort();
546       }
547   }
548
549   sp->from.x /= vp_w;
550   sp->from.y /= vp_h;
551   sp->from.w /= vp_w;
552   sp->from.h /= vp_h;
553   sp->to.x   /= vp_w;
554   sp->to.y   /= vp_h;
555   sp->to.w   /= vp_w;
556   sp->to.h   /= vp_h;
557 }
558
559
560 /* Allocate a new sprite and start its animation going.
561  */
562 static sprite *
563 new_sprite (ModeInfo *mi)
564 {
565   slideshow_state *ss = &sss[MI_SCREEN(mi)];
566   image *img = get_image (mi);
567   sprite *sp;
568
569   if (!img)
570     {
571       /* Oops, no images yet!  The machine is probably hurting bad.
572          Let's give it some time before thrashing again. */
573       usleep (250000);
574       return 0;
575     }
576
577   sp = (sprite *) calloc (1, sizeof (*sp));
578   sp->id = ++ss->sprite_id;
579   sp->start_time = ss->now;
580   sp->state_time = sp->start_time;
581   sp->state = sp->prev_state = NEW;
582   sp->img = img;
583
584   sp->img->refcount++;
585   sp->img->used_p = True;
586
587   ss->sprites[ss->nsprites++] = sp;
588   if (ss->nsprites >= countof(ss->sprites)) abort();
589
590   randomize_sprite (mi, sp);
591
592   return sp;
593 }
594
595
596 /* Free the given sprite, and decrement the reference count on its image.
597  */
598 static void
599 destroy_sprite (ModeInfo *mi, sprite *sp)
600 {
601   slideshow_state *ss = &sss[MI_SCREEN(mi)];
602   Bool freed_p = False;
603   image *img;
604   int i;
605
606   if (!sp) abort();
607   if (sp->state != DEAD) abort();
608   img = sp->img;
609   if (!img) abort();
610   if (!img->loaded_p) abort();
611   if (!img->used_p) abort();
612   if (img->refcount <= 0) abort();
613
614   for (i = 0; i < ss->nsprites; i++)            /* unlink it from the list */
615     if (ss->sprites[i] == sp)
616       {
617         int j;
618         for (j = i; j < ss->nsprites-1; j++)    /* pull remainder forward */
619           ss->sprites[j] = ss->sprites[j+1];
620         ss->sprites[j] = 0;
621         ss->nsprites--;
622         freed_p = True;
623         break;
624       }
625
626   if (!freed_p) abort();
627   free (sp);
628   sp = 0;
629
630   img->refcount--;
631   if (img->refcount < 0) abort();
632   if (img->refcount == 0)
633     destroy_image (mi, img);
634 }
635
636
637 /* Updates the sprite for the current frame of the animation based on
638    its creation time compared to the current wall clock.
639  */
640 static void
641 tick_sprite (ModeInfo *mi, sprite *sp)
642 {
643   slideshow_state *ss = &sss[MI_SCREEN(mi)];
644   image *img = sp->img;
645   double now = ss->now;
646   double secs;
647   double ratio;
648   rect prev_rect = sp->current;
649   GLfloat prev_opacity = sp->opacity;
650
651   if (! sp->img) abort();
652   if (! img->loaded_p) abort();
653
654   secs = now - sp->start_time;
655   ratio = secs / (pan_seconds + fade_seconds);
656   if (ratio > 1) ratio = 1;
657
658   sp->current.x = sp->from.x + ratio * (sp->to.x - sp->from.x);
659   sp->current.y = sp->from.y + ratio * (sp->to.y - sp->from.y);
660   sp->current.w = sp->from.w + ratio * (sp->to.w - sp->from.w);
661   sp->current.h = sp->from.h + ratio * (sp->to.h - sp->from.h);
662
663   sp->prev_state = sp->state;
664
665   if (secs < fade_seconds)
666     {
667       sp->state = IN;
668       sp->opacity = secs / (GLfloat) fade_seconds;
669     }
670   else if (secs < pan_seconds)
671     {
672       sp->state = FULL;
673       sp->opacity = 1;
674     }
675   else if (secs < pan_seconds + fade_seconds)
676     {
677       sp->state = OUT;
678       sp->opacity = 1 - ((secs - pan_seconds) / (GLfloat) fade_seconds);
679     }
680   else
681     {
682       sp->state = DEAD;
683       sp->opacity = 0;
684     }
685
686   if (sp->state != sp->prev_state &&
687       (sp->prev_state == IN ||
688        sp->prev_state == FULL))
689     {
690       double secs = now - sp->state_time;
691
692       if (debug_p)
693         fprintf (stderr,
694                  "%s: %s %3d frames %2.0f sec %5.1f fps (%.1f fps?)\n",
695                  blurb(),
696                  (sp->prev_state == IN ? "fade" : "pan "),
697                  sp->frame_count,
698                  secs,
699                  sp->frame_count / secs,
700                  ss->theoretical_fps);
701
702       sp->state_time = now;
703       sp->frame_count = 0;
704     }
705
706   sp->frame_count++;
707
708   if (sp->state != DEAD &&
709       (prev_rect.x != sp->current.x ||
710        prev_rect.y != sp->current.y ||
711        prev_rect.w != sp->current.w ||
712        prev_rect.h != sp->current.h ||
713        prev_opacity != sp->opacity))
714     ss->redisplay_needed_p = True;
715 }
716
717
718 /* Draw the given sprite at the phase of its animation dictated by
719    its creation time compared to the current wall clock.
720  */
721 static void
722 draw_sprite (ModeInfo *mi, sprite *sp)
723 {
724   slideshow_state *ss = &sss[MI_SCREEN(mi)];
725   int wire = MI_IS_WIREFRAME(mi);
726   image *img = sp->img;
727
728   if (! sp->img) abort();
729   if (! img->loaded_p) abort();
730
731   glPushMatrix();
732   {
733     glTranslatef (sp->current.x, sp->current.y, 0);
734     glScalef (sp->current.w, sp->current.h, 1);
735
736     if (wire)                   /* Draw a grid inside the box */
737       {
738         GLfloat dy = 0.1;
739         GLfloat dx = dy * img->w / img->h;
740         GLfloat x, y;
741
742         if (sp->id & 1)
743           glColor4f (sp->opacity, 0, 0, 1);
744         else
745           glColor4f (0, 0, sp->opacity, 1);
746
747         glBegin(GL_LINES);
748         glVertex3f (0, 0, 0); glVertex3f (1, 1, 0);
749         glVertex3f (1, 0, 0); glVertex3f (0, 1, 0);
750
751         for (y = 0; y < 1+dy; y += dy)
752           {
753             GLfloat yy = (y > 1 ? 1 : y);
754             for (x = 0.5; x < 1+dx; x += dx)
755               {
756                 GLfloat xx = (x > 1 ? 1 : x);
757                 glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
758                 glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
759               }
760             for (x = 0.5; x > -dx; x -= dx)
761               {
762                 GLfloat xx = (x < 0 ? 0 : x);
763                 glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
764                 glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
765               }
766           }
767         glEnd();
768       }
769     else                        /* Draw the texture quad */
770       {
771         GLfloat texw  = img->geom.width  / (GLfloat) img->tw;
772         GLfloat texh  = img->geom.height / (GLfloat) img->th;
773         GLfloat texx1 = img->geom.x / (GLfloat) img->tw;
774         GLfloat texy1 = img->geom.y / (GLfloat) img->th;
775         GLfloat texx2 = texx1 + texw;
776         GLfloat texy2 = texy1 + texh;
777
778         glBindTexture (GL_TEXTURE_2D, img->texid);
779         glColor4f (1, 1, 1, sp->opacity);
780         glNormal3f (0, 0, 1);
781         glBegin (GL_QUADS);
782         glTexCoord2f (texx1, texy2); glVertex3f (0, 0, 0);
783         glTexCoord2f (texx2, texy2); glVertex3f (1, 0, 0);
784         glTexCoord2f (texx2, texy1); glVertex3f (1, 1, 0);
785         glTexCoord2f (texx1, texy1); glVertex3f (0, 1, 0);
786         glEnd();
787
788         if (debug_p)            /* Draw a border around the image */
789           {
790             if (!wire) glDisable (GL_TEXTURE_2D);
791
792             if (sp->id & 1)
793               glColor4f (sp->opacity, 0, 0, 1);
794             else
795               glColor4f (0, 0, sp->opacity, 1);
796
797             glBegin (GL_LINE_LOOP);
798             glVertex3f (0, 0, 0);
799             glVertex3f (0, 1, 0);
800             glVertex3f (1, 1, 0);
801             glVertex3f (1, 0, 0);
802             glEnd();
803
804             if (!wire) glEnable (GL_TEXTURE_2D);
805           }
806       }
807
808
809     if (do_titles &&
810         img->title && *img->title)
811       {
812         int x = 10;
813         int y = mi->xgwa.height - 10;
814         glColor4f (0, 0, 0, sp->opacity);   /* cheap-assed dropshadow */
815         print_gl_string (mi->dpy, ss->xfont, ss->font_dlist,
816                          mi->xgwa.width, mi->xgwa.height, x, y,
817                          img->title, False);
818         x++; y++;
819         glColor4f (1, 1, 1, sp->opacity);
820         print_gl_string (mi->dpy, ss->xfont, ss->font_dlist,
821                          mi->xgwa.width, mi->xgwa.height, x, y,
822                          img->title, False);
823       }
824   }
825   glPopMatrix();
826
827   if (debug_p)
828     {
829       if (!wire) glDisable (GL_TEXTURE_2D);
830
831       if (sp->id & 1)
832         glColor4f (1, 0, 0, 1);
833       else
834         glColor4f (0, 0, 1, 1);
835
836       /* Draw the "from" and "to" boxes
837        */
838       glBegin (GL_LINE_LOOP);
839       glVertex3f (sp->from.x,              sp->from.y,              0);
840       glVertex3f (sp->from.x + sp->from.w, sp->from.y,              0);
841       glVertex3f (sp->from.x + sp->from.w, sp->from.y + sp->from.h, 0);
842       glVertex3f (sp->from.x,              sp->from.y + sp->from.h, 0);
843       glEnd();
844
845       glBegin (GL_LINE_LOOP);
846       glVertex3f (sp->to.x,                sp->to.y,                0);
847       glVertex3f (sp->to.x + sp->to.w,     sp->to.y,                0);
848       glVertex3f (sp->to.x + sp->to.w,     sp->to.y + sp->to.h,     0);
849       glVertex3f (sp->to.x,                sp->to.y + sp->to.h,     0);
850       glEnd();
851
852       if (!wire) glEnable (GL_TEXTURE_2D);
853     }
854 }
855
856
857 static void
858 tick_sprites (ModeInfo *mi)
859 {
860   slideshow_state *ss = &sss[MI_SCREEN(mi)];
861   int i;
862   for (i = 0; i < ss->nsprites; i++)
863       tick_sprite (mi, ss->sprites[i]);
864 }
865
866
867 static void
868 draw_sprites (ModeInfo *mi)
869 {
870   slideshow_state *ss = &sss[MI_SCREEN(mi)];
871   int i;
872
873   glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
874
875   glPushMatrix();
876   for (i = 0; i < ss->nsprites; i++)
877     draw_sprite (mi, ss->sprites[i]);
878   glPopMatrix();
879
880   if (debug_p)                          /* draw a white box (the "screen") */
881     {
882       int wire = MI_IS_WIREFRAME(mi);
883
884       if (!wire) glDisable (GL_TEXTURE_2D);
885
886       glColor4f (1, 1, 1, 1);
887       glBegin (GL_LINE_LOOP);
888       glVertex3f (0, 0, 0);
889       glVertex3f (0, 1, 0);
890       glVertex3f (1, 1, 0);
891       glVertex3f (1, 0, 0);
892       glEnd();
893
894       if (!wire) glEnable (GL_TEXTURE_2D);
895     }
896 }
897
898
899 ENTRYPOINT void
900 reshape_slideshow (ModeInfo *mi, int width, int height)
901 {
902   slideshow_state *ss = &sss[MI_SCREEN(mi)];
903   GLfloat s;
904   glViewport (0, 0, width, height);
905   glMatrixMode (GL_PROJECTION);
906   glLoadIdentity();
907   glMatrixMode (GL_MODELVIEW);
908   glLoadIdentity();
909
910   s = 2;
911
912   if (debug_p)
913     {
914       s *= (zoom / 100.0) * 0.75;
915       if (s < 0.1) s = 0.1;
916     }
917
918   glScalef (s, s, s);
919   glTranslatef (-0.5, -0.5, 0);
920
921   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
922
923   ss->redisplay_needed_p = True;
924 }
925
926
927 ENTRYPOINT Bool
928 slideshow_handle_event (ModeInfo *mi, XEvent *event)
929 {
930   slideshow_state *ss = &sss[MI_SCREEN(mi)];
931
932   if (event->xany.type == ButtonPress &&
933       event->xbutton.button == Button1)
934     {
935       ss->change_now_p = True;
936       return True;
937     }
938   else if (event->xany.type == KeyPress)
939     {
940       KeySym keysym;
941       char c = 0;
942       XLookupString (&event->xkey, &c, 1, &keysym, 0);
943       if (c == ' ' || c == '\r' || c == '\n' || c == '\t')
944         {
945           ss->change_now_p = True;
946           return True;
947         }
948     }
949   else if (event->xany.type == Expose ||
950            event->xany.type == GraphicsExpose ||
951            event->xany.type == VisibilityNotify)
952     {
953       ss->redisplay_needed_p = True;
954       if (debug_p)
955         fprintf (stderr, "%s: exposure\n", blurb());
956       return False;
957     }
958
959   return False;
960 }
961
962
963 /* Do some sanity checking on various user-supplied values, and make
964    sure they are all internally consistent.
965  */
966 static void
967 sanity_check (ModeInfo *mi)
968 {
969   if (zoom < 1) zoom = 1;           /* zoom is a positive percentage */
970   else if (zoom > 100) zoom = 100;
971
972   if (zoom == 100)                  /* with no zooming, there is no panning */
973     pan_seconds = 0;
974
975   if (pan_seconds < fade_seconds)   /* pan is inclusive of fade */
976     pan_seconds = fade_seconds;
977
978   if (pan_seconds == 0)             /* no zero-length cycles, please... */
979     pan_seconds = 1;
980
981   if (image_seconds < pan_seconds)  /* we only change images at fade-time */
982     image_seconds = pan_seconds;
983
984   /* If we're not panning/zooming within the image, then there's no point
985      in crossfading the image with itself -- only do crossfades when changing
986      to a new image. */
987   if (zoom == 100 && pan_seconds < image_seconds)
988     pan_seconds = image_seconds;
989
990   /* No need to use mipmaps if we're not changing the image size much */
991   if (zoom >= 80) mipmap_p = False;
992
993   if      (fps_cutoff < 0)  fps_cutoff = 0;
994   else if (fps_cutoff > 30) fps_cutoff = 30;
995 }
996
997
998 static void
999 check_fps (ModeInfo *mi)
1000 {
1001 #ifndef HAVE_COCOA  /* always assume Cocoa is fast enough */
1002
1003   slideshow_state *ss = &sss[MI_SCREEN(mi)];
1004
1005   double start_time, end_time, wall_elapsed, frame_duration, fps;
1006   int i;
1007
1008   start_time = ss->now;
1009   end_time = double_time();
1010   frame_duration = end_time - start_time;   /* time spent drawing this frame */
1011   ss->time_elapsed += frame_duration;       /* time spent drawing all frames */
1012   ss->frames_elapsed++;
1013
1014   wall_elapsed = end_time - ss->dawn_of_time;
1015   fps = ss->frames_elapsed / ss->time_elapsed;
1016   ss->theoretical_fps = fps;
1017
1018   if (ss->checked_fps_p) return;
1019
1020   if (wall_elapsed <= 8)    /* too early to be sure */
1021     return;
1022
1023   ss->checked_fps_p = True;
1024
1025   if (fps >= fps_cutoff)
1026     {
1027       if (debug_p)
1028         fprintf (stderr,
1029                  "%s: %.1f fps is fast enough (with %d frames in %.1f secs)\n",
1030                  blurb(), fps, ss->frames_elapsed, wall_elapsed);
1031       return;
1032     }
1033
1034   fprintf (stderr,
1035            "%s: only %.1f fps!  Turning off pan/fade to compensate...\n",
1036            blurb(), fps);
1037   zoom = 100;
1038   fade_seconds = 0;
1039
1040   sanity_check (mi);
1041
1042   for (i = 0; i < ss->nsprites; i++)
1043     {
1044       sprite *sp = ss->sprites[i];
1045       randomize_sprite (mi, sp);
1046       sp->state = FULL;
1047     }
1048
1049   ss->redisplay_needed_p = True;
1050
1051   /* Need this in case zoom changed. */
1052   reshape_slideshow (mi, mi->xgwa.width, mi->xgwa.height);
1053 #endif /* HAVE_COCOA */
1054 }
1055
1056
1057 /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
1058  */
1059 static void
1060 hack_resources (void)
1061 {
1062 #if 0
1063   char *res = "desktopGrabber";
1064   char *val = get_string_resource (res, "DesktopGrabber");
1065   char buf1[255];
1066   char buf2[255];
1067   XrmValue value;
1068   sprintf (buf1, "%.100s.%.100s", progclass, res);
1069   sprintf (buf2, "%.200s -v", val);
1070   value.addr = buf2;
1071   value.size = strlen(buf2);
1072   XrmPutResource (&db, buf1, "String", &value);
1073 #endif
1074 }
1075
1076
1077 ENTRYPOINT void
1078 init_slideshow (ModeInfo *mi)
1079 {
1080   int screen = MI_SCREEN(mi);
1081   slideshow_state *ss;
1082   int wire = MI_IS_WIREFRAME(mi);
1083   
1084   if (sss == NULL) {
1085     if ((sss = (slideshow_state *)
1086          calloc (MI_NUM_SCREENS(mi), sizeof(slideshow_state))) == NULL)
1087       return;
1088   }
1089   ss = &sss[screen];
1090
1091   if ((ss->glx_context = init_GL(mi)) != NULL) {
1092     reshape_slideshow (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
1093   } else {
1094     MI_CLEARWINDOW(mi);
1095   }
1096
1097   if (debug_p)
1098     fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
1099              blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
1100
1101   sanity_check(mi);
1102
1103   if (debug_p)
1104     fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n\n",
1105              blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
1106
1107   glDisable (GL_LIGHTING);
1108   glDisable (GL_DEPTH_TEST);
1109   glDepthMask (GL_FALSE);
1110   glEnable (GL_CULL_FACE);
1111   glCullFace (GL_BACK);
1112
1113   if (! wire)
1114     {
1115       glEnable (GL_TEXTURE_2D);
1116       glShadeModel (GL_SMOOTH);
1117       glEnable (GL_BLEND);
1118       glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1119     }
1120
1121   if (debug_p) glLineWidth (3);
1122
1123   load_font (mi->dpy, "titleFont", &ss->xfont, &ss->font_dlist);
1124
1125   if (debug_p)
1126     hack_resources();
1127
1128   ss->now = double_time();
1129   ss->dawn_of_time = ss->now;
1130   ss->prev_frame_time = ss->now;
1131
1132   ss->awaiting_first_image_p = True;
1133   alloc_image (mi);
1134 }
1135
1136
1137 ENTRYPOINT void
1138 draw_slideshow (ModeInfo *mi)
1139 {
1140   slideshow_state *ss = &sss[MI_SCREEN(mi)];
1141   int i;
1142
1143   if (!ss->glx_context)
1144     return;
1145
1146   glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
1147
1148   if (ss->awaiting_first_image_p)
1149     {
1150       image *img = ss->images[0];
1151       if (!img) abort();
1152       if (!img->loaded_p)
1153         return;
1154
1155       ss->awaiting_first_image_p = False;
1156       ss->dawn_of_time = double_time();
1157
1158       /* start the very first sprite fading in */
1159       new_sprite (mi);
1160     }
1161
1162   ss->now = double_time();
1163
1164   /* Each sprite has three states: fading in, full, fading out.
1165      The in/out states overlap like this:
1166
1167      iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . . . . . . . . . . . 
1168      . . . . . . . . . iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . .
1169      . . . . . . . . . . . . . . . . . . iiiiiiFFFFFFFFFFFFooooo
1170
1171      So as soon as a sprite goes into the "out" state, we create
1172      a new sprite (in the "in" state.)
1173    */
1174
1175   if (ss->nsprites > 2) abort();
1176
1177   /* If a sprite is just entering the fade-out state,
1178      then add a new sprite in the fade-in state.
1179    */
1180   for (i = 0; i < ss->nsprites; i++)
1181     {
1182       sprite *sp = ss->sprites[i];
1183       if (sp->state != sp->prev_state &&
1184           sp->state == (fade_seconds == 0 ? DEAD : OUT))
1185         new_sprite (mi);
1186     }
1187
1188   tick_sprites (mi);
1189
1190   /* Now garbage collect the dead sprites.
1191    */
1192   for (i = 0; i < ss->nsprites; i++)
1193     {
1194       sprite *sp = ss->sprites[i];
1195       if (sp->state == DEAD)
1196         {
1197           destroy_sprite (mi, sp);
1198           i--;
1199         }
1200     }
1201
1202   /* We can only ever end up with no sprites at all if the machine is
1203      being really slow and we hopped states directly from FULL to DEAD
1204      without passing OUT... */
1205   if (ss->nsprites == 0)
1206     new_sprite (mi);
1207
1208   if (!ss->redisplay_needed_p)
1209     return;
1210
1211   if (debug_p && ss->now - ss->prev_frame_time > 1)
1212     fprintf (stderr, "%s: static screen for %.1f secs\n",
1213              blurb(), ss->now - ss->prev_frame_time);
1214
1215   draw_sprites (mi);
1216
1217   ss->fps = fps_compute (mi->fpst, 0);
1218   if (mi->fps_p) do_fps (mi);
1219
1220   glFinish();
1221   glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
1222   ss->prev_frame_time = ss->now;
1223   ss->redisplay_needed_p = False;
1224   check_fps (mi);
1225 }
1226
1227 XSCREENSAVER_MODULE_2 ("GLSlideshow", glslideshow, slideshow)
1228
1229 #endif /* USE_GL */