http://www.uw-madison.lkams.kernel.org/pub/mirrors/fink/distfiles/xscreensaver-4...
[xscreensaver] / hacks / glx / glslideshow.c
index 9d8ca2e8a52062447c1fb688fe79844efde52683..f705a65c010da82bf24c30a26a6f4c23c2aba4cc 100644 (file)
@@ -1,14 +1,11 @@
-/*
- * glslideshow - takes a snapshot of the screen and smoothly scans around
- *               in it
+/* glslideshow, Copyright (c) 2003 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
 # 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_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" \
+                  "*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 */
+} 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];
+  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. */
 
 } slideshow_state;
 
@@ -93,365 +107,745 @@ 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;
+int fade_seconds;    /* Duration in seconds of fade transitions.
+                        If 0, jump-cut instead of fading. */
+int pan_seconds;     /* Duration of each pan through an image. */
+int image_seconds;   /* 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."  */
+int fps_cutoff;      /* If the frame-rate falls below this, turn off zooming.*/
+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     },
+  {"-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},
 };
 
 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.
+/* 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)
+draw_quad (ModeInfo *mi, quad *q)
 {
   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;
+  int wire = MI_IS_WIREFRAME(mi);
+  GLfloat ratio;
+  rect current;
+  GLfloat opacity;
+  double secs;
+  GLfloat texw = 0;
+  GLfloat texh = 0;
 
-  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 (q->state == DEAD)
+    return;
 
-  ss->qx[screen] += ss->dx[screen];
-  ss->qy[screen] -= ss->dy[screen];
-  ss->qz[screen] += ss->dz[screen];
+  secs = ss->now - ss->pan_start_time;
 
-  glTranslatef(0, 0, ss->qz[screen]);
+  if (q->state == OUT)
+    secs += pan_seconds;
 
-  if (ss->in_transition) {
-    a = 1 - (ss->frames/100.0);
+  ratio = secs / (pan_seconds + fade_seconds);
 
-    if (screen != ss->curr_screen) {
-      a = 1-a;
-    }
-  }
-  else {
-    a = 1;
-  }
+  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);
 
-  glColor4f(r, g, b, a);
+  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) {
-    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]);
-  }
+  if (q->state == OUT && opacity < 0.0001)
+    q->state = DEAD;
 
-  glBegin(wire ? GL_LINE_LOOP : GL_QUADS);
-
-  glNormal3f(0, 0, 1);
-
-  glTexCoord2f(0, ss->max_ty);
-  glVertex3f(x, y, 0);
-
-  glTexCoord2f(ss->max_tx, ss->max_ty);
-  glVertex3f(w, y, 0);
-
-  glTexCoord2f(ss->max_tx, 0);
-  glVertex3f(w, h, 0);
-
-  glTexCoord2f(0, 0);
-  glVertex3f(x, h, 0);
-
-  glEnd();
-
-  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();
-  }
+  glPushMatrix();
 
-  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]);
+  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();
+    }
+
+  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, 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",
+             progname, 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",
+               progname, 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 (ModeInfo *mi, quad *q)
 {
   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);
+
+  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;
+  }
+
+  if (debug_p)
+    fprintf (stderr, "%s: debug: loading image %d (%dx%d)\n",
+             progname, q->texid, mi->xgwa.width, mi->xgwa.height);
 
-  ss->qw = QW;
-  ss->qh = QH;
+  if (wire)
+    goto DONE;
 
   ximage = screen_to_ximage (mi->xgwa.screen, mi->window);
 
-  ss->tw = mi->xgwa.width;
-  ss->th = mi->xgwa.height;
-/*  ss->tw = ximage->width; */
-/*  ss->th = ximage->height; */
+  glBindTexture (GL_TEXTURE_2D, q->texid);
+  glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
+  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
+                   GL_LINEAR_MIPMAP_LINEAR);
   
-  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;
+  ss->img_w = ximage->width;
+  ss->img_h = ximage->height;
 
-  glBindTexture (GL_TEXTURE_2D, into_texid);
+ AGAIN:
 
-  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);
+  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 = 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",
+                  progname, MI_WIDTH(mi), MI_HEIGHT(mi),
+                  ximage->width, ximage->height,
+                  progname, s,
+                  progname);
+          exit (1);
+        }
+      else
+        {
+          if (debug_p)
+            fprintf (stderr, "%s: debug: mipmap error (%dx%d): %s\n",
+                     progname, 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);
+ 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;
+}
+
+
+
+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", progname);
+      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;
+}
+
+
 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]);
+  sanity_check(mi);
+
+  if (debug_p)
+    fprintf (stderr, "%s: debug: pan: %d; fade: %d; img: %d; zoom: %d%%\n",
+             progname, pan_seconds, fade_seconds, image_seconds, zoom);
+
+  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);
+    }
+
+  ss->now = double_time ();
+  ss->dawn_of_time = ss->now;
+
+  if (debug_p) glLineWidth (3);
+
+  ss->pan_start_time   = ss->now;
+  ss->image_start_time = ss->now;
+
+  for (i = 0; i < countof(ss->texids); i++)
+    glGenTextures (1, &ss->texids[i]);
+  ss->current_texid = 0;
+
+  for (i = 0; i < countof(ss->quads); i++)
+    {
+      quad *q = &ss->quads[i];
+      q->texid = ss->current_texid;
+      q->state = DEAD;
+      reset_quad (mi, q);
+      q->state = DEAD;
+    }
+
+  load_quad (mi, &ss->quads[0]);
+  ss->quads[0].state = IN;
+
+  ss->redisplay_needed_p = True;
 }
 
-void
-draw_slideshow (ModeInfo *mi)
+
+/* 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)];
-  Window w = MI_WINDOW(mi);
-  Display *disp = MI_DISPLAY(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
+    abort();  /* one of these should be non-zero! */
 
-  if(!ss->glx_context) return;
+  fps = frames / (GLfloat) secs;
 
-  glXMakeCurrent(disp, w, *(ss->glx_context));
-  
-  if (ss->frames == frames_per_pan) {
+  if (debug_p)
+    fprintf (stderr, "%s: debug: %s %3d frames %2d sec %4.1f fps\n",
+             progname, which, frames, secs, fps);
 
-    time_t now = time ((time_t *) 0);
 
-    if(fade) {
-      ss->in_transition = 1;
-      reset (mi, 1 - ss->curr_screen);
+  if (fps < fps_cutoff && !ss->low_fps_p)   /* oops, this computer sucks! */
+    {
+      int i;
 
-      if (ss->start_time + duration <= now) {
-        ss->in_file_transition = 1;
-        getSnapshot(mi, ss->texids[1 - ss->curr_texid_index]);
-      }
+      fprintf (stderr,
+               "%s: frame rate is only %.1f!  "
+               "Turning off pan/fade to compensate...\n",
+               progname, fps);
+      zoom = 100;
+      fade_seconds = 0;
+      ss->low_fps_p = True;
 
-    } else {
-      reset(mi, ss->curr_screen);
+      sanity_check (mi);
 
-      if (ss->start_time + duration <= now)
-        getSnapshot(mi, ss->texids[ss->curr_texid_index]);
+      /* Reset all quads, and mark only #0 as active. */
+      for (i = 0; i < countof(ss->quads); i++)
+        {
+          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);
     }
-  }
+}
 
-  if (fade && ss->in_transition && ss->frames == frames_per_fade) {
-    ss->in_transition = 0;
-    ss->curr_screen = 1 - ss->curr_screen;
 
-    if (ss->in_file_transition) {
-      ss->in_file_transition = 0;
-      ss->curr_texid_index = 1 - ss->curr_texid_index;
+void
+draw_slideshow (ModeInfo *mi)
+{
+  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.)
+       */
+      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();
     }
-  }
 
-  display(mi);
-  
-  ss->frames++;
+  ss->fps = fps_1 (mi);
 
-  if(mi->fps_p) do_fps(mi);
+  if (!ss->redisplay_needed_p)
+    return;
+  else if (debug_p && zoom == 100)
+    fprintf (stderr, "%s: debug: drawing (%d)\n", progname,
+             (int) (ss->now - ss->dawn_of_time));
 
-  glFinish(); 
-  glXSwapBuffers(disp, w);
-}
+  draw_quads (mi);
+  ss->redisplay_needed_p = False;
 
-void
-release_slideshow (ModeInfo *mi)
-{
-  if(sss != NULL) {
-    (void) free((void *) sss);
-    sss = NULL;
-  }
+  if (mi->fps_p) fps_2(mi);
 
-  FreeAllGL(MI);
+  glFinish();
+  glXSwapBuffers (MI_DISPLAY (mi), w);
 }
 
-#endif
+#endif /* USE_GL */