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