X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?p=xscreensaver;a=blobdiff_plain;f=hacks%2Fglx%2Fglslideshow.c;h=e3108980609f7573ac3f0ab657aed18d6adee44b;hp=e98864d4fe788e94aac9170a8396acc7444a9d22;hb=6b1c86cf395f59389e4ece4ea8f4bea2c332745b;hpb=40eacb5812ef7c0e3374fb139afbb4f5bc8bbfb5 diff --git a/hacks/glx/glslideshow.c b/hacks/glx/glslideshow.c index e98864d4..e3108980 100644 --- a/hacks/glx/glslideshow.c +++ b/hacks/glx/glslideshow.c @@ -1,14 +1,10 @@ -/* - * glslideshow - takes a snapshot of the screen and smoothly scans around - * in it +/* glslideshow, Copyright (c) 2003-2008 Jamie Zawinski + * 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 + * Originally written by Mike Oliphant (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 @@ -18,73 +14,159 @@ * 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 - - -# 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 -#include -#include -#include -#include + +# 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 */