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