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