http://www.tienza.es/crux/src/www.jwz.org/xscreensaver/xscreensaver-5.05.tar.gz
[xscreensaver] / hacks / glx / glslideshow.c
index e98864d4fe788e94aac9170a8396acc7444a9d22..e3108980609f7573ac3f0ab657aed18d6adee44b 100644 (file)
@@ -1,14 +1,10 @@
-/*
- * glslideshow - takes a snapshot of the screen and smoothly scans around
- *               in it
+/* glslideshow, Copyright (c) 2003-2008 Jamie Zawinski <jwz@jwz.org>
+ * Loads a sequence of images and smoothly pans around them; crossfades
+ * when loading new images.
  *
- * Copyright (c) 2002, 2003 Mike Oliphant (oliphant@gtk.org)
- *
- * Framework based on flipscreen3d
- *   Copyright (C) 2001 Ben Buxton (bb@cactii.net)
- *
- * Smooth transitions between multiple files added by
- * Jamie Zawinski <jwz@jwz.org>
+ * Originally written by Mike Oliphant <oliphant@gtk.org> (c) 2002, 2003.
+ * Rewritten by jwz, 21-Jun-2003.
+ * Rewritten by jwz again, 6-Feb-2005.
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
  * software for any purpose.  It is provided "as is" without express or
  * implied warranty.
  *
+ *****************************************************************************
+ *
+ * TODO:
+ *
+ * - When a new image is loaded, there is a glitch: animation pauses during
+ *   the period when we're loading the image-to-fade-in.  On fast (2GHz)
+ *   machines, this stutter is short but noticable (usually around 1/10th
+ *   second.)  On slower machines, it can be much more pronounced.
+ *   This turns out to be hard to fix...
+ *
+ *   Image loading happens in three stages:
+ *
+ *    1: Fork a process and run xscreensaver-getimage in the background.
+ *       This writes image data to a server-side X pixmap.
+ *
+ *    2: When that completes, a callback informs us that the pixmap is ready.
+ *       We must then download the pixmap data from the server with XGetImage
+ *       (or XShmGetImage.)
+ *
+ *    3: Once we have the bits, we must convert them from server-native bitmap
+ *       layout to 32 bit RGBA in client-endianness, to make them usable as
+ *       OpenGL textures.
+ *
+ *    4: We must actually construct a texture.
+ *
+ *   So, the speed of step 1 doesn't really matter, since that happens in
+ *   the background.  But steps 2, 3, and 4 happen in *this* process, and
+ *   cause the visible glitch.
+ *
+ *   Step 2 can't be moved to another process without opening a second
+ *   connection to the X server, which is pretty heavy-weight.  (That would
+ *   be possible, though; the other process could open an X connection,
+ *   retrieve the pixmap, and feed it back to us through a pipe or
+ *   something.)
+ *
+ *   Step 3 might be able to be optimized by coding tuned versions of
+ *   grab-ximage.c:copy_ximage() for the most common depths and bit orders.
+ *   (Or by moving it into the other process along with step 2.)
+ *
+ *   Step 4 is the hard one, though.  It might be possible to speed up this
+ *   step if there is some way to allow two GL processes share texture
+ *   data.  Unless, of course, all the time being consumed by step 4 is
+ *   because the graphics pipeline is flooded, in which case, that other
+ *   process would starve the screen anyway.
+ *
+ *   Is it possible to use a single GLX context in a multithreaded way?
+ *   Or use a second GLX context, but allow the two contexts to share data?
+ *   I can't find any documentation about this.
+ *
+ *   How does Apple do this with their MacOSX slideshow screen saver?
+ *   Perhaps it's easier for them because their OpenGL libraries have
+ *   thread support at a lower level?
  */
 
-#include <X11/Intrinsic.h>
-
-
-# define PROGCLASS "GLSlideshow"
-# define HACK_INIT init_slideshow
-# define HACK_DRAW draw_slideshow
-# define HACK_RESHAPE reshape_slideshow
-# define slideshow_opts xlockmore_opts
-
-# define DEF_FADE     "True"
-# define DEF_DURATION "30"
-# define DEF_ZOOM     "75"
-
-#define DEFAULTS  "*delay:       20000          \n" \
-                  "*fade:       " DEF_FADE     "\n" \
-                  "*duration:   " DEF_DURATION "\n" \
-                  "*zoom:       " DEF_ZOOM     "\n" \
-                 "*wireframe:   False          \n" \
-                  "*showFPS:     False          \n" \
-                 "*fpsSolid:    True           \n" \
-                  "*desktopGrabber: xscreensaver-getimage -no-desktop %s\n"
-
+#define DEFAULTS  "*delay:           20000                \n" \
+                 "*wireframe:       False                \n" \
+                  "*showFPS:         False                \n" \
+                 "*fpsSolid:        True                 \n" \
+                 "*useSHM:          True                 \n" \
+                 "*titleFont:       -*-times-bold-r-normal-*-180-*\n" \
+                  "*desktopGrabber:  xscreensaver-getimage -no-desktop %s\n" \
+                 "*grabDesktopImages:   False \n" \
+                 "*chooseRandomImages:  True  \n"
+
+# define refresh_slideshow 0
+# define release_slideshow 0
 # include "xlockmore.h"
 
-#define RRAND(range) (random()%(range))
 #undef countof
 #define countof(x) (sizeof((x))/sizeof((*x)))
 
 #ifdef USE_GL
 
-#include <GL/glu.h>
-#include <math.h>
-#include <sys/time.h>
-#include <stdio.h>
-#include <stdlib.h>
+
+# define DEF_FADE_DURATION  "2"
+# define DEF_PAN_DURATION   "6"
+# define DEF_IMAGE_DURATION "30"
+# define DEF_ZOOM           "75"
+# define DEF_FPS_CUTOFF     "5"
+# define DEF_TITLES         "False"
+# define DEF_LETTERBOX      "True"
+# define DEF_DEBUG          "False"
+# define DEF_MIPMAP         "True"
+
 #include "grab-ximage.h"
+#include "glxfonts.h"
 
+typedef struct {
+  double x, y, w, h;
+} rect;
 
-#define QW 12.4   /* arbitrary size of the textured quads we render */
-#define QH 12.4
-#define QX -6.2
-#define QY  6.2
+typedef struct {
+  ModeInfo *mi;
+  int id;                         /* unique number for debugging */
+  char *title;                    /* the filename of this image */
+  int w, h;                       /* size in pixels of the image */
+  int tw, th;                     /* size in pixels of the texture */
+  XRectangle geom;                /* where in the image the bits are */
+  Bool loaded_p;                  /* whether the image has finished loading */
+  Bool used_p;                    /* whether the image has yet appeared
+                                      on screen */
+  GLuint texid;                           /* which texture contains the image */
+  int refcount;                           /* how many sprites refer to this image */
+} image;
+
+
+typedef enum { NEW, IN, FULL, OUT, DEAD } sprite_state;
+
+typedef struct {
+  int id;                         /* unique number for debugging */
+  image *img;                     /* which image this animation displays */
+  GLfloat opacity;                /* how to render it */
+  double start_time;              /* when this animation began */
+  rect from, to, current;         /* the journey this image is taking */
+  sprite_state state;             /* the state we're in right now */
+  sprite_state prev_state;        /* the state we were in previously */
+  double state_time;              /* time of last state change */
+  int frame_count;                /* frames since last state change */
+} sprite;
 
-#define NQUADS 2  /* sometimes we draw 2 at once */
 
 typedef struct {
   GLXContext *glx_context;
-  Window window;
+  int nimages;                 /* how many images are loaded or loading now */
+  image *images[10];           /* pointers to the images */
+
+  int nsprites;                        /* how many sprites are animating right now */
+  sprite *sprites[10];         /* pointers to the live sprites */
+
+  double now;                  /* current time in seconds */
+  double dawn_of_time;         /* when the program launched */
+  double image_load_time;      /* time when we last loaded a new image */
+  double prev_frame_time;      /* time when we last drew a frame */
 
-  int tw, th;                  /* texture width, height */
-  GLfloat max_tx, max_ty;
+  Bool awaiting_first_image_p;  /* Early in startup: nothing to display yet */
+  Bool redisplay_needed_p;     /* Sometimes we can get away with not
+                                   re-painting.  Tick this if a redisplay
+                                   is required. */
+  Bool change_now_p;           /* Set when the user clicks to ask for a new
+                                   image right now. */
 
-  GLfloat qw, qh;              /* q? are for the quad we'll draw */
-  GLfloat qx[NQUADS], qy[NQUADS], qz[NQUADS];
-  GLfloat dx[NQUADS], dy[NQUADS], dz[NQUADS];
+  GLfloat fps;                  /* approximate frame rate we're achieving */
+  GLfloat theoretical_fps;      /* maximum frame rate that might be possible */
+  Bool checked_fps_p;          /* Whether we have checked for a low
+                                   frame rate. */
 
-  GLuint texids[NQUADS];       /* two textures: current img, incoming img */
+  XFontStruct *xfont;          /* for printing image file names */
+  GLuint font_dlist;
 
-  time_t start_time;           /* when we started displaying this image */
+  int sprite_id, image_id;      /* debugging id counters */
 
-  int curr_screen;
-  int curr_texid_index;
-  int in_transition;           /* true while we're drawing overlapping imgs */
-  int in_file_transition;      /* ...plus loading a new image */
-  int frames;                  /* how many frames we've drawn in this pan */
+  double time_elapsed;
+  int frames_elapsed;
 
 } slideshow_state;
 
@@ -93,363 +175,1051 @@ static slideshow_state *sss = NULL;
 
 /* Command-line arguments
  */
-int fade;     /* If true, transitions between pans (and between images) will
-                 be translucent; otherwise, they will be jump-cuts. */
-int duration; /* how many seconds until loading a new image */
-int zoom;     /* how far in to zoom when panning, in percent of image size:
-                 that is, 75 means "when zoomed all the way in, 75% of the
-                 image will be on screen."  */
-
-
-/* blah, apparently other magic numbers elsewhere in the file also
-   affect this...   can't just change these to speed up / slow down...
- */
-static int frames_per_pan  = 300;
-static int frames_per_fade = 100;
+static int fade_seconds;    /* Duration in seconds of fade transitions.
+                               If 0, jump-cut instead of fading. */
+static int pan_seconds;     /* Duration of each pan through an image. */
+static int image_seconds;   /* How many seconds until loading a new image. */
+static int zoom;            /* How far in to zoom when panning, in percent of
+                               image size: that is, 75 means "when zoomed all
+                               the way in, 75% of the image will be visible."
+                             */
+static int fps_cutoff;      /* If the frame-rate falls below this, turn off
+                               zooming.*/
+static Bool letterbox_p;    /* When a loaded image is not the same aspect
+                               ratio as the window, whether to display black
+                               bars.
+                             */
+static Bool mipmap_p;      /* Use mipmaps instead of single textures. */
+static Bool do_titles;     /* Display image titles. */
+static Bool debug_p;       /* Be loud and do weird things. */
 
 
 static XrmOptionDescRec opts[] = {
-  {"+fade",     ".slideshow.fade",     XrmoptionNoArg, (caddr_t) "False" },
-  {"-fade",     ".slideshow.fade",     XrmoptionNoArg, (caddr_t) "True" },
-  {"-duration", ".slideshow.duration", XrmoptionSepArg, 0},
-  {"-zoom",     ".slideshow.zoom",     XrmoptionSepArg, 0}
+  {"-fade",         ".fadeDuration",  XrmoptionSepArg, 0      },
+  {"-pan",          ".panDuration",   XrmoptionSepArg, 0      },
+  {"-duration",     ".imageDuration", XrmoptionSepArg, 0      },
+  {"-zoom",         ".zoom",          XrmoptionSepArg, 0      },
+  {"-cutoff",       ".FPScutoff",     XrmoptionSepArg, 0      },
+  {"-titles",       ".titles",        XrmoptionNoArg, "True"  },
+  {"-letterbox",    ".letterbox",     XrmoptionNoArg, "True"  },
+  {"-no-letterbox", ".letterbox",     XrmoptionNoArg, "False" },
+  {"-clip",         ".letterbox",     XrmoptionNoArg, "False" },
+  {"-mipmaps",      ".mipmap",        XrmoptionNoArg, "True"  },
+  {"-no-mipmaps",   ".mipmap",        XrmoptionNoArg, "False" },
+  {"-debug",        ".debug",         XrmoptionNoArg, "True"  },
 };
 
 static argtype vars[] = {
-  {(caddr_t *) &fade,     "fade",     "Fade",     DEF_FADE,     t_Bool},
-  {(caddr_t *) &duration, "duration", "Duration", DEF_DURATION, t_Int},
-  {(caddr_t *) &zoom,     "zoom",     "Zoom",     DEF_ZOOM,     t_Int}
+  { &fade_seconds,  "fadeDuration", "FadeDuration", DEF_FADE_DURATION,  t_Int},
+  { &pan_seconds,   "panDuration",  "PanDuration",  DEF_PAN_DURATION,   t_Int},
+  { &image_seconds, "imageDuration","ImageDuration",DEF_IMAGE_DURATION, t_Int},
+  { &zoom,          "zoom",         "Zoom",         DEF_ZOOM,           t_Int},
+  { &mipmap_p,      "mipmap",       "Mipmap",       DEF_MIPMAP,        t_Bool},
+  { &letterbox_p,   "letterbox",    "Letterbox",    DEF_LETTERBOX,     t_Bool},
+  { &fps_cutoff,    "FPScutoff",    "FPSCutoff",    DEF_FPS_CUTOFF,     t_Int},
+  { &debug_p,       "debug",        "Debug",        DEF_DEBUG,         t_Bool},
+  { &do_titles,     "titles",       "Titles",       DEF_TITLES,        t_Bool},
 };
 
-ModeSpecOpt slideshow_opts = {countof(opts), opts, countof(vars), vars, NULL};
+ENTRYPOINT ModeSpecOpt slideshow_opts = {countof(opts), opts, countof(vars), vars, NULL};
 
 
-/* draw the texture mapped quad.
-   `screen' specifies which of the independently-moving images to draw.
+static const char *
+blurb (void)
+{
+# ifdef HAVE_COCOA
+  return "GLSlideshow";
+# else
+  static char buf[255];
+  time_t now = time ((time_t *) 0);
+  char *ct = (char *) ctime (&now);
+  int n = strlen(progname);
+  if (n > 100) n = 99;
+  strncpy(buf, progname, n);
+  buf[n++] = ':';
+  buf[n++] = ' ';
+  strncpy(buf+n, ct+11, 8);
+  strcpy(buf+n+9, ": ");
+  return buf;
+# endif
+}
+
+
+/* Returns the current time in seconds as a double.
+ */
+static double
+double_time (void)
+{
+  struct timeval now;
+# ifdef GETTIMEOFDAY_TWO_ARGS
+  struct timezone tzp;
+  gettimeofday(&now, &tzp);
+# else
+  gettimeofday(&now);
+# endif
+
+  return (now.tv_sec + ((double) now.tv_usec * 0.000001));
+}
+
+
+static void image_loaded_cb (const char *filename, XRectangle *geom,
+                             int image_width, int image_height,
+                             int texture_width, int texture_height,
+                             void *closure);
+
+
+/* Allocate an image structure and start a file loading in the background.
+ */
+static image *
+alloc_image (ModeInfo *mi)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  image *img = (image *) calloc (1, sizeof (*img));
+
+  img->id = ++ss->image_id;
+  img->loaded_p = False;
+  img->used_p = False;
+  img->mi = mi;
+
+  glGenTextures (1, &img->texid);
+  if (img->texid <= 0) abort();
+
+  ss->image_load_time = ss->now;
+
+  if (wire)
+    image_loaded_cb (0, 0, 0, 0, 0, 0, img);
+  else
+    load_texture_async (mi->xgwa.screen, mi->window, *ss->glx_context,
+                        0, 0, mipmap_p, img->texid, image_loaded_cb, img);
+
+  ss->images[ss->nimages++] = img;
+  if (ss->nimages >= countof(ss->images)) abort();
+
+  return img;
+}
+
+
+/* Callback that tells us that the texture has been loaded.
  */
 static void
-showscreen (ModeInfo *mi, int wire, int screen, int texid_index)
+image_loaded_cb (const char *filename, XRectangle *geom,
+                 int image_width, int image_height,
+                 int texture_width, int texture_height,
+                 void *closure)
+{
+  image *img = (image *) closure;
+  ModeInfo *mi = img->mi;
+  /* slideshow_state *ss = &sss[MI_SCREEN(mi)]; */
+
+  int wire = MI_IS_WIREFRAME(mi);
+
+  if (wire)
+    {
+      img->w = MI_WIDTH (mi) * (0.5 + frand (1.0));
+      img->h = MI_HEIGHT (mi);
+      img->geom.width  = img->w;
+      img->geom.height = img->h;
+      goto DONE;
+    }
+
+  if (image_width == 0 || image_height == 0)
+    exit (1);
+
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
+                   mipmap_p ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
+
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+
+  img->w  = image_width;
+  img->h  = image_height;
+  img->tw = texture_width;
+  img->th = texture_height;
+  img->geom = *geom;
+  img->title = (filename ? strdup (filename) : 0);
+
+  /* If the image's width doesn't come back as the width of the screen,
+     then the image must have been scaled down (due to insufficient
+     texture memory.)  Scale up the coordinates to stretch the image
+     to fill the window.
+   */
+  if (img->w != MI_WIDTH(mi))
+    {
+      double scale = (double) MI_WIDTH(mi) / img->w;
+      img->w  *= scale;
+      img->h  *= scale;
+      img->tw *= scale;
+      img->th *= scale;
+      img->geom.x      *= scale;
+      img->geom.y      *= scale;
+      img->geom.width  *= scale;
+      img->geom.height *= scale;
+    }
+
+  if (img->title)   /* strip filename to part after last /. */
+    {
+      char *s = strrchr (img->title, '/');
+      if (s) strcpy (img->title, s+1);
+    }
+
+  if (debug_p)
+    fprintf (stderr, "%s: loaded   img %2d: \"%s\"\n",
+             blurb(), img->id, (img->title ? img->title : "(null)"));
+ DONE:
+
+  img->loaded_p = True;
+}
+
+
+
+/* Free the image and texture, after nobody is referencing it.
+ */
+static void
+destroy_image (ModeInfo *mi, image *img)
 {
   slideshow_state *ss = &sss[MI_SCREEN(mi)];
-  static GLfloat r = 1, g = 1, b = 1, a = 1;
-  GLfloat qxw, qyh;
-  GLfloat x, y, w, h;
+  Bool freed_p = False;
+  int i;
+
+  if (!img) abort();
+  if (!img->loaded_p) abort();
+  if (!img->used_p) abort();
+  if (img->texid <= 0) abort();
+  if (img->refcount != 0) abort();
+
+  for (i = 0; i < ss->nimages; i++)            /* unlink it from the list */
+    if (ss->images[i] == img)
+      {
+        int j;
+        for (j = i; j < ss->nimages-1; j++)    /* pull remainder forward */
+          ss->images[j] = ss->images[j+1];
+        ss->images[j] = 0;
+        ss->nimages--;
+        freed_p = True;
+        break;
+      }
 
-  if (screen >= NQUADS) abort();
-  qxw = ss->qx[screen] + ss->qw;
-  qyh = ss->qy[screen] - ss->qh;
-  x = ss->qx[screen];
-  y = ss->qy[screen];
-  w = qxw;
-  h = qyh;
+  if (!freed_p) abort();
 
-  ss->qx[screen] += ss->dx[screen];
-  ss->qy[screen] -= ss->dy[screen];
-  ss->qz[screen] += ss->dz[screen];
+  if (debug_p)
+    fprintf (stderr, "%s: unloaded img %2d: \"%s\"\n",
+             blurb(), img->id, (img->title ? img->title : "(null)"));
 
-  glTranslatef(0, 0, ss->qz[screen]);
+  if (img->title) free (img->title);
+  glDeleteTextures (1, &img->texid);
+  free (img);
+}
 
-  if (ss->in_transition) {
-    a = 1 - (ss->frames/100.0);
 
-    if (screen != ss->curr_screen) {
-      a = 1-a;
+/* Return an image to use for a sprite.
+   If it's time for a new one, get a new one.
+   Otherwise, use an old one.
+   Might return 0 if the machine is really slow.
+ */
+static image *
+get_image (ModeInfo *mi)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  image *img = 0;
+  double now = ss->now;
+  Bool want_new_p = (ss->change_now_p ||
+                     ss->image_load_time + image_seconds <= now);
+  image *new_img = 0;
+  image *old_img = 0;
+  image *loading_img = 0;
+  int i;
+
+  for (i = 0; i < ss->nimages; i++)
+    {
+      image *img2 = ss->images[i];
+      if (!img2) abort();
+      if (!img2->loaded_p)
+        loading_img = img2;
+      else if (!img2->used_p)
+        new_img = img2;
+      else
+        old_img = img2;
     }
-  }
-  else {
-    a = 1;
-  }
 
-  glColor4f(r, g, b, a);
+  if (want_new_p && new_img)
+    img = new_img, new_img = 0, ss->change_now_p = False;
+  else if (old_img)
+    img = old_img, old_img = 0;
+  else if (new_img)
+    img = new_img, new_img = 0, ss->change_now_p = False;
 
-  if(!wire) {
-    glEnable(GL_TEXTURE_2D);
-    glEnable(GL_BLEND);
-    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-    glDepthMask(GL_FALSE);
-    glBindTexture (GL_TEXTURE_2D, ss->texids[texid_index]);
-  }
+  /* Make sure that there is always one unused image in the pipe.
+   */
+  if (!new_img && !loading_img)
+    alloc_image (mi);
 
-  glBegin(wire ? GL_LINE_LOOP : GL_QUADS);
+  return img;
+}
 
-  glNormal3f(0, 0, 1);
 
-  glTexCoord2f(0, ss->max_ty);
-  glVertex3f(x, y, 0);
+/* Pick random starting and ending positions for the given sprite.
+ */
+static void
+randomize_sprite (ModeInfo *mi, sprite *sp)
+{
+  int vp_w = MI_WIDTH(mi);
+  int vp_h = MI_HEIGHT(mi);
+  int img_w = sp->img->geom.width;
+  int img_h = sp->img->geom.height;
+  int min_w, min_h, max_w, max_h;
+  double ratio = (double) img_h / img_w;
+
+  if (letterbox_p)
+    {
+      min_w = img_w;
+      min_h = img_h;
+    }
+  else
+    {
+      if (img_w < vp_w)
+        {
+          min_w = vp_w;
+          min_h = img_h * (float) vp_w / img_w;
+        }
+      else
+        {
+          min_w = img_w * (float) vp_h / img_h;
+          min_h = vp_h;
+        }
+    }
 
-  glTexCoord2f(ss->max_tx, ss->max_ty);
-  glVertex3f(w, y, 0);
+  max_w = min_w * 100 / zoom;
+  max_h = min_h * 100 / zoom;
+
+  sp->from.w = min_w + frand ((max_w - min_w) * 0.4);
+  sp->to.w   = max_w - frand ((max_w - min_w) * 0.4);
+  sp->from.h = sp->from.w * ratio;
+  sp->to.h   = sp->to.w   * ratio;
+
+  if (zoom == 100)     /* only one box, and it is centered */
+    {
+      sp->from.x = (sp->from.w > vp_w
+                    ? -(sp->from.w - vp_w) / 2
+                    :  (vp_w - sp->from.w) / 2);
+      sp->from.y = (sp->from.h > vp_h
+                    ? -(sp->from.h - vp_h) / 2
+                    :  (vp_h - sp->from.h) / 2);
+      sp->to = sp->from;
+    }
+  else                 /* position both boxes randomly */
+    {
+      sp->from.x = (sp->from.w > vp_w
+                    ? -frand (sp->from.w - vp_w)
+                    :  frand (vp_w - sp->from.w));
+      sp->from.y = (sp->from.h > vp_h
+                    ? -frand (sp->from.h - vp_h)
+                    :  frand (vp_h - sp->from.h));
+      sp->to.x   = (sp->to.w > vp_w
+                    ? -frand (sp->to.w - vp_w)
+                    :  frand (vp_w - sp->to.w));
+      sp->to.y   = (sp->to.h > vp_h
+                    ? -frand (sp->to.h - vp_h)
+                    :  frand (vp_h - sp->to.h));
+    }
 
-  glTexCoord2f(ss->max_tx, 0);
-  glVertex3f(w, h, 0);
+  if (random() & 1)
+    {
+      rect swap = sp->to;
+      sp->to = sp->from;
+      sp->from = swap;
+    }
 
-  glTexCoord2f(0, 0);
-  glVertex3f(x, h, 0);
+  /* Make sure the aspect ratios are within 0.0001 of each other.
+   */
+  if ((int) (0.5 + (sp->from.w * 1000 / sp->from.h)) !=
+      (int) (0.5 + (sp->to.w   * 1000 / sp->to.h)))
+    {
+      fprintf (stderr, "%s: botched aspect: %f x %f vs  %f x %f: %s\n",
+               progname, sp->from.w, sp->from.h, sp->to.w, sp->to.h,
+               sp->img->title);
+      abort();
+    }
 
-  glEnd();
+  sp->from.x /= vp_w;
+  sp->from.y /= vp_h;
+  sp->from.w /= vp_w;
+  sp->from.h /= vp_h;
+  sp->to.x   /= vp_w;
+  sp->to.y   /= vp_h;
+  sp->to.w   /= vp_w;
+  sp->to.h   /= vp_h;
+}
+
+
+/* Allocate a new sprite and start its animation going.
+ */
+static sprite *
+new_sprite (ModeInfo *mi)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  image *img = get_image (mi);
+  sprite *sp;
+
+  if (!img)
+    {
+      /* Oops, no images yet!  The machine is probably hurting bad.
+         Let's give it some time before thrashing again. */
+      usleep (250000);
+      return 0;
+    }
+
+  sp = (sprite *) calloc (1, sizeof (*sp));
+  sp->id = ++ss->sprite_id;
+  sp->start_time = ss->now;
+  sp->state_time = sp->start_time;
+  sp->state = sp->prev_state = NEW;
+  sp->img = img;
 
-  if (wire) {
-    GLfloat i;
-    int k = 10;
-    glBegin(GL_LINES);
-    for (i = x; i < w; i += ((w-x)/k))
+  sp->img->refcount++;
+  sp->img->used_p = True;
+
+  ss->sprites[ss->nsprites++] = sp;
+  if (ss->nsprites >= countof(ss->sprites)) abort();
+
+  randomize_sprite (mi, sp);
+
+  return sp;
+}
+
+
+/* Free the given sprite, and decrement the reference count on its image.
+ */
+static void
+destroy_sprite (ModeInfo *mi, sprite *sp)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  Bool freed_p = False;
+  image *img;
+  int i;
+
+  if (!sp) abort();
+  if (sp->state != DEAD) abort();
+  img = sp->img;
+  if (!img) abort();
+  if (!img->loaded_p) abort();
+  if (!img->used_p) abort();
+  if (img->refcount <= 0) abort();
+
+  for (i = 0; i < ss->nsprites; i++)           /* unlink it from the list */
+    if (ss->sprites[i] == sp)
       {
-        glVertex3f(i, y, 0);
-        glVertex3f(i, h, 0);
+        int j;
+        for (j = i; j < ss->nsprites-1; j++)   /* pull remainder forward */
+          ss->sprites[j] = ss->sprites[j+1];
+        ss->sprites[j] = 0;
+        ss->nsprites--;
+        freed_p = True;
+        break;
       }
-    for (i = y; i >= h; i -= ((y-h)/k))
+
+  if (!freed_p) abort();
+  free (sp);
+  sp = 0;
+
+  img->refcount--;
+  if (img->refcount < 0) abort();
+  if (img->refcount == 0)
+    destroy_image (mi, img);
+}
+
+
+/* Updates the sprite for the current frame of the animation based on
+   its creation time compared to the current wall clock.
+ */
+static void
+tick_sprite (ModeInfo *mi, sprite *sp)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  image *img = sp->img;
+  double now = ss->now;
+  double secs;
+  double ratio;
+  rect prev_rect = sp->current;
+  GLfloat prev_opacity = sp->opacity;
+
+  if (! sp->img) abort();
+  if (! img->loaded_p) abort();
+
+  secs = now - sp->start_time;
+  ratio = secs / (pan_seconds + fade_seconds);
+  if (ratio > 1) ratio = 1;
+
+  sp->current.x = sp->from.x + ratio * (sp->to.x - sp->from.x);
+  sp->current.y = sp->from.y + ratio * (sp->to.y - sp->from.y);
+  sp->current.w = sp->from.w + ratio * (sp->to.w - sp->from.w);
+  sp->current.h = sp->from.h + ratio * (sp->to.h - sp->from.h);
+
+  sp->prev_state = sp->state;
+
+  if (secs < fade_seconds)
+    {
+      sp->state = IN;
+      sp->opacity = secs / (GLfloat) fade_seconds;
+    }
+  else if (secs < pan_seconds)
+    {
+      sp->state = FULL;
+      sp->opacity = 1;
+    }
+  else if (secs < pan_seconds + fade_seconds)
+    {
+      sp->state = OUT;
+      sp->opacity = 1 - ((secs - pan_seconds) / (GLfloat) fade_seconds);
+    }
+  else
+    {
+      sp->state = DEAD;
+      sp->opacity = 0;
+    }
+
+  if (sp->state != sp->prev_state &&
+      (sp->prev_state == IN ||
+       sp->prev_state == FULL))
+    {
+      double secs = now - sp->state_time;
+
+      if (debug_p)
+        fprintf (stderr,
+                 "%s: %s %3d frames %2.0f sec %5.1f fps (%.1f fps?)\n",
+                 blurb(),
+                 (sp->prev_state == IN ? "fade" : "pan "),
+                 sp->frame_count,
+                 secs,
+                 sp->frame_count / secs,
+                 ss->theoretical_fps);
+
+      sp->state_time = now;
+      sp->frame_count = 0;
+    }
+
+  sp->frame_count++;
+
+  if (sp->state != DEAD &&
+      (prev_rect.x != sp->current.x ||
+       prev_rect.y != sp->current.y ||
+       prev_rect.w != sp->current.w ||
+       prev_rect.h != sp->current.h ||
+       prev_opacity != sp->opacity))
+    ss->redisplay_needed_p = True;
+}
+
+
+/* Draw the given sprite at the phase of its animation dictated by
+   its creation time compared to the current wall clock.
+ */
+static void
+draw_sprite (ModeInfo *mi, sprite *sp)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  image *img = sp->img;
+
+  if (! sp->img) abort();
+  if (! img->loaded_p) abort();
+
+  glPushMatrix();
+  {
+    glTranslatef (sp->current.x, sp->current.y, 0);
+    glScalef (sp->current.w, sp->current.h, 1);
+
+    if (wire)                  /* Draw a grid inside the box */
+      {
+        GLfloat dy = 0.1;
+        GLfloat dx = dy * img->w / img->h;
+        GLfloat x, y;
+
+        if (sp->id & 1)
+          glColor4f (sp->opacity, 0, 0, 1);
+        else
+          glColor4f (0, 0, sp->opacity, 1);
+
+        glBegin(GL_LINES);
+        glVertex3f (0, 0, 0); glVertex3f (1, 1, 0);
+        glVertex3f (1, 0, 0); glVertex3f (0, 1, 0);
+
+        for (y = 0; y < 1+dy; y += dy)
+          {
+            GLfloat yy = (y > 1 ? 1 : y);
+            for (x = 0.5; x < 1+dx; x += dx)
+              {
+                GLfloat xx = (x > 1 ? 1 : x);
+                glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
+                glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
+              }
+            for (x = 0.5; x > -dx; x -= dx)
+              {
+                GLfloat xx = (x < 0 ? 0 : x);
+                glVertex3f (0, xx, 0); glVertex3f (1, xx, 0);
+                glVertex3f (yy, 0, 0); glVertex3f (yy, 1, 0);
+              }
+          }
+        glEnd();
+      }
+    else                       /* Draw the texture quad */
+      {
+        GLfloat texw  = img->geom.width  / (GLfloat) img->tw;
+        GLfloat texh  = img->geom.height / (GLfloat) img->th;
+        GLfloat texx1 = img->geom.x / (GLfloat) img->tw;
+        GLfloat texy1 = img->geom.y / (GLfloat) img->th;
+        GLfloat texx2 = texx1 + texw;
+        GLfloat texy2 = texy1 + texh;
+
+        glBindTexture (GL_TEXTURE_2D, img->texid);
+        glColor4f (1, 1, 1, sp->opacity);
+        glNormal3f (0, 0, 1);
+        glBegin (GL_QUADS);
+        glTexCoord2f (texx1, texy2); glVertex3f (0, 0, 0);
+        glTexCoord2f (texx2, texy2); glVertex3f (1, 0, 0);
+        glTexCoord2f (texx2, texy1); glVertex3f (1, 1, 0);
+        glTexCoord2f (texx1, texy1); glVertex3f (0, 1, 0);
+        glEnd();
+
+        if (debug_p)           /* Draw a border around the image */
+          {
+            if (!wire) glDisable (GL_TEXTURE_2D);
+
+            if (sp->id & 1)
+              glColor4f (sp->opacity, 0, 0, 1);
+            else
+              glColor4f (0, 0, sp->opacity, 1);
+
+            glBegin (GL_LINE_LOOP);
+            glVertex3f (0, 0, 0);
+            glVertex3f (0, 1, 0);
+            glVertex3f (1, 1, 0);
+            glVertex3f (1, 0, 0);
+            glEnd();
+
+            if (!wire) glEnable (GL_TEXTURE_2D);
+          }
+      }
+
+
+    if (do_titles &&
+        img->title && *img->title)
       {
-        glVertex3f(x, i, 0);
-        glVertex3f(w, i, 0);
+        int x = 10;
+        int y = mi->xgwa.height - 10;
+        glColor4f (0, 0, 0, sp->opacity);   /* cheap-assed dropshadow */
+        print_gl_string (mi->dpy, ss->xfont, ss->font_dlist,
+                         mi->xgwa.width, mi->xgwa.height, x, y,
+                         img->title);
+        x++; y++;
+        glColor4f (1, 1, 1, sp->opacity);
+        print_gl_string (mi->dpy, ss->xfont, ss->font_dlist,
+                         mi->xgwa.width, mi->xgwa.height, x, y,
+                         img->title);
       }
-    glVertex3f(x, y, 0);
-    glVertex3f(w, h, 0);
-    glVertex3f(x, h, 0);
-    glVertex3f(w, y, 0);
-    glEnd();
   }
+  glPopMatrix();
 
-  glDisable(GL_TEXTURE_2D);
-  glDepthMask(GL_TRUE);
-  
-  glBegin(GL_LINE_LOOP);
-  glVertex3f(x, y, 0);
-  glVertex3f(x, h, 0);
-  glVertex3f(w, h, 0);
-  glVertex3f(w, y, 0);
-  glEnd();
-  glDisable(GL_BLEND);
-
-  glTranslatef(0, 0, -ss->qz[screen]);
+  if (debug_p)
+    {
+      if (!wire) glDisable (GL_TEXTURE_2D);
+
+      if (sp->id & 1)
+        glColor4f (1, 0, 0, 1);
+      else
+        glColor4f (0, 0, 1, 1);
+
+      /* Draw the "from" and "to" boxes
+       */
+      glBegin (GL_LINE_LOOP);
+      glVertex3f (sp->from.x,              sp->from.y,              0);
+      glVertex3f (sp->from.x + sp->from.w, sp->from.y,              0);
+      glVertex3f (sp->from.x + sp->from.w, sp->from.y + sp->from.h, 0);
+      glVertex3f (sp->from.x,              sp->from.y + sp->from.h, 0);
+      glEnd();
+
+      glBegin (GL_LINE_LOOP);
+      glVertex3f (sp->to.x,                sp->to.y,                0);
+      glVertex3f (sp->to.x + sp->to.w,     sp->to.y,                0);
+      glVertex3f (sp->to.x + sp->to.w,     sp->to.y + sp->to.h,     0);
+      glVertex3f (sp->to.x,                sp->to.y + sp->to.h,     0);
+      glEnd();
+
+      if (!wire) glEnable (GL_TEXTURE_2D);
+    }
 }
 
 
 static void
-display (ModeInfo *mi)
+tick_sprites (ModeInfo *mi)
 {
   slideshow_state *ss = &sss[MI_SCREEN(mi)];
-  int wire = MI_IS_WIREFRAME(mi);
-  glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
-  glLoadIdentity();
-  gluLookAt(0, 0, 15,
-            0, 0, 0,
-            0, 1, 0);
-  glPushMatrix();
+  int i;
+  for (i = 0; i < ss->nsprites; i++)
+      tick_sprite (mi, ss->sprites[i]);
+}
 
-  showscreen (mi, wire, ss->curr_screen, ss->curr_texid_index);
 
-  if (ss->in_transition)
-    showscreen (mi, wire, 1-ss->curr_screen,
-                (ss->in_file_transition
-                 ? 1 - ss->curr_texid_index
-                 : ss->curr_texid_index));
+static void
+draw_sprites (ModeInfo *mi)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  int i;
 
+  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+  glPushMatrix();
+  for (i = 0; i < ss->nsprites; i++)
+    draw_sprite (mi, ss->sprites[i]);
   glPopMatrix();
-  glFlush();
+
+  if (debug_p)                         /* draw a white box (the "screen") */
+    {
+      int wire = MI_IS_WIREFRAME(mi);
+
+      if (!wire) glDisable (GL_TEXTURE_2D);
+
+      glColor4f (1, 1, 1, 1);
+      glBegin (GL_LINE_LOOP);
+      glVertex3f (0, 0, 0);
+      glVertex3f (0, 1, 0);
+      glVertex3f (1, 1, 0);
+      glVertex3f (1, 0, 0);
+      glEnd();
+
+      if (!wire) glEnable (GL_TEXTURE_2D);
+    }
 }
 
-void
+
+ENTRYPOINT void
 reshape_slideshow (ModeInfo *mi, int width, int height)
 {
-  glViewport(0,0,(GLint)width, (GLint) height);
-  glMatrixMode(GL_PROJECTION);
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  GLfloat s;
+  glViewport (0, 0, width, height);
+  glMatrixMode (GL_PROJECTION);
+  glLoadIdentity();
+  glMatrixMode (GL_MODELVIEW);
   glLoadIdentity();
-  gluPerspective(45, 1, 2.0, 85);
-  glMatrixMode(GL_MODELVIEW);
+
+  s = 2;
+
+  if (debug_p)
+    {
+      s *= (zoom / 100.0) * 0.75;
+      if (s < 0.1) s = 0.1;
+    }
+
+  glScalef (s, s, s);
+  glTranslatef (-0.5, -0.5, 0);
+
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+  ss->redisplay_needed_p = True;
 }
 
-static void
-reset (ModeInfo *mi, int screen)
+
+ENTRYPOINT Bool
+slideshow_handle_event (ModeInfo *mi, XEvent *event)
 {
   slideshow_state *ss = &sss[MI_SCREEN(mi)];
-  ss->frames = 0;
 
-  if (screen >= NQUADS) abort();
-  ss->dz[screen] = (-.02+(RRAND(400)/10000.0)) * (GLfloat) zoom/100.0;
+  if (event->xany.type == ButtonPress &&
+      event->xbutton.button == Button1)
+    {
+      ss->change_now_p = True;
+      return True;
+    }
+  else if (event->xany.type == KeyPress)
+    {
+      KeySym keysym;
+      char c = 0;
+      XLookupString (&event->xkey, &c, 1, &keysym, 0);
+      if (c == ' ' || c == '\r' || c == '\n' || c == '\t')
+        {
+          ss->change_now_p = True;
+          return True;
+        }
+    }
+  else if (event->xany.type == Expose ||
+           event->xany.type == GraphicsExpose ||
+           event->xany.type == VisibilityNotify)
+    {
+      ss->redisplay_needed_p = True;
+      if (debug_p)
+        fprintf (stderr, "%s: exposure\n", blurb());
+      return False;
+    }
 
-  if (ss->dz[screen] < 0.0) {
-    ss->qz[screen] = 6.0 + RRAND(300)/100.0;
-  }
-  else {
-    ss->qz[screen] = 1.0 + RRAND(300)/100.0;
-  }
+  return False;
+}
+
+
+/* Do some sanity checking on various user-supplied values, and make
+   sure they are all internally consistent.
+ */
+static void
+sanity_check (ModeInfo *mi)
+{
+  if (zoom < 1) zoom = 1;           /* zoom is a positive percentage */
+  else if (zoom > 100) zoom = 100;
+
+  if (zoom == 100)                 /* with no zooming, there is no panning */
+    pan_seconds = 0;
+
+  if (pan_seconds < fade_seconds)   /* pan is inclusive of fade */
+    pan_seconds = fade_seconds;
+
+  if (pan_seconds == 0)             /* no zero-length cycles, please... */
+    pan_seconds = 1;
 
-  ss->qz[screen] *= (GLfloat) zoom/100.0;
+  if (image_seconds < pan_seconds)  /* we only change images at fade-time */
+    image_seconds = pan_seconds;
 
-  ss->dx[screen] = -.02 + RRAND(400)/10000.0;
-  ss->dy[screen] =- .01 + RRAND(200)/10000.0;
+  /* If we're not panning/zooming within the image, then there's no point
+     in crossfading the image with itself -- only do crossfades when changing
+     to a new image. */
+  if (zoom == 100 && pan_seconds < image_seconds)
+    pan_seconds = image_seconds;
 
-  ss->dx[screen] *= ss->qz[screen]/12.0;
-  ss->dy[screen] *= ss->qz[screen]/12.0;
+  /* No need to use mipmaps if we're not changing the image size much */
+  if (zoom >= 80) mipmap_p = False;
 
-  ss->qx[screen] = QX - ss->dx[screen] * 40.0 * ss->qz[screen];
-  ss->qy[screen] = QY + ss->dy[screen] * 40.0 * ss->qz[screen];  
+  if      (fps_cutoff < 0)  fps_cutoff = 0;
+  else if (fps_cutoff > 30) fps_cutoff = 30;
 }
 
 
 static void
-getSnapshot (ModeInfo *mi, int into_texid)
+check_fps (ModeInfo *mi)
 {
+#ifndef HAVE_COCOA  /* always assume Cocoa is fast enough */
+
   slideshow_state *ss = &sss[MI_SCREEN(mi)];
-  XImage *ximage;
-  int status;
-  
-  if(MI_IS_WIREFRAME(mi)) return;
 
-  ss->qw = QW;
-  ss->qh = QH;
+  double start_time, end_time, wall_elapsed, frame_duration, fps;
+  int i;
 
-  ximage = screen_to_ximage (mi->xgwa.screen, mi->window);
+  start_time = ss->now;
+  end_time = double_time();
+  frame_duration = end_time - start_time;   /* time spent drawing this frame */
+  ss->time_elapsed += frame_duration;       /* time spent drawing all frames */
+  ss->frames_elapsed++;
 
-  ss->tw = ximage->width;
-  ss->th = ximage->height;
-  
-  ss->qw *= (GLfloat) ss->tw / MI_WIDTH(mi);
-  ss->qh *= (GLfloat) ss->th / MI_HEIGHT(mi);
-  
-  ss->max_tx = (GLfloat) ss->tw / (GLfloat) ximage->width;
-  ss->max_ty = (GLfloat) ss->th / (GLfloat) ximage->height;
+  wall_elapsed = end_time - ss->dawn_of_time;
+  fps = ss->frames_elapsed / ss->time_elapsed;
+  ss->theoretical_fps = fps;
 
-  glBindTexture (GL_TEXTURE_2D, into_texid);
+  if (ss->checked_fps_p) return;
 
-  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
-  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
-                  GL_LINEAR_MIPMAP_LINEAR);
-  
-  clear_gl_error();
-  status = gluBuild2DMipmaps(GL_TEXTURE_2D, 3,
-                            ximage->width, ximage->height,
-                            GL_RGBA, GL_UNSIGNED_BYTE, ximage->data);
-  
-  if(!status && glGetError())
-   /* Some implementations of gluBuild2DMipmaps(), but set a GL error anyway.
-      We could just call check_gl_error(), but that would exit. */
-    status = -1;
-
-  if(status) {
-    const char *s = gluErrorString (status);
-
-    fprintf(stderr, "%s: error mipmapping %dx%d texture: %s\n",
-           progname, ximage->width, ximage->height,
-           (s ? s : "(unknown)"));
-    fprintf(stderr, "%s: turning on -wireframe.\n", progname);
-    MI_IS_WIREFRAME(mi) = 1;
-    clear_gl_error();
-  }
+  if (wall_elapsed <= 8)    /* too early to be sure */
+    return;
 
-  check_gl_error("mipmapping");  /* should get a return code instead of a
-                                   GL error, but just in case... */
-  
-  free(ximage->data);
-  ximage->data = 0;
-  XDestroyImage(ximage);
+  ss->checked_fps_p = True;
+
+  if (fps >= fps_cutoff)
+    {
+      if (debug_p)
+        fprintf (stderr,
+                 "%s: %.1f fps is fast enough (with %d frames in %.1f secs)\n",
+                 blurb(), fps, ss->frames_elapsed, wall_elapsed);
+      return;
+    }
+
+  fprintf (stderr,
+           "%s: only %.1f fps!  Turning off pan/fade to compensate...\n",
+           blurb(), fps);
+  zoom = 100;
+  fade_seconds = 0;
 
-  ss->start_time = time ((time_t *) 0);
+  sanity_check (mi);
+
+  for (i = 0; i < ss->nsprites; i++)
+    {
+      sprite *sp = ss->sprites[i];
+      randomize_sprite (mi, sp);
+      sp->state = FULL;
+    }
+
+  ss->redisplay_needed_p = True;
+
+  /* Need this in case zoom changed. */
+  reshape_slideshow (mi, mi->xgwa.width, mi->xgwa.height);
+#endif /* HAVE_COCOA */
 }
 
-void
+
+/* Kludge to add "-v" to invocation of "xscreensaver-getimage" in -debug mode
+ */
+static void
+hack_resources (void)
+{
+#if 0
+  char *res = "desktopGrabber";
+  char *val = get_string_resource (res, "DesktopGrabber");
+  char buf1[255];
+  char buf2[255];
+  XrmValue value;
+  sprintf (buf1, "%.100s.%.100s", progclass, res);
+  sprintf (buf2, "%.200s -v", val);
+  value.addr = buf2;
+  value.size = strlen(buf2);
+  XrmPutResource (&db, buf1, "String", &value);
+#endif
+}
+
+
+ENTRYPOINT void
 init_slideshow (ModeInfo *mi)
 {
   int screen = MI_SCREEN(mi);
   slideshow_state *ss;
+  int wire = MI_IS_WIREFRAME(mi);
   
-  if(sss == NULL) {
-    if((sss = (slideshow_state *)
-        calloc(MI_NUM_SCREENS(mi), sizeof(slideshow_state))) == NULL)
+  if (sss == NULL) {
+    if ((sss = (slideshow_state *)
+         calloc (MI_NUM_SCREENS(mi), sizeof(slideshow_state))) == NULL)
       return;
   }
-
   ss = &sss[screen];
-  ss->window = MI_WINDOW(mi);
-  ss->qw = QW;
-  ss->qh = QH;
 
-  if((ss->glx_context = init_GL(mi)) != NULL) {
-    reshape_slideshow(mi, MI_WIDTH(mi), MI_HEIGHT(mi));
+  if ((ss->glx_context = init_GL(mi)) != NULL) {
+    reshape_slideshow (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
   } else {
     MI_CLEARWINDOW(mi);
   }
 
-  glClearColor(0.0,0.0,0.0,0.0);
-  
-  if(! MI_IS_WIREFRAME(mi)) {
-    glShadeModel(GL_SMOOTH);
-    glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
-    glEnable(GL_DEPTH_TEST);
-    glEnable(GL_CULL_FACE);
-    glCullFace(GL_FRONT);
-    glDisable(GL_LIGHTING);
-
-    glGenTextures (1, &ss->texids[0]);  /* texture for image A */
-    glGenTextures (1, &ss->texids[1]);  /* texture for image B */
-  }
-  
-  reset(mi, ss->curr_screen);
-  ss->curr_texid_index = 0;
-  getSnapshot(mi, ss->texids[ss->curr_texid_index]);
+  if (debug_p)
+    fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
+             blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
+
+  sanity_check(mi);
+
+  if (debug_p)
+    fprintf (stderr, "%s: pan: %d; fade: %d; img: %d; zoom: %d%%\n\n",
+             blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
+
+  glDisable (GL_LIGHTING);
+  glDisable (GL_DEPTH_TEST);
+  glDepthMask (GL_FALSE);
+  glEnable (GL_CULL_FACE);
+  glCullFace (GL_BACK);
+
+  if (! wire)
+    {
+      glEnable (GL_TEXTURE_2D);
+      glShadeModel (GL_SMOOTH);
+      glEnable (GL_BLEND);
+      glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    }
+
+  if (debug_p) glLineWidth (3);
+
+  load_font (mi->dpy, "titleFont", &ss->xfont, &ss->font_dlist);
+
+  if (debug_p)
+    hack_resources();
+
+  ss->now = double_time();
+  ss->dawn_of_time = ss->now;
+  ss->prev_frame_time = ss->now;
+
+  ss->awaiting_first_image_p = True;
+  alloc_image (mi);
 }
 
-void
+
+ENTRYPOINT void
 draw_slideshow (ModeInfo *mi)
 {
   slideshow_state *ss = &sss[MI_SCREEN(mi)];
-  Window w = MI_WINDOW(mi);
-  Display *disp = MI_DISPLAY(mi);
+  int i;
 
-  if(!ss->glx_context) return;
+  if (!ss->glx_context)
+    return;
 
-  glXMakeCurrent(disp, w, *(ss->glx_context));
-  
-  if (ss->frames == frames_per_pan) {
+  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *(ss->glx_context));
 
-    time_t now = time ((time_t *) 0);
+  if (ss->awaiting_first_image_p)
+    {
+      image *img = ss->images[0];
+      if (!img) abort();
+      if (!img->loaded_p)
+        return;
 
-    if(fade) {
-      ss->in_transition = 1;
-      reset (mi, 1 - ss->curr_screen);
+      ss->awaiting_first_image_p = False;
+      ss->dawn_of_time = double_time();
 
-      if (ss->start_time + duration <= now) {
-        ss->in_file_transition = 1;
-        getSnapshot(mi, ss->texids[1 - ss->curr_texid_index]);
-      }
+      /* start the very first sprite fading in */
+      new_sprite (mi);
+    }
 
-    } else {
-      reset(mi, ss->curr_screen);
+  ss->now = double_time();
 
-      if (ss->start_time + duration <= now)
-        getSnapshot(mi, ss->texids[ss->curr_texid_index]);
-    }
-  }
+  /* Each sprite has three states: fading in, full, fading out.
+     The in/out states overlap like this:
+
+     iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . . . . . . . . . . . 
+     . . . . . . . . . iiiiiiFFFFFFFFFFFFoooooo  . . . . . . . .
+     . . . . . . . . . . . . . . . . . . iiiiiiFFFFFFFFFFFFooooo
 
-  if (fade && ss->in_transition && ss->frames == frames_per_fade) {
-    ss->in_transition = 0;
-    ss->curr_screen = 1 - ss->curr_screen;
+     So as soon as a sprite goes into the "out" state, we create
+     a new sprite (in the "in" state.)
+   */
 
-    if (ss->in_file_transition) {
-      ss->in_file_transition = 0;
-      ss->curr_texid_index = 1 - ss->curr_texid_index;
+  if (ss->nsprites > 2) abort();
+
+  /* If a sprite is just entering the fade-out state,
+     then add a new sprite in the fade-in state.
+   */
+  for (i = 0; i < ss->nsprites; i++)
+    {
+      sprite *sp = ss->sprites[i];
+      if (sp->state != sp->prev_state &&
+          sp->state == (fade_seconds == 0 ? DEAD : OUT))
+        new_sprite (mi);
     }
-  }
 
-  display(mi);
-  
-  ss->frames++;
+  tick_sprites (mi);
+
+  /* Now garbage collect the dead sprites.
+   */
+  for (i = 0; i < ss->nsprites; i++)
+    {
+      sprite *sp = ss->sprites[i];
+      if (sp->state == DEAD)
+        {
+          destroy_sprite (mi, sp);
+          i--;
+        }
+    }
 
-  if(mi->fps_p) do_fps(mi);
+  /* We can only ever end up with no sprites at all if the machine is
+     being really slow and we hopped states directly from FULL to DEAD
+     without passing OUT... */
+  if (ss->nsprites == 0)
+    new_sprite (mi);
 
-  glFinish(); 
-  glXSwapBuffers(disp, w);
-}
+  if (!ss->redisplay_needed_p)
+    return;
 
-void
-release_slideshow (ModeInfo *mi)
-{
-  if(sss != NULL) {
-    (void) free((void *) sss);
-    sss = NULL;
-  }
+  if (debug_p && ss->now - ss->prev_frame_time > 1)
+    fprintf (stderr, "%s: static screen for %.1f secs\n",
+             blurb(), ss->now - ss->prev_frame_time);
+
+  draw_sprites (mi);
 
-  FreeAllGL(MI);
+  ss->fps = fps_1 (mi);
+  if (mi->fps_p) fps_2 (mi);
+
+  glFinish();
+  glXSwapBuffers (MI_DISPLAY (mi), MI_WINDOW(mi));
+  ss->prev_frame_time = ss->now;
+  ss->redisplay_needed_p = False;
+  check_fps (mi);
 }
 
-#endif
+XSCREENSAVER_MODULE_2 ("GLSlideshow", glslideshow, slideshow)
+
+#endif /* USE_GL */