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