From http://www.jwz.org/xscreensaver/xscreensaver-5.30.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 "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   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_COCOA
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       {
811         int x = 10;
812         int y = mi->xgwa.height - 10;
813         glColor4f (0, 0, 0, sp->opacity);   /* cheap-assed dropshadow */
814         print_gl_string (mi->dpy, ss->font_data,
815                          mi->xgwa.width, mi->xgwa.height, x, y,
816                          img->title, False);
817         x++; y++;
818         glColor4f (1, 1, 1, sp->opacity);
819         print_gl_string (mi->dpy, ss->font_data,
820                          mi->xgwa.width, mi->xgwa.height, x, y,
821                          img->title, False);
822       }
823   }
824   glPopMatrix();
825
826   if (debug_p)
827     {
828       if (!wire) glDisable (GL_TEXTURE_2D);
829
830       if (sp->id & 1)
831         glColor4f (1, 0, 0, 1);
832       else
833         glColor4f (0, 0, 1, 1);
834
835       /* Draw the "from" and "to" boxes
836        */
837       glBegin (GL_LINE_LOOP);
838       glVertex3f (sp->from.x,              sp->from.y,              0);
839       glVertex3f (sp->from.x + sp->from.w, sp->from.y,              0);
840       glVertex3f (sp->from.x + sp->from.w, sp->from.y + sp->from.h, 0);
841       glVertex3f (sp->from.x,              sp->from.y + sp->from.h, 0);
842       glEnd();
843
844       glBegin (GL_LINE_LOOP);
845       glVertex3f (sp->to.x,                sp->to.y,                0);
846       glVertex3f (sp->to.x + sp->to.w,     sp->to.y,                0);
847       glVertex3f (sp->to.x + sp->to.w,     sp->to.y + sp->to.h,     0);
848       glVertex3f (sp->to.x,                sp->to.y + sp->to.h,     0);
849       glEnd();
850
851       if (!wire) glEnable (GL_TEXTURE_2D);
852     }
853 }
854
855
856 static void
857 tick_sprites (ModeInfo *mi)
858 {
859   slideshow_state *ss = &sss[MI_SCREEN(mi)];
860   int i;
861   for (i = 0; i < ss->nsprites; i++)
862       tick_sprite (mi, ss->sprites[i]);
863 }
864
865
866 static void
867 draw_sprites (ModeInfo *mi)
868 {
869   slideshow_state *ss = &sss[MI_SCREEN(mi)];
870   int i;
871
872   glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
873
874   glPushMatrix();
875
876   {
877     GLfloat rot = current_device_rotation();
878     glTranslatef (0.5, 0.5, 0);
879     glRotatef(rot, 0, 0, 1);
880     if ((rot >  45 && rot <  135) ||
881         (rot < -45 && rot > -135))
882       {
883         GLfloat s = MI_WIDTH(mi) / (GLfloat) MI_HEIGHT(mi);
884         glScalef (s, 1/s, 1);
885       }
886     glTranslatef (-0.5, -0.5, 0);
887   }
888
889   for (i = 0; i < ss->nsprites; i++)
890     draw_sprite (mi, ss->sprites[i]);
891   glPopMatrix();
892
893   if (debug_p)                          /* draw a white box (the "screen") */
894     {
895       int wire = MI_IS_WIREFRAME(mi);
896
897       if (!wire) glDisable (GL_TEXTURE_2D);
898
899       glColor4f (1, 1, 1, 1);
900       glBegin (GL_LINE_LOOP);
901       glVertex3f (0, 0, 0);
902       glVertex3f (0, 1, 0);
903       glVertex3f (1, 1, 0);
904       glVertex3f (1, 0, 0);
905       glEnd();
906
907       if (!wire) glEnable (GL_TEXTURE_2D);
908     }
909 }
910
911
912 ENTRYPOINT void
913 reshape_slideshow (ModeInfo *mi, int width, int height)
914 {
915   slideshow_state *ss = &sss[MI_SCREEN(mi)];
916   GLfloat s;
917   glViewport (0, 0, width, height);
918   glMatrixMode (GL_PROJECTION);
919   glLoadIdentity();
920   glMatrixMode (GL_MODELVIEW);
921   glLoadIdentity();
922
923   s = 2;
924
925   if (debug_p)
926     {
927       s *= (zoom / 100.0) * 0.75;
928       if (s < 0.1) s = 0.1;
929     }
930
931   glScalef (s, s, s);
932   glTranslatef (-0.5, -0.5, 0);
933
934   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
935
936   ss->redisplay_needed_p = True;
937 }
938
939
940 ENTRYPOINT Bool
941 slideshow_handle_event (ModeInfo *mi, XEvent *event)
942 {
943   slideshow_state *ss = &sss[MI_SCREEN(mi)];
944
945   if (event->xany.type == Expose ||
946            event->xany.type == GraphicsExpose ||
947            event->xany.type == VisibilityNotify)
948     {
949       ss->redisplay_needed_p = True;
950       if (debug_p)
951         fprintf (stderr, "%s: exposure\n", blurb());
952       return False;
953     }
954   else if (screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event))
955     {
956       ss->change_now_p = True;
957       return True;
958     }
959
960   return False;
961 }
962
963
964 /* Do some sanity checking on various user-supplied values, and make
965    sure they are all internally consistent.
966  */
967 static void
968 sanity_check (ModeInfo *mi)
969 {
970   if (zoom < 1) zoom = 1;           /* zoom is a positive percentage */
971   else if (zoom > 100) zoom = 100;
972
973   if (zoom == 100)                  /* with no zooming, there is no panning */
974     pan_seconds = 0;
975
976   if (pan_seconds < fade_seconds)   /* pan is inclusive of fade */
977     pan_seconds = fade_seconds;
978
979   if (pan_seconds == 0)             /* no zero-length cycles, please... */
980     pan_seconds = 1;
981
982   if (image_seconds < pan_seconds)  /* we only change images at fade-time */
983     image_seconds = pan_seconds;
984
985   /* If we're not panning/zooming within the image, then there's no point
986      in crossfading the image with itself -- only do crossfades when changing
987      to a new image. */
988   if (zoom == 100 && pan_seconds < image_seconds)
989     pan_seconds = image_seconds;
990
991   /* No need to use mipmaps if we're not changing the image size much */
992   if (zoom >= 80) mipmap_p = False;
993
994   if      (fps_cutoff < 0)  fps_cutoff = 0;
995   else if (fps_cutoff > 30) fps_cutoff = 30;
996 }
997
998
999 static void
1000 check_fps (ModeInfo *mi)
1001 {
1002 #ifndef HAVE_COCOA  /* always assume Cocoa is fast enough */
1003
1004   slideshow_state *ss = &sss[MI_SCREEN(mi)];
1005
1006   double start_time, end_time, wall_elapsed, frame_duration, fps;
1007   int i;
1008
1009   start_time = ss->now;
1010   end_time = double_time();
1011   frame_duration = end_time - start_time;   /* time spent drawing this frame */
1012   ss->time_elapsed += frame_duration;       /* time spent drawing all frames */
1013   ss->frames_elapsed++;
1014
1015   wall_elapsed = end_time - ss->dawn_of_time;
1016   fps = ss->frames_elapsed / ss->time_elapsed;
1017   ss->theoretical_fps = fps;
1018
1019   if (ss->checked_fps_p) return;
1020
1021   if (wall_elapsed <= 8)    /* too early to be sure */
1022     return;
1023
1024   ss->checked_fps_p = True;
1025
1026   if (fps >= fps_cutoff)
1027     {
1028       if (debug_p)
1029         fprintf (stderr,
1030                  "%s: %.1f fps is fast enough (with %d frames in %.1f secs)\n",
1031                  blurb(), fps, ss->frames_elapsed, wall_elapsed);
1032       return;
1033     }
1034
1035   fprintf (stderr,
1036            "%s: only %.1f fps!  Turning off pan/fade to compensate...\n",
1037            blurb(), fps);
1038   zoom = 100;
1039   fade_seconds = 0;
1040
1041   sanity_check (mi);
1042
1043   for (i = 0; i < ss->nsprites; i++)
1044     {
1045       sprite *sp = ss->sprites[i];
1046       randomize_sprite (mi, sp);
1047       sp->state = FULL;
1048     }
1049
1050   ss->redisplay_needed_p = True;
1051
1052   /* Need this in case zoom changed. */
1053   reshape_slideshow (mi, mi->xgwa.width, mi->xgwa.height);
1054 #endif /* HAVE_COCOA */
1055 }
1056
1057
1058 /* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
1059  */
1060 static void
1061 hack_resources (void)
1062 {
1063 #if 0
1064   char *res = "desktopGrabber";
1065   char *val = get_string_resource (res, "DesktopGrabber");
1066   char buf1[255];
1067   char buf2[255];
1068   XrmValue value;
1069   sprintf (buf1, "%.100s.%.100s", progclass, res);
1070   sprintf (buf2, "%.200s -v", val);
1071   value.addr = buf2;
1072   value.size = strlen(buf2);
1073   XrmPutResource (&db, buf1, "String", &value);
1074 #endif
1075 }
1076
1077
1078 ENTRYPOINT void
1079 init_slideshow (ModeInfo *mi)
1080 {
1081   int screen = MI_SCREEN(mi);
1082   slideshow_state *ss;
1083   int wire = MI_IS_WIREFRAME(mi);
1084   
1085   if (sss == NULL) {
1086     if ((sss = (slideshow_state *)
1087          calloc (MI_NUM_SCREENS(mi), sizeof(slideshow_state))) == NULL)
1088       return;
1089   }
1090   ss = &sss[screen];
1091
1092   if ((ss->glx_context = init_GL(mi)) != NULL) {
1093     reshape_slideshow (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
1094   } else {
1095     MI_CLEARWINDOW(mi);
1096   }
1097
1098   if (debug_p)
1099     fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
1100              blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
1101
1102   sanity_check(mi);
1103
1104   if (debug_p)
1105     fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n\n",
1106              blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
1107
1108   glDisable (GL_LIGHTING);
1109   glDisable (GL_DEPTH_TEST);
1110   glDepthMask (GL_FALSE);
1111   glEnable (GL_CULL_FACE);
1112   glCullFace (GL_BACK);
1113
1114   if (! wire)
1115     {
1116       glEnable (GL_TEXTURE_2D);
1117       glShadeModel (GL_SMOOTH);
1118       glEnable (GL_BLEND);
1119       glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
1120     }
1121
1122   if (debug_p) glLineWidth (3);
1123
1124   ss->font_data = load_texture_font (mi->dpy, "titleFont");
1125
1126   if (debug_p)
1127     hack_resources();
1128
1129   ss->now = double_time();
1130   ss->dawn_of_time = ss->now;
1131   ss->prev_frame_time = ss->now;
1132
1133   ss->awaiting_first_image_p = True;
1134   alloc_image (mi);
1135 }
1136
1137
1138 ENTRYPOINT void
1139 draw_slideshow (ModeInfo *mi)
1140 {
1141   slideshow_state *ss = &sss[MI_SCREEN(mi)];
1142   int i;
1143
1144   if (!ss->glx_context)
1145     return;
1146
1147   glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
1148
1149   if (ss->awaiting_first_image_p)
1150     {
1151       image *img = ss->images[0];
1152       if (!img) abort();
1153       if (!img->loaded_p)
1154         return;
1155
1156       ss->awaiting_first_image_p = False;
1157       ss->dawn_of_time = double_time();
1158
1159       /* start the very first sprite fading in */
1160       new_sprite (mi);
1161     }
1162
1163   ss->now = double_time();
1164
1165   /* Each sprite has three states: fading in, full, fading out.
1166      The in/out states overlap like this:
1167
1168      iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . . . . . . . . . . . 
1169      . . . . . . . . . iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . .
1170      . . . . . . . . . . . . . . . . . . iiiiiiFFFFFFFFFFFFooooo
1171
1172      So as soon as a sprite goes into the "out" state, we create
1173      a new sprite (in the "in" state.)
1174    */
1175
1176   if (ss->nsprites > 2) abort();
1177
1178   /* If a sprite is just entering the fade-out state,
1179      then add a new sprite in the fade-in state.
1180    */
1181   for (i = 0; i < ss->nsprites; i++)
1182     {
1183       sprite *sp = ss->sprites[i];
1184       if (sp->state != sp->prev_state &&
1185           sp->state == (fade_seconds == 0 ? DEAD : OUT))
1186         new_sprite (mi);
1187     }
1188
1189   tick_sprites (mi);
1190
1191   /* Now garbage collect the dead sprites.
1192    */
1193   for (i = 0; i < ss->nsprites; i++)
1194     {
1195       sprite *sp = ss->sprites[i];
1196       if (sp->state == DEAD)
1197         {
1198           destroy_sprite (mi, sp);
1199           i--;
1200         }
1201     }
1202
1203   /* We can only ever end up with no sprites at all if the machine is
1204      being really slow and we hopped states directly from FULL to DEAD
1205      without passing OUT... */
1206   if (ss->nsprites == 0)
1207     new_sprite (mi);
1208
1209   if (!ss->redisplay_needed_p)
1210     return;
1211
1212   if (debug_p && ss->now - ss->prev_frame_time > 1)
1213     fprintf (stderr, "%s: static screen for %.1f secs\n",
1214              blurb(), ss->now - ss->prev_frame_time);
1215
1216   draw_sprites (mi);
1217
1218   if (mi->fps_p) do_fps (mi);
1219
1220   glFinish();
1221   glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
1222   ss->prev_frame_time = ss->now;
1223   ss->redisplay_needed_p = False;
1224   check_fps (mi);
1225 }
1226
1227 XSCREENSAVER_MODULE_2 ("GLSlideshow", glslideshow, slideshow)
1228
1229 #endif /* USE_GL */