ftp://ftp.linux.ncsu.edu/mirror/ftp.redhat.com/pub/redhat/linux/enterprise/4/en/os...
[xscreensaver] / hacks / glx / glslideshow.c
index 9d8ca2e8a52062447c1fb688fe79844efde52683..d735e7bd5e3514010a9f41981587dfe45d4a3aa1 100644 (file)
@@ -1,14 +1,11 @@
-/*
- * glslideshow - takes a snapshot of the screen and smoothly scans around
- *               in it
+/* glslideshow, Copyright (c) 2003, 2004 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)
+ * First version Copyright (c) 2002, 2003 Mike Oliphant (oliphant@gtk.org)
+ * based on flipscreen3d, Copyright (C) 2001 Ben Buxton (bb@cactii.net).
  *
- * Framework based on flipscreen3d
- *   Copyright (C) 2001 Ben Buxton (bb@cactii.net)
- *
- * Smooth transitions between multiple files added by
- * Jamie Zawinski <jwz@jwz.org>
+ * Almost entirely rewritten by jwz, 21-Jun-2003.
  *
  * 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:
+ *
+ * - Resizing the window makes everything go black forevermore.  No idea why.
+ *
+ *
+ * - 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 less than half a
+ *   second.)  On slower machines, it can be much more pronounced.
+ *
+ *   In xscreensaver 4.17, I added the new functions fork_load_random_image()
+ *   and fork_screen_to_ximage() to make it possible to do image loading in
+ *   the background, in an attempt to solve this (the idea being to only swap
+ *   in the new image once it has been loaded.)  Using those routines, we
+ *   continue animating while the file system is being searched for an image
+ *   file; while that image data is read, parsed, and decompressed; while that
+ *   data is placed on a Pixmap in the X server.
+ *
+ *   However, two things still happen in the "parent" (glslideshow) process:
+ *   converting that server-side Pixmap to a client-side XImage (XGetImage);
+ *   and converting that XImage to an OpenGL texture (gluBuild2DMipmaps).
+ *   It's possible that some new code would allow us to do the Pixmap-to-XImage
+ *   conversion in the forked process (feed it back upstream through a pipe or
+ *   SHM segment or something); however, it turns out that significant
+ *   parent-process image-loading time is being spent in gluBuild2DMipmaps().
+ *
+ *   So, the next step would be to figure out some way to create a texture on
+ *   the other end of the fork that would be usable by the parent process.  Is
+ *   that even possible?  Is it possible to use a single GLX context in a
+ *   multithreaded way like that?  (Or use a second GLX context, but allow the
+ *   two contexts to share data?)
+ *
+ *   Another question remains: is the stalling happening in the GL/GLX
+ *   libraries, or are we actually seeing a stall on the graphics pipeline?
+ *   If the latter, then no amount of threading would help, because the
+ *   bottleneck is pushing the bits from system memory to the graphics card.
+ *
+ *   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?
+ *
+ *
+ * - Even if the glitch was solved, there's still a bug in the background
+ *   loading of images: as soon as the image comes in, we slap it into place
+ *   in the target quad.  This can lead to an image being changed while it is
+ *   still being drawn, if that quad happens to be visible already.  Instead,
+ *   when the callback goes off, we should make sure to load it into the
+ *   invisible quad, or if both are visible, we should wait until one goes
+ *   invisible and then load it there (in other words, wait for the next
+ *   fade-out to end.)
  */
 
 #include <X11/Intrinsic.h>
 # define HACK_INIT init_slideshow
 # define HACK_DRAW draw_slideshow
 # define HACK_RESHAPE reshape_slideshow
+# define HACK_HANDLE_EVENT glslideshow_handle_event
+# define EVENT_MASK        (ExposureMask|VisibilityChangeMask)
 # 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 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_DEBUG          "False"
+
+#define DEFAULTS  "*delay:           20000                \n" \
+                  "*fadeDuration:  " DEF_FADE_DURATION   "\n" \
+                  "*panDuration:   " DEF_PAN_DURATION    "\n" \
+                  "*imageDuration: " DEF_IMAGE_DURATION  "\n" \
+                  "*zoom:          " DEF_ZOOM            "\n" \
+                  "*FPScutoff:     " DEF_FPS_CUTOFF      "\n" \
+                 "*debug   :      " DEF_DEBUG           "\n" \
+                 "*wireframe:       False                \n" \
+                  "*showFPS:         False                \n" \
+                 "*fpsSolid:        True                 \n" \
+                 "*titles:        " DEF_TITLES  "\n" \
+                 "*titleFont:       -*-times-bold-r-normal-*-180-*\n" \
+                  "*desktopGrabber:  xscreensaver-getimage -no-desktop %s\n"
 
 # include "xlockmore.h"
 
-#define RRAND(range) (random()%(range))
 #undef countof
 #define countof(x) (sizeof((x))/sizeof((*x)))
 
 #include <stdlib.h>
 #include "grab-ximage.h"
 
+typedef struct {
+  GLfloat 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 {
+  GLuint texid;                           /* which texture to draw */
+  enum { IN, OUT, DEAD } state;    /* how to draw it */
+  rect from, to;                  /* the journey this quad is taking */
+  char *title;
+} gls_quad;
 
-#define NQUADS 2  /* sometimes we draw 2 at once */
 
 typedef struct {
   GLXContext *glx_context;
-  Window window;
+  time_t start_time;           /* when we started displaying this image */
 
-  int tw, th;                  /* texture width, height */
-  GLfloat max_tx, max_ty;
+  int motion_frames;            /* how many frames each pan takes */
+  int fade_frames;              /* how many frames fading in/out takes */
 
-  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];
+  gls_quad quads[2];           /* the (up to) 2 quads we animate */
+  GLuint texids[2];            /* textures: "old" and "new" */
+  GLuint current_texid;         /* the "new" one */
 
-  GLuint texids[NQUADS];       /* two textures: current img, incoming img */
+  int img_w, img_h;            /* Size (pixels) of currently-loaded image */
 
-  time_t start_time;           /* when we started displaying this image */
+  double now;                  /* current time in seconds */
+  double pan_start_time;       /* when this pan began */
+  double image_start_time;     /* when this image was loaded */
+  double dawn_of_time;         /* when the program launched */
+
+  Bool redisplay_needed_p;     /* Sometimes we can get away with not
+                                   re-painting.  Tick this if a redisplay
+                                   is required. */
 
-  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 */
+  GLfloat fps;                  /* approximate frame rate we're achieving */
+  int pan_frame_count;         /* More frame-rate stats */
+  int fade_frame_count;
+  Bool low_fps_p;              /* Whether we have compensated for a low
+                                   frame rate. */
+
+  Bool fork_p;                 /* threaded image loading; #### still buggy */
+
+  XFontStruct *xfont;
+  GLuint font_dlist;
 
 } slideshow_state;
 
@@ -93,365 +166,987 @@ 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 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",     ".slideshow.fadeDuration",  XrmoptionSepArg, 0     },
+  {"-pan",      ".slideshow.panDuration",   XrmoptionSepArg, 0     },
+  {"-duration", ".slideshow.imageDuration", XrmoptionSepArg, 0     },
+  {"-zoom",     ".slideshow.zoom",          XrmoptionSepArg, 0     },
+  {"-cutoff",   ".slideshow.FPScutoff",     XrmoptionSepArg, 0     },
+  {"-titles",   ".slideshow.titles",        XrmoptionNoArg, "True" },
+  {"+titles",   ".slideshow.titles",        XrmoptionNoArg, "True" },
+  {"-debug",    ".slideshow.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},
+  { &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};
 
 
-/* draw the texture mapped quad.
-   `screen' specifies which of the independently-moving images to draw.
+static const char *
+blurb (void)
+{
+  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;
+}
+
+
+/* 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
-showscreen (ModeInfo *mi, int wire, int screen, int texid_index)
+load_font (ModeInfo *mi, char *res, XFontStruct **fontP, GLuint *dlistP)
 {
-  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;
+  const char *font = get_string_resource (res, "Font");
+  XFontStruct *f;
+  Font id;
+  int first, last;
+
+  if (!font) font = "-*-times-bold-r-normal-*-180-*";
 
-  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;
+  f = XLoadQueryFont(mi->dpy, font);
+  if (!f) f = XLoadQueryFont(mi->dpy, "fixed");
 
-  ss->qx[screen] += ss->dx[screen];
-  ss->qy[screen] -= ss->dy[screen];
-  ss->qz[screen] += ss->dz[screen];
+  id = f->fid;
+  first = f->min_char_or_byte2;
+  last = f->max_char_or_byte2;
+  
+  clear_gl_error ();
+  *dlistP = glGenLists ((GLuint) last+1);
+  check_gl_error ("glGenLists");
+  glXUseXFont(id, first, last-first+1, *dlistP + first);
+  check_gl_error ("glXUseXFont");
 
-  glTranslatef(0, 0, ss->qz[screen]);
+  *fontP = f;
+}
 
-  if (ss->in_transition) {
-    a = 1 - (ss->frames/100.0);
 
-    if (screen != ss->curr_screen) {
-      a = 1-a;
+static void
+print_title_string (ModeInfo *mi, const char *string, GLfloat x, GLfloat y)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  XFontStruct *font = ss->xfont;
+  GLfloat line_height = font->ascent + font->descent;
+
+  y -= line_height;
+
+  glPushAttrib (GL_TRANSFORM_BIT |  /* for matrix contents */
+                GL_ENABLE_BIT);     /* for various glDisable calls */
+  glDisable (GL_LIGHTING);
+  glDisable (GL_DEPTH_TEST);
+  {
+    glMatrixMode(GL_PROJECTION);
+    glPushMatrix();
+    {
+      glLoadIdentity();
+
+      glMatrixMode(GL_MODELVIEW);
+      glPushMatrix();
+      {
+        unsigned int i;
+        int x2 = x;
+        glLoadIdentity();
+
+        gluOrtho2D (0, mi->xgwa.width, 0, mi->xgwa.height);
+
+        glRasterPos2f (x, y);
+        for (i = 0; i < strlen(string); i++)
+          {
+            char c = string[i];
+            if (c == '\n')
+              {
+                glRasterPos2f (x, (y -= line_height));
+                x2 = x;
+              }
+            else
+              {
+                glCallList (ss->font_dlist + (int)(c));
+                x2 += (font->per_char
+                       ? font->per_char[c - font->min_char_or_byte2].width
+                       : font->min_bounds.width);
+              }
+          }
+      }
+      glPopMatrix();
     }
+    glMatrixMode(GL_PROJECTION);
+    glPopMatrix();
   }
-  else {
-    a = 1;
-  }
+  glPopAttrib();
 
-  glColor4f(r, g, b, a);
+  glMatrixMode(GL_MODELVIEW);
+}
 
-  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]);
-  }
 
-  glBegin(wire ? GL_LINE_LOOP : GL_QUADS);
+static void
+draw_quad (ModeInfo *mi, gls_quad *q)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  int wire = MI_IS_WIREFRAME(mi);
+  GLfloat ratio;
+  rect current;
+  GLfloat opacity;
+  double secs;
+  GLfloat texw = 0;
+  GLfloat texh = 0;
 
-  glNormal3f(0, 0, 1);
+  if (q->state == DEAD)
+    return;
 
-  glTexCoord2f(0, ss->max_ty);
-  glVertex3f(x, y, 0);
+  secs = ss->now - ss->pan_start_time;
 
-  glTexCoord2f(ss->max_tx, ss->max_ty);
-  glVertex3f(w, y, 0);
+  if (q->state == OUT)
+    secs += pan_seconds;
 
-  glTexCoord2f(ss->max_tx, 0);
-  glVertex3f(w, h, 0);
+  ratio = secs / (pan_seconds + fade_seconds);
 
-  glTexCoord2f(0, 0);
-  glVertex3f(x, h, 0);
+  current.x = q->from.x + ratio * (q->to.x - q->from.x);
+  current.y = q->from.y + ratio * (q->to.y - q->from.y);
+  current.w = q->from.w + ratio * (q->to.w - q->from.w);
+  current.h = q->from.h + ratio * (q->to.h - q->from.h);
 
-  glEnd();
+  if (secs < fade_seconds)
+    opacity = secs / (GLfloat) fade_seconds;    /* fading in or out... */
+  else if (secs < pan_seconds)
+    opacity = 1;                               /* panning opaquely. */
+  else
+    opacity = 1 - ((secs - pan_seconds) /
+                   (GLfloat) fade_seconds);    /* fading in or out... */
 
-  if (wire) {
-    GLfloat i;
-    int k = 10;
-    glBegin(GL_LINES);
-    for (i = x; i < w; i += ((w-x)/k))
-      {
-        glVertex3f(i, y, 0);
-        glVertex3f(i, h, 0);
-      }
-    for (i = y; i >= h; i -= ((y-h)/k))
-      {
-        glVertex3f(x, i, 0);
-        glVertex3f(w, i, 0);
-      }
-    glVertex3f(x, y, 0);
-    glVertex3f(w, h, 0);
-    glVertex3f(x, h, 0);
-    glVertex3f(w, y, 0);
-    glEnd();
-  }
+  if (q->state == OUT && opacity < 0.0001)
+    q->state = DEAD;
 
-  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]);
+  glPushMatrix();
+
+  glTranslatef (current.x, current.y, 0);
+  glScalef (current.w, current.h, 1);
+
+  if (!wire)
+    {
+      texw = mi->xgwa.width  / (GLfloat) ss->img_w;
+      texh = mi->xgwa.height / (GLfloat) ss->img_h;
+
+      glEnable (GL_TEXTURE_2D);
+      glEnable (GL_BLEND);
+      glBindTexture (GL_TEXTURE_2D, q->texid);
+      glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+      glDepthMask (GL_FALSE);
+
+      /* Draw the texture quad
+       */
+      glColor4f (1, 1, 1, opacity);
+      glNormal3f (0, 0, 1);
+      glBegin (GL_QUADS);
+      glTexCoord2f (0,    0);    glVertex3f (0, 0, 0);
+      glTexCoord2f (0,    texh); glVertex3f (0, 1, 0);
+      glTexCoord2f (texw, texh); glVertex3f (1, 1, 0);
+      glTexCoord2f (texw, 0);    glVertex3f (1, 0, 0);
+      glEnd();
+
+      glDisable (GL_TEXTURE_2D);
+      glDisable (GL_BLEND);
+    }
+
+  if (wire)
+    glColor4f ((q->texid == ss->texids[0] ? opacity : 0), 0,
+               (q->texid == ss->texids[0] ? 0 : opacity),
+               opacity);
+  else
+    glColor4f (1, 1, 1, opacity);
+
+
+  /* Draw a grid inside the box
+   */
+  if (wire)
+    {
+      GLfloat d = 0.1;
+      GLfloat x, y;
+      glBegin(GL_LINES);
+      glVertex3f (0, 0, 0); glVertex3f (1, 1, 0);
+      glVertex3f (1, 0, 0); glVertex3f (0, 1, 0);
+
+      for (y = 0; y < 1+d; y += d)
+        for (x = 0; x < 1+d; x += d)
+          {
+            glVertex3f (0, y, 0); glVertex3f (1, y, 0);
+            glVertex3f (x, 0, 0); glVertex3f (x, 1, 0);
+          }
+      glEnd();
+    }
+
+  if (do_titles &&
+      q->state != DEAD &&
+      q->title && *q->title)
+    {
+      /* #### this is wrong -- I really want to draw this with
+         "1,1,1,opacity", so that the text gets laid down on top
+         of the image with alpha, but that doesn't work, and I
+         don't know why...
+       */
+      glColor4f (opacity, opacity, opacity, 1);
+      print_title_string (mi, q->title,
+                          10, mi->xgwa.height - 10);
+    }
+
+  glPopMatrix();
+
+  if (debug_p)
+    {
+      /* Draw the "from" and "to" boxes
+       */
+      glColor4f ((q->texid == ss->texids[0] ? opacity : 0), 0,
+                 (q->texid == ss->texids[0] ? 0 : opacity),
+                 opacity);
+
+      glBegin (GL_LINE_LOOP);
+      glVertex3f (q->from.x,             q->from.y,             0);
+      glVertex3f (q->from.x + q->from.w, q->from.y,             0);
+      glVertex3f (q->from.x + q->from.w, q->from.y + q->from.h, 0);
+      glVertex3f (q->from.x,             q->from.y + q->from.h, 0);
+      glEnd();
+
+      glBegin (GL_LINE_LOOP);
+      glVertex3f (q->to.x,               q->to.y,               0);
+      glVertex3f (q->to.x + q->to.w,     q->to.y,               0);
+      glVertex3f (q->to.x + q->to.w,     q->to.y + q->to.h,     0);
+      glVertex3f (q->to.x,               q->to.y + q->to.h,     0);
+      glEnd();
+    }
 }
 
 
 static void
-display (ModeInfo *mi)
+draw_quads (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);
+  GLfloat s, o;
+  int i;
+
+  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
   glPushMatrix();
 
-  showscreen (mi, wire, ss->curr_screen, ss->curr_texid_index);
+  s = (100.0 / zoom);
+  o = (1-s)/2;
+  glTranslatef (o, o, 0);
+  glScalef (s, s, s);
 
-  if (ss->in_transition)
-    showscreen (mi, wire, 1-ss->curr_screen,
-                (ss->in_file_transition
-                 ? 1 - ss->curr_texid_index
-                 : ss->curr_texid_index));
+  for (i = 0; i < countof(ss->quads); i++)
+    draw_quad (mi, &ss->quads[i]);
 
   glPopMatrix();
-  glFlush();
-}
 
-void
-reshape_slideshow (ModeInfo *mi, int width, int height)
-{
-  glViewport(0,0,(GLint)width, (GLint) height);
-  glMatrixMode(GL_PROJECTION);
-  glLoadIdentity();
-  gluPerspective(45, 1, 2.0, 85);
-  glMatrixMode(GL_MODELVIEW);
+  if (debug_p)
+    {
+      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();
+    }
 }
 
+
+/* Re-randomize the state of the given quad.
+ */
 static void
-reset (ModeInfo *mi, int screen)
+reset_quad (ModeInfo *mi, gls_quad *q)
 {
-  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;
+/*  slideshow_state *ss = &sss[MI_SCREEN(mi)];*/
+
+  GLfloat mid_w = (zoom / 100.0);
+  GLfloat mid_h = (zoom / 100.0);
+  GLfloat mid_x = (1 - mid_w) / 2;
+  GLfloat mid_y = (1 - mid_h) / 2;
+
+  GLfloat small = mid_w + frand ((1 - mid_w) * 0.3);
+#if 0
+  GLfloat large = small + frand ((1 - small) / 2) + ((1 - small) / 2);
+#else
+  GLfloat large = small + frand (1 - small);
+#endif
 
-  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;
-  }
+  if (q->state != DEAD)
+    abort();    /* we should only be resetting a quad when it's not visible. */
+
+  /* Possible box sizes range between "zoom" and "100%".
+     Pick a small box size, and a large box size.
+     Assign each a random position within the 1x1 box,
+     such that they encompass the middle "zoom" percentage.
+     One of those is the start, and one is the end.
+     Each frame will transition between one and the other.
+   */
+
+  if (random() & 1)
+    {
+      q->from.w = small; q->from.h = small;
+      q->to.w   = large; q->to.h   = large;
+    }
+  else
+    {
+      q->from.w = large; q->from.h = large;
+      q->to.w   = small; q->to.h   = small;
+    }
 
-  ss->qz[screen] *= (GLfloat) zoom/100.0;
+  q->from.x = mid_x - frand (q->from.w - mid_w);
+  q->from.y = mid_y - frand (q->from.h - mid_h);
+  q->to.x   = mid_x - frand (q->to.w - mid_w);
+  q->to.y   = mid_y - frand (q->to.w - mid_h);
 
-  ss->dx[screen] = -.02 + RRAND(400)/10000.0;
-  ss->dy[screen] =- .01 + RRAND(200)/10000.0;
+  q->state = IN;
+}
 
-  ss->dx[screen] *= ss->qz[screen]/12.0;
-  ss->dy[screen] *= ss->qz[screen]/12.0;
 
-  ss->qx[screen] = QX - ss->dx[screen] * 40.0 * ss->qz[screen];
-  ss->qy[screen] = QY + ss->dy[screen] * 40.0 * ss->qz[screen];  
+/* Shrinks the XImage by a factor of two.
+ */
+static void
+shrink_image (ModeInfo *mi, XImage *ximage)
+{
+  int w2 = ximage->width/2;
+  int h2 = ximage->height/2;
+  int x, y;
+  XImage *ximage2;
+
+  if (w2 <= 32 || h2 <= 32)   /* let's not go crazy here, man. */
+    return;
+
+  if (debug_p)
+    fprintf (stderr, "%s: debug: shrinking image %dx%d -> %dx%d\n",
+             blurb(), ximage->width, ximage->height, w2, h2);
+
+  ximage2 = XCreateImage (MI_DISPLAY (mi), mi->xgwa.visual,
+                          32, ZPixmap, 0, 0,
+                          w2, h2, 32, 0);
+  ximage2->data = (char *) calloc (h2, ximage2->bytes_per_line);
+  if (!ximage2->data)
+    {
+      fprintf (stderr, "%s: out of memory (scaling %dx%d image to %dx%d)\n",
+               blurb(), ximage->width, ximage->height, w2, h2);
+      exit (1);
+    }
+  for (y = 0; y < h2; y++)
+    for (x = 0; x < w2; x++)
+      XPutPixel (ximage2, x, y, XGetPixel (ximage, x*2, y*2));
+  free (ximage->data);
+  *ximage = *ximage2;
+  ximage2->data = 0;
+  XFree (ximage2);
 }
 
 
+/* Load a new image into a texture for the given quad.
+ */
 static void
-getSnapshot (ModeInfo *mi, int into_texid)
+load_quad_1 (ModeInfo *mi, gls_quad *q, XImage *ximage,
+             const char *filename, double start_time, double cvt_time)
 {
   slideshow_state *ss = &sss[MI_SCREEN(mi)];
-  XImage *ximage;
   int status;
-  
-  if(MI_IS_WIREFRAME(mi)) return;
+  int max_reduction = 7;
+  int err_count = 0;
+  int wire = MI_IS_WIREFRAME(mi);
+  double load_time=0, mipmap_time=0;   /* for debugging messages */
+
+  /* if (q->state != DEAD) abort(); */
+
+  /* Figure out which texid is currently in use, and pick the other one.
+   */
+  {
+    GLuint tid = 0;
+    int i;
+    if (ss->current_texid == 0)
+      tid = ss->texids[0];
+    else
+      for (i = 0; i < countof(ss->texids); i++)
+        if (ss->texids[i] != ss->current_texid)
+          {
+            tid = ss->texids[i];
+            break;
+          }
+
+    if (tid == 0) abort();   /* both textures in use by visible quads? */
+    q->texid = tid;
+    ss->current_texid = tid;
+  }
 
-  ss->qw = QW;
-  ss->qh = QH;
+  if (wire)
+    goto DONE;
 
-  ximage = screen_to_ximage (mi->xgwa.screen, mi->window);
+  if (q->title) free (q->title);
+  q->title = (filename ? strdup (filename) : 0);
 
-  ss->tw = mi->xgwa.width;
-  ss->th = mi->xgwa.height;
-/*  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;
+  if (q->title)   /* strip filename to part after last /. */
+    {
+      char *s = strrchr (q->title, '/');
+      if (s) strcpy (q->title, s+1);
+    }
 
-  glBindTexture (GL_TEXTURE_2D, into_texid);
+  if (debug_p)
+    {
+      fprintf (stderr, "%s: debug: loaded    image %d: \"%s\"\n",
+               blurb(), q->texid, (q->title ? q->title : "(null)"));
+      load_time = double_time();
+    }
 
-  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
-  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
-                  GL_LINEAR_MIPMAP_LINEAR);
+  glBindTexture (GL_TEXTURE_2D, q->texid);
+  glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
+                   GL_LINEAR_MIPMAP_LINEAR);
   
+  ss->img_w = ximage->width;
+  ss->img_h = ximage->height;
+
+ AGAIN:
+
   clear_gl_error();
-  status = gluBuild2DMipmaps(GL_TEXTURE_2D, 3,
-                            ximage->width, ximage->height,
-                            GL_RGBA, GL_UNSIGNED_BYTE, ximage->data);
+  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 (status)
+    {
+      char buf[100];
+      const char *s = (char *) gluErrorString (status);
+
+      if (!s || !*s)
+        {
+          sprintf (buf, "unknown error %d", status);
+          s = buf;
+        }
+
+      clear_gl_error();
+
+      if (++err_count > max_reduction)
+        {
+          fprintf(stderr,
+                  "\n"
+                  "%s: %dx%d texture failed, even after reducing to %dx%d:\n"
+                  "%s: GLU said: \"%s\".\n"
+                  "%s: probably this means "
+                  "\"your video card is worthless and weak\"?\n\n",
+                  blurb(), MI_WIDTH(mi), MI_HEIGHT(mi),
+                  ximage->width, ximage->height,
+                  blurb(), s,
+                  blurb());
+          exit (1);
+        }
+      else
+        {
+          if (debug_p)
+            fprintf (stderr, "%s: debug: mipmap error (%dx%d): %s\n",
+                     blurb(), ximage->width, ximage->height, s);
+          shrink_image (mi, ximage);
+          goto AGAIN;
+        }
+    }
 
   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->start_time = time ((time_t *) 0);
+  if (debug_p)
+    {
+      fprintf (stderr, "%s: debug: mipmapped image %d: %dx%d\n",
+               blurb(), q->texid, mi->xgwa.width, mi->xgwa.height);
+      mipmap_time = double_time();
+    }
+
+  if (cvt_time == 0)
+    cvt_time = load_time;
+  if (debug_p)
+    fprintf (stderr,
+             "%s: debug: load time elapsed: %.2f + %.2f + %.2f = %.2f sec\n",
+             blurb(),
+             cvt_time    - start_time,
+             load_time   - cvt_time,
+             mipmap_time - load_time,
+             mipmap_time - start_time);
+
+ DONE:
+
+  /* Re-set "now" so that time spent loading the image file does not count
+     against the time remaining in this stage of the animation: image loading,
+     if it takes a perceptible amount of time, will cause the animation to
+     pause, but will not cause it to drop frames.
+   */
+  ss->now = double_time ();
+  ss->image_start_time = ss->now;
+
+  ss->redisplay_needed_p = True;
 }
 
+
+static void slideshow_load_cb (Screen *, Window, XImage *,
+                               const char *filename, void *closure,
+                               double cvt_time);
+
+typedef struct {
+  ModeInfo *mi;
+  gls_quad *q;
+  double start_time;
+} img_load_closure;
+
+
+/* Load a new image into a texture for the given quad.
+ */
+static void
+load_quad (ModeInfo *mi, gls_quad *q)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  img_load_closure *data;
+
+  if (debug_p)
+    fprintf (stderr, "%s: debug: loading   image %d: %dx%d\n",
+             blurb(), q->texid, mi->xgwa.width, mi->xgwa.height);
+
+  if (q->state != DEAD) abort();
+  if (q->title) free (q->title);
+  q->title = 0;
+
+  if (MI_IS_WIREFRAME(mi))
+    return;
+
+  data = (img_load_closure *) calloc (1, sizeof(*data));
+  data->mi = mi;
+  data->q = q;
+  data->start_time = double_time();
+
+  if (ss->fork_p)
+    {
+      fork_screen_to_ximage (mi->xgwa.screen, mi->window,
+                             slideshow_load_cb, data);
+    }
+  else
+    {
+      char *title = 0;
+      XImage *ximage = screen_to_ximage (mi->xgwa.screen, mi->window, &title);
+      slideshow_load_cb (mi->xgwa.screen, mi->window, ximage, title, data, 0);
+    }
+}
+
+
+static void
+slideshow_load_cb (Screen *screen, Window window, XImage *ximage,
+                   const char *filename, void *closure, double cvt_time)
+{
+  img_load_closure *data = (img_load_closure *) closure;
+  load_quad_1 (data->mi, data->q, ximage, filename,
+               data->start_time, cvt_time);
+  memset (data, 0, sizeof (*data));
+  free (data);
+}
+
+
+void
+reshape_slideshow (ModeInfo *mi, int width, int height)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  GLfloat s;
+  glViewport (0, 0, width, height);
+  glMatrixMode (GL_PROJECTION);
+  glLoadIdentity();
+  glMatrixMode (GL_MODELVIEW);
+  glLoadIdentity();
+
+  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;
+}
+
+
+Bool
+glslideshow_handle_event (ModeInfo *mi, XEvent *event)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+
+  if (event->xany.type == Expose ||
+      event->xany.type == GraphicsExpose ||
+      event->xany.type == VisibilityNotify)
+    {
+      if (debug_p)
+        fprintf (stderr, "%s: debug: exposure\n", blurb());
+      ss->redisplay_needed_p = True;
+      return True;
+    }
+  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 (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;
+
+  if (image_seconds < pan_seconds)  /* we only change images at fade-time */
+    image_seconds = pan_seconds;
+
+  /* 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;
+
+  if      (fps_cutoff < 0)  fps_cutoff = 0;
+  else if (fps_cutoff > 30) fps_cutoff = 30;
+}
+
+
+/* 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
+}
+
+
 void
 init_slideshow (ModeInfo *mi)
 {
   int screen = MI_SCREEN(mi);
   slideshow_state *ss;
+  int wire = MI_IS_WIREFRAME(mi);
+  int i;
   
-  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: debug: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
+             blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
 
-void
-draw_slideshow (ModeInfo *mi)
-{
-  slideshow_state *ss = &sss[MI_SCREEN(mi)];
-  Window w = MI_WINDOW(mi);
-  Display *disp = MI_DISPLAY(mi);
+  sanity_check(mi);
 
-  if(!ss->glx_context) return;
+  if (debug_p)
+    fprintf (stderr, "%s: debug: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
+             blurb(), pan_seconds, fade_seconds, image_seconds, zoom);
 
-  glXMakeCurrent(disp, w, *(ss->glx_context));
-  
-  if (ss->frames == frames_per_pan) {
+  if (! wire)
+    {
+      glShadeModel (GL_SMOOTH);
+      glPolygonMode (GL_FRONT_AND_BACK,GL_FILL);
+      glEnable (GL_DEPTH_TEST);
+      glEnable (GL_CULL_FACE);
+      glCullFace (GL_FRONT);
+      glDisable (GL_LIGHTING);
+    }
 
-    time_t now = time ((time_t *) 0);
+  ss->now = double_time ();
+  ss->dawn_of_time = ss->now;
 
-    if(fade) {
-      ss->in_transition = 1;
-      reset (mi, 1 - ss->curr_screen);
+  if (debug_p) glLineWidth (3);
 
-      if (ss->start_time + duration <= now) {
-        ss->in_file_transition = 1;
-        getSnapshot(mi, ss->texids[1 - ss->curr_texid_index]);
-      }
+  ss->pan_start_time   = ss->now;
+  ss->image_start_time = ss->now;
+
+  load_font (mi, "titleFont", &ss->xfont, &ss->font_dlist);
 
-    } else {
-      reset(mi, ss->curr_screen);
+  for (i = 0; i < countof(ss->texids); i++)
+    glGenTextures (1, &ss->texids[i]);
+  ss->current_texid = 0;
 
-      if (ss->start_time + duration <= now)
-        getSnapshot(mi, ss->texids[ss->curr_texid_index]);
+  for (i = 0; i < countof(ss->quads); i++)
+    {
+      gls_quad *q = &ss->quads[i];
+      q->texid = ss->current_texid;
+      q->state = DEAD;
+      reset_quad (mi, q);
+      q->state = DEAD;
     }
-  }
 
-  if (fade && ss->in_transition && ss->frames == frames_per_fade) {
-    ss->in_transition = 0;
-    ss->curr_screen = 1 - ss->curr_screen;
+  if (debug_p)
+    hack_resources();
+
+  load_quad (mi, &ss->quads[0]);
+  ss->quads[0].state = IN;
+
+  ss->redisplay_needed_p = True;
+
+  ss->fork_p = 0; /* #### buggy */
+
+}
+
 
-    if (ss->in_file_transition) {
-      ss->in_file_transition = 0;
-      ss->curr_texid_index = 1 - ss->curr_texid_index;
+/* Call this each time we change from one state to another.
+   It gathers statistics on the frame rate of the previous state,
+   and if it's bad, turn things off (under the assumption that
+   we're running on sucky hardware.)
+ */
+static void
+ponder_state_change (ModeInfo *mi)
+{
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  const char *which;
+  int frames, secs;
+  GLfloat fps;
+
+  if (ss->fade_frame_count && ss->pan_frame_count)
+    abort();  /* one of these should be zero! */
+  else if (ss->fade_frame_count)   /* just finished fading */
+    {
+      which = "faded ";
+      secs = fade_seconds;
+      frames = ss->fade_frame_count;
+      ss->fade_frame_count = 0;
     }
-  }
+  else if (ss->pan_frame_count)   /* just finished panning */
+    {
+      which = "panned";
+      secs = pan_seconds;
+      frames = ss->pan_frame_count;
+      ss->pan_frame_count = 0;
+    }
+  else
+    return;  /* One of those should be non-zero! Maybe we just started,
+                and the machine is insanely slow. */
 
-  display(mi);
-  
-  ss->frames++;
+  fps = frames / (GLfloat) secs;
 
-  if(mi->fps_p) do_fps(mi);
+  if (debug_p)
+    fprintf (stderr, "%s: debug: %s %3d frames %2d sec %4.1f fps\n",
+             blurb(), which, frames, secs, fps);
 
-  glFinish(); 
-  glXSwapBuffers(disp, w);
+
+  if (fps < fps_cutoff && !ss->low_fps_p)   /* oops, this computer sucks! */
+    {
+      int i;
+
+      fprintf (stderr,
+               "%s: only %.1f fps!  Turning off pan/fade to compensate...\n",
+               blurb(), fps);
+      zoom = 100;
+      fade_seconds = 0;
+      ss->low_fps_p = True;
+
+      sanity_check (mi);
+
+      /* Reset all quads, and mark only #0 as active. */
+      for (i = 0; i < countof(ss->quads); i++)
+        {
+          gls_quad *q = &ss->quads[i];
+          q->state = DEAD;
+          reset_quad (mi, q);
+          q->texid = ss->current_texid;
+          q->state = (i == 0 ? IN : DEAD);
+        }
+
+      ss->pan_start_time = ss->now;
+      ss->redisplay_needed_p = True;
+
+      /* Need this in case zoom changed. */
+      reshape_slideshow (mi, mi->xgwa.width, mi->xgwa.height);
+    }
 }
 
+
 void
-release_slideshow (ModeInfo *mi)
+draw_slideshow (ModeInfo *mi)
 {
-  if(sss != NULL) {
-    (void) free((void *) sss);
-    sss = NULL;
-  }
+  slideshow_state *ss = &sss[MI_SCREEN(mi)];
+  Window w = MI_WINDOW(mi);
+  double secs;
+
+  if (!ss->glx_context)
+    return;
+
+  if (zoom < 100)
+    ss->redisplay_needed_p = True;
+
+  /* States:
+      0: - A invisible,  B invisible
+         - A fading in,  B invisible
+
+      1: - A opaque,     B invisible
+         - A fading out, B fading in
+         - A invisible, gets reset
+         - A invisible,  B opaque
+
+      2: - A invisible,  B opaque
+         - A fading in,  B fading out
+         - B invisible, gets reset
+         - A opaque,     B invisible (goto 1)
+  */
+
+  ss->now = double_time();
+
+  secs = ss->now - ss->pan_start_time;
+
+  if (secs < fade_seconds)
+    {
+      /* We are in the midst of a fade:
+         one quad is fading in, the other is fading out.
+         (If this is the very first time, then the one
+         fading out is already out.)
+       */
+      ss->redisplay_needed_p = True;
+      ss->fade_frame_count++;
+
+      if (! ((ss->quads[0].state == IN && ss->quads[1].state == OUT) ||
+             (ss->quads[1].state == IN && ss->quads[0].state == OUT) ||
+             (ss->quads[0].state == IN && ss->quads[1].state == DEAD)))
+        abort();
+    }
+  else if (secs < pan_seconds)
+    {
+      /* One quad is visible and in motion, the other is not.
+      */
+      if (ss->fade_frame_count != 0)  /* we just switched from fade to pan */
+        ponder_state_change (mi);
+      ss->pan_frame_count++;
+    }
+  else
+    {
+      /* One quad is visible and in motion, the other is not.
+         It's time to begin fading the visible one out, and the
+         invisible one in.  (Reset the invisible one first.)
+       */
+      gls_quad *vq, *iq;
+
+      ponder_state_change (mi);
+
+      if (ss->quads[0].state == IN)
+        {
+          vq = &ss->quads[0];
+          iq = &ss->quads[1];
+        }
+      else
+        {
+          vq = &ss->quads[1];
+          iq = &ss->quads[0];
+        }
+
+      if (vq->state != IN)   abort();
+
+      /* I don't understand why sometimes iq is still OUT and not DEAD. */
+      if (iq->state == OUT)  iq->state = DEAD;
+      if (iq->state != DEAD) abort();
+
+      vq->state = OUT;
+
+      if (ss->image_start_time + image_seconds <= ss->now)
+        load_quad (mi, iq);
+
+      reset_quad (mi, iq);               /* fade invisible in */
+      iq->texid = ss->current_texid;     /* make sure we're using latest img */
+
+      ss->pan_start_time = ss->now;
+
+      if (! ((ss->quads[0].state == IN && ss->quads[1].state == OUT) ||
+             (ss->quads[1].state == IN && ss->quads[0].state == OUT)))
+        abort();
+    }
+
+  ss->fps = fps_1 (mi);
+
+  if (!ss->redisplay_needed_p)
+    return;
+  else if (debug_p && zoom == 100)
+    fprintf (stderr, "%s: debug: drawing (%d)\n", blurb(),
+             (int) (ss->now - ss->dawn_of_time));
 
-  FreeAllGL(MI);
+  draw_quads (mi);
+  ss->redisplay_needed_p = False;
+
+  if (mi->fps_p) fps_2(mi);
+
+  glFinish();
+  glXSwapBuffers (MI_DISPLAY (mi), w);
 }
 
-#endif
+#endif /* USE_GL */