/* xscreensaver, Copyright © 1992-2021 Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation. No representations are made about the suitability of this * software for any purpose. It is provided "as is" without express or * implied warranty. */ /* There are several different mechanisms here for fading the desktop to black, and then fading it back in again. - Colormaps: This only works on 8-bit displays, which basically haven't existed since the 90s. It takes the current colormap, makes a writable copy of it, and then animates the color cells to fade and unfade. - XF86 Gamma or RANDR Gamma: These do the fade by altering the brightness settings of the screen. This works on any system that has the "XF86 Video-Mode" extension (which is every modern system) AND ALSO has gamma support in the video driver. But it turns out that as of 2021, the Raspberry Pi HDMI video driver still does not support gamma. And there's no way to determine that the video driver lacks gamma support even though the extension exists. Since the Pi is probably the single most popular platform for running X11 on the desktop these days, that makes this method pretty much useless now. - SGI VC: Same as the above, but only works on SGI. - XSHM: This works by taking a screenshot and hacking the bits by hand. It's slow. Also, in order to fade in from black to the desktop (possibly hours after it faded out) it has to retain that first screenshot of the desktop to fade back to. But if the desktop had changed in the meantime, there will be a glitch at the end as it snaps from the screenshot to the new current reality. In summary, everything is terrible because X11 doesn't support alpha. The fade process goes like this: Screen saver activates: - Fade out: - Desktop is visible - Save screenshot for later - Map invisible temp windows - Fade from desktop to black - Erase saver windows to black and raise them - Destroy temp windows Screen saver deactivates: - Fade out: - Saver graphics are visible - Map invisible temp windows - Do not save a screenshot - Fade from graphics to black - Erase saver windows to black and raise them - Destroy temp windows - Fade in: - Screen is black - Map invisible temp windows - Do not save a screenshot - Unmap saver windows - Fade from black to saved screenshot - Destroy temp windows */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #ifdef HAVE_JWXYZ # include "jwxyz.h" #else /* real X11 */ # include # include # include # include #endif /* !HAVE_JWXYZ */ #include "blurb.h" #include "visual.h" #include "usleep.h" #include "fade.h" #include "xshm.h" #include "atoms.h" #include "clientmsg.h" #include "xmu.h" /* Since gamma fading doesn't work on the Raspberry Pi, probably the single most popular desktop Linux system these days, let's not use this fade method even if the extension exists (which it does). */ #undef HAVE_XF86VMODE_GAMMA /* I'm not sure that the RANDR fade method brings anything to the party that the XF86 method does See below. */ #undef HAVE_RANDR_12 #define HAVE_XINPUT2 1 /* Mandatory */ #ifdef HAVE_XINPUT2 # include # include "xinput.h" #endif typedef struct { int nscreens; Pixmap *screenshots; } fade_state; /* #### There's a bunch of duplicated code in the back half of the four _fade and _whack functions that could probably be combined. */ #ifdef HAVE_SGI_VC_EXTENSION static int sgi_gamma_fade (XtAppContext, Display *, Window *wins, int count, double secs, Bool out_p); #endif #ifdef HAVE_XF86VMODE_GAMMA static int xf86_gamma_fade (XtAppContext, Display *, Window *wins, int count, double secs, Bool out_p); #endif #ifdef HAVE_RANDR_12 static int randr_gamma_fade (XtAppContext, Display *, Window *wins, int count, double secs, Bool out_p); #endif static int colormap_fade (XtAppContext, Display *, Window *wins, int count, double secs, Bool out_p, Bool from_desktop_p); static int xshm_fade (XtAppContext, Display *, Window *wins, int count, double secs, Bool out_p, Bool from_desktop_p, fade_state *); 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)); } #ifdef HAVE_XINPUT2 static int xi_opcode = -1; #endif /* Closure arg points to a Bool saying whether motion events count. Motion aborts fade-out, but only clicks and keys abort fade-in. */ static Bool user_event_p (Display *dpy, XEvent *event, XPointer arg) { Bool motion_p = *((Bool *) arg); switch (event->xany.type) { case KeyPress: case ButtonPress: return True; break; case MotionNotify: if (motion_p) return True; break; # ifdef HAVE_XINPUT2 case GenericEvent: { XIRawEvent *re; if (event->xcookie.extension != xi_opcode) return False; if (! event->xcookie.data) XGetEventData (dpy, &event->xcookie); if (! event->xcookie.data) return False; re = event->xcookie.data; if (re->evtype == XI_RawKeyPress || re->evtype == XI_RawButtonPress) return True; else if (motion_p && re->evtype == XI_RawMotion) return True; /* Calling XFreeEventData here is bad news */ } break; # endif /* HAVE_XINPUT2 */ default: break; } return False; } static Bool user_active_p (XtAppContext app, Display *dpy, Bool fade_out_p) { XEvent event; XtInputMask m; Bool motion_p = fade_out_p; /* Motion aborts fade-out, not fade-in. */ motion_p = False; /* Naah, never abort on motion only */ # ifdef HAVE_XINPUT2 if (xi_opcode == -1) { Bool ov = verbose_p; xi_opcode = 0; /* only init once */ verbose_p = False; /* xscreensaver already printed this */ init_xinput (dpy, &xi_opcode); verbose_p = ov; } # endif m = XtAppPending (app); if (m & ~XtIMXEvent) { /* Process timers and signals only, don't block. */ if (verbose_p > 1) fprintf (stderr, "%s: Xt pending %ld\n", blurb(), m); XtAppProcessEvent (app, m); } /* If there is user activity, bug out. (Bug out on keypresses or mouse presses, but not motion, and not release events. Bugging out on motion made the unfade hack be totally useless, I think.) */ if (XCheckIfEvent (dpy, &event, &user_event_p, (XPointer) &motion_p)) { if (verbose_p > 1) { XIRawEvent *re = 0; if (event.xany.type == GenericEvent && !event.xcookie.data) { XGetEventData (dpy, &event.xcookie); re = event.xcookie.data; } fprintf (stderr, "%s: user input %d %d\n", blurb(), event.xany.type, (re ? re->evtype : -1)); } XPutBackEvent (dpy, &event); return True; } return False; } static void flush_user_input (Display *dpy) { XEvent event; Bool motion_p = True; while (XCheckIfEvent (dpy, &event, &user_event_p, (XPointer) &motion_p)) if (verbose_p > 1) { XIRawEvent *re = 0; if (event.xany.type == GenericEvent && !event.xcookie.data) { XGetEventData (dpy, &event.xcookie); re = event.xcookie.data; } fprintf (stderr, "%s: flushed user event %d %d\n", blurb(), event.xany.type, (re ? re->evtype : -1)); } } /* This bullshit is needed because the VidMode and SHM extensions don't work on remote displays. */ static Bool error_handler_hit_p = False; static int ignore_all_errors_ehandler (Display *dpy, XErrorEvent *error) { if (verbose_p > 1) XmuPrintDefaultErrorMessage (dpy, error, stderr); error_handler_hit_p = True; return 0; } /* Returns true if canceled by user activity. */ Bool fade_screens (XtAppContext app, Display *dpy, Window *saver_windows, int nwindows, double seconds, Bool out_p, Bool from_desktop_p, void **closureP) { int status = False; fade_state *state = 0; if (nwindows <= 0) abort(); if (!saver_windows) abort(); if (!closureP) abort(); state = (fade_state *) *closureP; if (!state) { state = (fade_state *) calloc (1, sizeof (*state)); *closureP = state; } if (from_desktop_p && !out_p) abort(); /* Fading in from desktop makes no sense */ if (out_p) flush_user_input (dpy); /* Flush at start of cycle */ # ifdef HAVE_SGI_VC_EXTENSION /* First try to do it by fading the gamma in an SGI-specific way... */ status = sgi_gamma_fade (app, dpy, saver_windows, nwindows, seconds, out_p); if (status == 0 || status == 1) return status; /* faded, possibly canceled */ # endif # ifdef HAVE_RANDR_12 /* Then try to do it by fading the gamma in an RANDR-specific way... */ status = randr_gamma_fade (app, dpy, saver_windows, nwindows, seconds, out_p); if (status == 0 || status == 1) return status; /* faded, possibly canceled */ # endif # ifdef HAVE_XF86VMODE_GAMMA /* Then try to do it by fading the gamma in an XFree86-specific way... */ status = xf86_gamma_fade(app, dpy, saver_windows, nwindows, seconds, out_p); if (status == 0 || status == 1) return status; /* faded, possibly canceled */ # endif if (has_writable_cells (DefaultScreenOfDisplay (dpy), DefaultVisual (dpy, 0))) { /* Do it the old-fashioned way, which only really worked on 8-bit displays. */ status = colormap_fade (app, dpy, saver_windows, nwindows, seconds, out_p, from_desktop_p); if (status == 0 || status == 1) return status; /* faded, possibly canceled */ } /* Else do it the hard way, by hacking a screenshot. */ status = xshm_fade (app, dpy, saver_windows, nwindows, seconds, out_p, from_desktop_p, state); status = (status ? True : False); return status; } /**************************************************************************** Colormap fading ****************************************************************************/ /* The business with `cmaps_per_screen' is to fake out the SGI 8-bit video hardware, which is capable of installing multiple (4) colormaps simultaneously. We have to install multiple copies of the same set of colors in order to fill up all the available slots in the hardware color lookup table, so we install an extra N colormaps per screen to make sure that all screens really go black. I'm told that this trick also works with XInside's AcceleratedX when using the Matrox Millennium card (which also allows multiple PseudoColor and TrueColor visuals to co-exist and display properly at the same time.) This trick works ok on the 24-bit Indy video hardware, but doesn't work at all on the O2 24-bit hardware. I guess the higher-end hardware is too "good" for this to work (dammit.) So... I figured out the "right" way to do this on SGIs, which is to ramp the monitor's gamma down to 0. That's what is implemented in sgi_gamma_fade(), so we use that if we can. Returns: 0: faded normally 1: canceled by user activity -1: unable to fade because the extension isn't supported. */ static int colormap_fade (XtAppContext app, Display *dpy, Window *saver_windows, int nwindows, double seconds, Bool out_p, Bool from_desktop_p) { int status = -1; Colormap *window_cmaps = 0; int i, j, k; int cmaps_per_screen = 5; int nscreens = ScreenCount(dpy); int ncmaps = nscreens * cmaps_per_screen; Colormap *fade_cmaps = 0; Bool installed = False; int total_ncolors; XColor *orig_colors, *current_colors, *screen_colors, *orig_screen_colors; int screen; window_cmaps = (Colormap *) calloc(sizeof(Colormap), nwindows); if (!window_cmaps) abort(); for (screen = 0; screen < nwindows; screen++) { XWindowAttributes xgwa; XGetWindowAttributes (dpy, saver_windows[screen], &xgwa); window_cmaps[screen] = xgwa.colormap; } error_handler_hit_p = False; if (verbose_p > 1) fprintf (stderr, "%s: colormap fade %s\n", blurb(), (out_p ? "out" : "in")); total_ncolors = 0; for (i = 0; i < nscreens; i++) total_ncolors += CellsOfScreen (ScreenOfDisplay(dpy, i)); orig_colors = (XColor *) calloc(sizeof(XColor), total_ncolors); current_colors = (XColor *) calloc(sizeof(XColor), total_ncolors); /* Get the contents of the colormap we are fading from or to. */ screen_colors = orig_colors; for (i = 0; i < nscreens; i++) { Screen *sc = ScreenOfDisplay (dpy, i); int ncolors = CellsOfScreen (sc); Colormap cmap = (from_desktop_p || !out_p ? DefaultColormapOfScreen(sc) : window_cmaps[i]); for (j = 0; j < ncolors; j++) screen_colors[j].pixel = j; XQueryColors (dpy, cmap, screen_colors, ncolors); screen_colors += ncolors; } memcpy (current_colors, orig_colors, total_ncolors * sizeof (XColor)); /* Make the writable colormaps (we keep these around and reuse them.) */ if (!fade_cmaps) { fade_cmaps = (Colormap *) calloc(sizeof(Colormap), ncmaps); for (i = 0; i < nscreens; i++) { Visual *v = DefaultVisual(dpy, i); Screen *s = ScreenOfDisplay(dpy, i); if (has_writable_cells (s, v)) for (j = 0; j < cmaps_per_screen; j++) fade_cmaps[(i * cmaps_per_screen) + j] = XCreateColormap (dpy, RootWindowOfScreen (s), v, AllocAll); } } /* Run the animation at the maximum frame rate in the time allotted. */ { double start_time = double_time(); double end_time = start_time + seconds; double prev = 0; double now; int frames = 0; double max = 1/60.0; /* max FPS */ while ((now = double_time()) < end_time) { double ratio = (end_time - now) / seconds; if (!out_p) ratio = 1-ratio; /* For each screen, compute the current value of each color... */ orig_screen_colors = orig_colors; screen_colors = current_colors; for (j = 0; j < nscreens; j++) { int ncolors = CellsOfScreen (ScreenOfDisplay (dpy, j)); for (k = 0; k < ncolors; k++) { /* This doesn't take into account the relative luminance of the RGB components (0.299, 0.587, and 0.114 at gamma 2.2) but the difference is imperceptible for this application... */ screen_colors[k].red = orig_screen_colors[k].red * ratio; screen_colors[k].green = orig_screen_colors[k].green * ratio; screen_colors[k].blue = orig_screen_colors[k].blue * ratio; } screen_colors += ncolors; orig_screen_colors += ncolors; } /* Put the colors into the maps... */ screen_colors = current_colors; for (j = 0; j < nscreens; j++) { int ncolors = CellsOfScreen (ScreenOfDisplay (dpy, j)); for (k = 0; k < cmaps_per_screen; k++) { Colormap c = fade_cmaps[j * cmaps_per_screen + k]; if (c) XStoreColors (dpy, c, screen_colors, ncolors); } screen_colors += ncolors; } /* Put the maps on the screens, and then take the windows off the screen. (only need to do this the first time through the loop.) */ if (!installed) { for (j = 0; j < ncmaps; j++) if (fade_cmaps[j]) XInstallColormap (dpy, fade_cmaps[j]); installed = True; if (!out_p) for (j = 0; j < nwindows; j++) { XUnmapWindow (dpy, saver_windows[j]); XClearWindow (dpy, saver_windows[j]); } } XSync (dpy, False); if (error_handler_hit_p) goto DONE; if (user_active_p (app, dpy, out_p)) { status = 1; /* user activity status code */ goto DONE; } frames++; if (now < prev + max) usleep (1000000 * (prev + max - now)); prev = now; } if (verbose_p > 1) fprintf (stderr, "%s: %.0f FPS\n", blurb(), frames / (now - start_time)); } status = 0; /* completed fade with no user activity */ DONE: if (orig_colors) free (orig_colors); if (current_colors) free (current_colors); /* If we've been given windows to raise after blackout, raise them before releasing the colormaps. */ if (out_p) { for (i = 0; i < nwindows; i++) { XClearWindow (dpy, saver_windows[i]); XMapRaised (dpy, saver_windows[i]); } XSync(dpy, False); } /* Now put the target maps back. If we're fading out, use the given cmap (or the default cmap, if none.) If we're fading in, always use the default cmap. */ for (i = 0; i < nscreens; i++) { Colormap cmap = window_cmaps[i]; if (!cmap || !out_p) cmap = DefaultColormap(dpy, i); XInstallColormap (dpy, cmap); } /* The fade (in or out) is complete, so we don't need the black maps on stage any more. */ for (i = 0; i < ncmaps; i++) if (fade_cmaps[i]) { XUninstallColormap(dpy, fade_cmaps[i]); XFreeColormap(dpy, fade_cmaps[i]); fade_cmaps[i] = 0; } free (window_cmaps); free(fade_cmaps); fade_cmaps = 0; if (error_handler_hit_p) status = -1; return status; } /**************************************************************************** SGI gamma fading ****************************************************************************/ #ifdef HAVE_SGI_VC_EXTENSION # include struct screen_sgi_gamma_info { int gamma_map; /* ??? always using 0 */ int nred, ngreen, nblue; unsigned short *red1, *green1, *blue1; unsigned short *red2, *green2, *blue2; int gamma_size; int gamma_precision; Bool alpha_p; }; static void sgi_whack_gamma(Display *dpy, int screen, struct screen_sgi_gamma_info *info, float ratio); /* Returns: 0: faded normally 1: canceled by user activity -1: unable to fade because the extension isn't supported. */ static int sgi_gamma_fade (XtAppContext app, Display *dpy, Window *saver_windows, int nwindows, double seconds, Bool out_p) { int nscreens = ScreenCount(dpy); struct timeval then, now; int i, screen; int status = -1; struct screen_sgi_gamma_info *info = (struct screen_sgi_gamma_info *) calloc(nscreens, sizeof(*info)); if (verbose_p > 1) fprintf (stderr, "%s: sgi fade %s\n", blurb(), (out_p ? "out" : "in")); /* Get the current gamma maps for all screens. Bug out and return -1 if we can't get them for some screen. */ for (screen = 0; screen < nscreens; screen++) { if (!XSGIvcQueryGammaMap(dpy, screen, info[screen].gamma_map, &info[screen].gamma_size, &info[screen].gamma_precision, &info[screen].alpha_p)) goto FAIL; if (!XSGIvcQueryGammaColors(dpy, screen, info[screen].gamma_map, XSGIVC_COMPONENT_RED, &info[screen].nred, &info[screen].red1)) goto FAIL; if (! XSGIvcQueryGammaColors(dpy, screen, info[screen].gamma_map, XSGIVC_COMPONENT_GREEN, &info[screen].ngreen, &info[screen].green1)) goto FAIL; if (!XSGIvcQueryGammaColors(dpy, screen, info[screen].gamma_map, XSGIVC_COMPONENT_BLUE, &info[screen].nblue, &info[screen].blue1)) goto FAIL; if (info[screen].gamma_precision == 8) /* Scale it up to 16 bits. */ { int j; for(j = 0; j < info[screen].nred; j++) info[screen].red1[j] = ((info[screen].red1[j] << 8) | info[screen].red1[j]); for(j = 0; j < info[screen].ngreen; j++) info[screen].green1[j] = ((info[screen].green1[j] << 8) | info[screen].green1[j]); for(j = 0; j < info[screen].nblue; j++) info[screen].blue1[j] = ((info[screen].blue1[j] << 8) | info[screen].blue1[j]); } info[screen].red2 = (unsigned short *) malloc(sizeof(*info[screen].red2) * (info[screen].nred+1)); info[screen].green2 = (unsigned short *) malloc(sizeof(*info[screen].green2) * (info[screen].ngreen+1)); info[screen].blue2 = (unsigned short *) malloc(sizeof(*info[screen].blue2) * (info[screen].nblue+1)); } #ifdef GETTIMEOFDAY_TWO_ARGS gettimeofday(&then, &tzp); #else gettimeofday(&then); #endif /* If we're fading in (from black), then first crank the gamma all the way down to 0, then take the windows off the screen. */ if (!out_p) { for (screen = 0; screen < nscreens; screen++) sgi_whack_gamma(dpy, screen, &info[screen], 0.0); for (screen = 0; screen < nwindows; screen++) { XUnmapWindow (dpy, saver_windows[screen]); XClearWindow (dpy, saver_windows[screen]); XSync(dpy, False); } } /* Run the animation at the maximum frame rate in the time allotted. */ { double start_time = double_time(); double end_time = start_time + seconds; double prev = 0; double now; int frames = 0; double max = 1/60.0; /* max FPS */ while ((now = double_time()) < end_time) { double ratio = (end_time - now) / seconds; if (!out_p) ratio = 1-ratio; for (screen = 0; screen < nwindows; screen++) sgi_whack_gamma (dpy, screen, &info[screen], ratio); if (error_handler_hit_p) goto FAIL; if (user_active_p (app, dpy, out_p)) { status = 1; /* user activity status code */ goto DONE; } frames++; if (now < prev + max) usleep (1000000 * (prev + max - now)); prev = now; } if (verbose_p > 1) fprintf (stderr, "%s: %.0f FPS\n", blurb(), frames / (now - start_time)); } status = 0; /* completed fade with no user activity */ DONE: if (out_p) { for (screen = 0; screen < nwindows; screen++) { XClearWindow (dpy, saver_windows[screen]); XMapRaised (dpy, saver_windows[screen]); } XSync(dpy, False); } /* I can't explain this; without this delay, we get a flicker. I suppose there's some lossage with stale bits being in the hardware frame buffer or something, and this delay gives it time to flush out. This sucks! */ usleep(100000); /* 1/10th second */ for (screen = 0; screen < nscreens; screen++) sgi_whack_gamma(dpy, screen, &info[screen], 1.0); XSync(dpy, False); FAIL: for (screen = 0; screen < nscreens; screen++) { if (info[screen].red1) free (info[screen].red1); if (info[screen].green1) free (info[screen].green1); if (info[screen].blue1) free (info[screen].blue1); if (info[screen].red2) free (info[screen].red2); if (info[screen].green2) free (info[screen].green2); if (info[screen].blue2) free (info[screen].blue2); } free(info); if (verbose_p > 1 && status) fprintf (stderr, "%s: SGI fade %s failed\n", blurb(), (out_p ? "out" : "in")); if (error_handler_hit_p) status = -1; return status; } static void sgi_whack_gamma (Display *dpy, int screen, struct screen_sgi_gamma_info *info, float ratio) { int k; if (ratio < 0) ratio = 0; if (ratio > 1) ratio = 1; for (k = 0; k < info->gamma_size; k++) { info->red2[k] = info->red1[k] * ratio; info->green2[k] = info->green1[k] * ratio; info->blue2[k] = info->blue1[k] * ratio; } XSGIvcStoreGammaColors16(dpy, screen, info->gamma_map, info->nred, XSGIVC_MComponentRed, info->red2); XSGIvcStoreGammaColors16(dpy, screen, info->gamma_map, info->ngreen, XSGIVC_MComponentGreen, info->green2); XSGIvcStoreGammaColors16(dpy, screen, info->gamma_map, info->nblue, XSGIVC_MComponentBlue, info->blue2); XSync(dpy, False); } #endif /* HAVE_SGI_VC_EXTENSION */ /**************************************************************************** XFree86 gamma fading ****************************************************************************/ #ifdef HAVE_XF86VMODE_GAMMA #include typedef struct { XF86VidModeGamma vmg; int size; unsigned short *r, *g, *b; } xf86_gamma_info; static int xf86_check_gamma_extension (Display *dpy); static Bool xf86_whack_gamma (Display *dpy, int screen, xf86_gamma_info *ginfo, float ratio); /* Returns: 0: faded normally 1: canceled by user activity -1: unable to fade because the extension isn't supported. */ static int xf86_gamma_fade (XtAppContext app, Display *dpy, Window *saver_windows, int nwindows, double seconds, Bool out_p) { int nscreens = ScreenCount(dpy); int screen; int status = -1; xf86_gamma_info *info = 0; static int ext_ok = -1; if (verbose_p > 1) fprintf (stderr, "%s: xf86 fade %s\n", blurb(), (out_p ? "out" : "in")); /* Only probe the extension once: the answer isn't going to change. */ if (ext_ok == -1) ext_ok = xf86_check_gamma_extension (dpy); /* If this server doesn't have the gamma extension, bug out. */ if (ext_ok == 0) goto FAIL; # ifndef HAVE_XF86VMODE_GAMMA_RAMP if (ext_ok == 2) ext_ok = 1; /* server is newer than client! */ # endif info = (xf86_gamma_info *) calloc(nscreens, sizeof(*info)); /* Get the current gamma maps for all screens. Bug out and return -1 if we can't get them for some screen. */ for (screen = 0; screen < nscreens; screen++) { if (ext_ok == 1) /* only have gamma parameter, not ramps. */ { if (!XF86VidModeGetGamma(dpy, screen, &info[screen].vmg)) goto FAIL; } # ifdef HAVE_XF86VMODE_GAMMA_RAMP else if (ext_ok == 2) /* have ramps */ { if (!XF86VidModeGetGammaRampSize(dpy, screen, &info[screen].size)) goto FAIL; if (info[screen].size <= 0) goto FAIL; info[screen].r = (unsigned short *) calloc(info[screen].size, sizeof(unsigned short)); info[screen].g = (unsigned short *) calloc(info[screen].size, sizeof(unsigned short)); info[screen].b = (unsigned short *) calloc(info[screen].size, sizeof(unsigned short)); if (!(info[screen].r && info[screen].g && info[screen].b)) goto FAIL; # if 0 if (verbose_p > 1 && out_p) { int i; fprintf (stderr, "%s: initial gamma ramps, size %d:\n", blurb(), info[screen].size); fprintf (stderr, "%s: R:", blurb()); for (i = 0; i < info[screen].size; i++) fprintf (stderr, " %d", info[screen].r[i]); fprintf (stderr, "\n%s: G:", blurb()); for (i = 0; i < info[screen].size; i++) fprintf (stderr, " %d", info[screen].g[i]); fprintf (stderr, "\n%s: B:", blurb()); for (i = 0; i < info[screen].size; i++) fprintf (stderr, " %d", info[screen].b[i]); fprintf (stderr, "\n"); } # endif /* 0 */ if (!XF86VidModeGetGammaRamp(dpy, screen, info[screen].size, info[screen].r, info[screen].g, info[screen].b)) goto FAIL; } # endif /* HAVE_XF86VMODE_GAMMA_RAMP */ else abort(); } /* If we're fading in (from black), then first crank the gamma all the way down to 0, then take the windows off the screen. */ if (!out_p) { for (screen = 0; screen < nscreens; screen++) xf86_whack_gamma(dpy, screen, &info[screen], 0.0); for (screen = 0; screen < nwindows; screen++) { XUnmapWindow (dpy, saver_windows[screen]); XClearWindow (dpy, saver_windows[screen]); XSync(dpy, False); } } /* Run the animation at the maximum frame rate in the time allotted. */ { double start_time = double_time(); double end_time = start_time + seconds; double prev = 0; double now; int frames = 0; double max = 1/60.0; /* max FPS */ while ((now = double_time()) < end_time) { double ratio = (end_time - now) / seconds; if (!out_p) ratio = 1-ratio; for (screen = 0; screen < nscreens; screen++) xf86_whack_gamma (dpy, screen, &info[screen], ratio); if (error_handler_hit_p) goto FAIL; if (user_active_p (app, dpy, out_p)) { status = 1; /* user activity status code */ goto DONE; } frames++; if (now < prev + max) usleep (1000000 * (prev + max - now)); prev = now; } if (verbose_p > 1) fprintf (stderr, "%s: %.0f FPS\n", blurb(), frames / (now - start_time)); } status = 0; /* completed fade with no user activity */ DONE: if (out_p) { for (screen = 0; screen < nwindows; screen++) { XClearWindow (dpy, saver_windows[screen]); XMapRaised (dpy, saver_windows[screen]); } XSync(dpy, False); } /* I can't explain this; without this delay, we get a flicker. I suppose there's some lossage with stale bits being in the hardware frame buffer or something, and this delay gives it time to flush out. This sucks! */ usleep(100000); /* 1/10th second */ for (screen = 0; screen < nscreens; screen++) xf86_whack_gamma(dpy, screen, &info[screen], 1.0); XSync(dpy, False); FAIL: if (info) { for (screen = 0; screen < nscreens; screen++) { if (info[screen].r) free(info[screen].r); if (info[screen].g) free(info[screen].g); if (info[screen].b) free(info[screen].b); } free(info); } if (verbose_p > 1 && status) fprintf (stderr, "%s: xf86 fade %s failed\n", blurb(), (out_p ? "out" : "in")); if (error_handler_hit_p) status = -1; return status; } static Bool safe_XF86VidModeQueryVersion (Display *dpy, int *majP, int *minP) { Bool result; XErrorHandler old_handler; XSync (dpy, False); error_handler_hit_p = False; old_handler = XSetErrorHandler (ignore_all_errors_ehandler); result = XF86VidModeQueryVersion (dpy, majP, minP); XSync (dpy, False); XSetErrorHandler (old_handler); XSync (dpy, False); return (error_handler_hit_p ? False : result); } /* VidModeExtension version 2.0 or better is needed to do gamma. 2.0 added gamma values; 2.1 added gamma ramps. */ # define XF86_VIDMODE_GAMMA_MIN_MAJOR 2 # define XF86_VIDMODE_GAMMA_MIN_MINOR 0 # define XF86_VIDMODE_GAMMA_RAMP_MIN_MAJOR 2 # define XF86_VIDMODE_GAMMA_RAMP_MIN_MINOR 1 /* Returns 0 if gamma fading not available; 1 if only gamma value setting is available; 2 if gamma ramps are available. */ static int xf86_check_gamma_extension (Display *dpy) { int event, error, major, minor; if (!XF86VidModeQueryExtension (dpy, &event, &error)) return 0; /* display doesn't have the extension. */ if (!safe_XF86VidModeQueryVersion (dpy, &major, &minor)) return 0; /* unable to get version number? */ if (major < XF86_VIDMODE_GAMMA_MIN_MAJOR || (major == XF86_VIDMODE_GAMMA_MIN_MAJOR && minor < XF86_VIDMODE_GAMMA_MIN_MINOR)) return 0; /* extension is too old for gamma. */ if (major < XF86_VIDMODE_GAMMA_RAMP_MIN_MAJOR || (major == XF86_VIDMODE_GAMMA_RAMP_MIN_MAJOR && minor < XF86_VIDMODE_GAMMA_RAMP_MIN_MINOR)) return 1; /* extension is too old for gamma ramps. */ /* Copacetic */ return 2; } /* XFree doesn't let you set gamma to a value smaller than this. Apparently they didn't anticipate the trick I'm doing here... */ #define XF86_MIN_GAMMA 0.1 static Bool xf86_whack_gamma(Display *dpy, int screen, xf86_gamma_info *info, float ratio) { XErrorHandler old_handler; XSync (dpy, False); error_handler_hit_p = False; old_handler = XSetErrorHandler (ignore_all_errors_ehandler); if (ratio < 0) ratio = 0; if (ratio > 1) ratio = 1; if (info->size == 0) /* we only have a gamma number, not a ramp. */ { XF86VidModeGamma g2; g2.red = info->vmg.red * ratio; g2.green = info->vmg.green * ratio; g2.blue = info->vmg.blue * ratio; # ifdef XF86_MIN_GAMMA if (g2.red < XF86_MIN_GAMMA) g2.red = XF86_MIN_GAMMA; if (g2.green < XF86_MIN_GAMMA) g2.green = XF86_MIN_GAMMA; if (g2.blue < XF86_MIN_GAMMA) g2.blue = XF86_MIN_GAMMA; # endif if (! XF86VidModeSetGamma (dpy, screen, &g2)) return -1; } else { # ifdef HAVE_XF86VMODE_GAMMA_RAMP unsigned short *r, *g, *b; int i; r = (unsigned short *) malloc(info->size * sizeof(unsigned short)); g = (unsigned short *) malloc(info->size * sizeof(unsigned short)); b = (unsigned short *) malloc(info->size * sizeof(unsigned short)); for (i = 0; i < info->size; i++) { r[i] = info->r[i] * ratio; g[i] = info->g[i] * ratio; b[i] = info->b[i] * ratio; } if (! XF86VidModeSetGammaRamp(dpy, screen, info->size, r, g, b)) return -1; free (r); free (g); free (b); # else /* !HAVE_XF86VMODE_GAMMA_RAMP */ abort(); # endif /* !HAVE_XF86VMODE_GAMMA_RAMP */ } XSync (dpy, False); XSetErrorHandler (old_handler); XSync (dpy, False); return status; } #endif /* HAVE_XF86VMODE_GAMMA */ /**************************************************************************** RANDR gamma fading **************************************************************************** Dec 2020: I noticed that gamma fading was not working on a Raspberry Pi with Raspbian 10.6, and wrote this under the hypothesis that the XF86 gamma fade code was failing and maybe the RANDR version would work better. Then I discovered that gamma simply isn't supported by the Raspberry Pi HDMI driver: https://github.com/raspberrypi/firmware/issues/1274 I should have tried this first and seen it not work: xrandr --output HDMI-1 --brightness .1 Since I still don't have an answer to the question of whether the XF86 gamma fading method works on modern Linux systems that also have RANDR, I'm leaving this new code turned off for now, as it is largely untested. The new code would be useful if: A) The XF86 way doesn't work but the RANDR way does, or B) There exist systems that have RANDR but do not have XF86. But until Raspberry Pi supports gamma, both gamma methods fail to work for far too many users for them to be used in XScreenSaver. */ #ifdef HAVE_RANDR_12 # include typedef struct { RRCrtc crtc; Bool enabled_p; XRRCrtcGamma *gamma; } randr_gamma_info; static int randr_check_gamma_extension (Display *dpy) { int event, error, major, minor; if (! XRRQueryExtension (dpy, &event, &error)) return 0; if (! XRRQueryVersion (dpy, &major, &minor)) { if (verbose_p > 1) fprintf (stderr, "%s: no randr ext\n", blurb()); return 0; } /* Reject if < 1.5. It's possible that 1.2 - 1.4 work, but untested. */ if (major < 1 || (major == 1 && minor < 5)) { if (verbose_p > 1) fprintf (stderr, "%s: randr ext only version %d.%d\n", blurb(), major, minor); return 0; } return 1; } static void randr_whack_gamma (Display *dpy, int screen, randr_gamma_info *ginfo, float ratio); /* Returns: 0: faded normally 1: canceled by user activity -1: unable to fade because the extension isn't supported. */ static int randr_gamma_fade (XtAppContext app, Display *dpy, Window *saver_windows, int nwindows, double seconds, Bool out_p) { int xsc = ScreenCount (dpy); int nscreens = 0; int j, screen; int status = -1; randr_gamma_info *info = 0; static int ext_ok = -1; if (verbose_p > 1) fprintf (stderr, "%s: randr fade %s\n", blurb(), (out_p ? "out" : "in")); /* Only probe the extension once: the answer isn't going to change. */ if (ext_ok == -1) ext_ok = randr_check_gamma_extension (dpy); /* If this server doesn't have the RANDR extension, bug out. */ if (ext_ok == 0) goto FAIL; /* Add up the virtual screens on each X screen. */ for (screen = 0; screen < xsc; screen++) { XRRScreenResources *res = XRRGetScreenResources (dpy, RootWindow (dpy, screen)); nscreens += res->noutput; XRRFreeScreenResources (res); } if (nscreens <= 0) goto FAIL; info = (randr_gamma_info *) calloc(nscreens, sizeof(*info)); /* Get the current gamma maps for all screens. Bug out and return -1 if we can't get them for some screen. */ for (screen = 0, j = 0; screen < xsc; screen++) { XRRScreenResources *res = XRRGetScreenResources (dpy, RootWindow (dpy, screen)); int k; for (k = 0; k < res->noutput; k++, j++) { XRROutputInfo *rroi = XRRGetOutputInfo (dpy, res, res->outputs[j]); RRCrtc crtc = (rroi->crtc ? rroi->crtc : rroi->ncrtc ? rroi->crtcs[0] : 0); info[j].crtc = crtc; info[j].gamma = XRRGetCrtcGamma (dpy, crtc); /* #### is this test sufficient? */ info[j].enabled_p = (rroi->connection != RR_Disconnected); # if 0 if (verbose_p > 1 && out_p) { int m; fprintf (stderr, "%s: initial gamma ramps, size %d:\n", blurb(), info[j].gamma->size); fprintf (stderr, "%s: R:", blurb()); for (m = 0; m < info[j].gamma->size; m++) fprintf (stderr, " %d", info[j].gamma->red[m]); fprintf (stderr, "\n%s: G:", blurb()); for (m = 0; m < info[j].gamma->size; m++) fprintf (stderr, " %d", info[j].gamma->green[m]); fprintf (stderr, "\n%s: B:", blurb()); for (m = 0; m < info[j].gamma->size; m++) fprintf (stderr, " %d", info[j].gamma->blue[m]); fprintf (stderr, "\n"); } # endif /* 0 */ XRRFreeOutputInfo (rroi); } XRRFreeScreenResources (res); } /* If we're fading in (from black), then first crank the gamma all the way down to 0, then take the windows off the screen. */ if (!out_p) { for (screen = 0; screen < nscreens; screen++) randr_whack_gamma(dpy, screen, &info[screen], 0.0); for (screen = 0; screen < nwindows; screen++) { XUnmapWindow (dpy, saver_windows[screen]); XClearWindow (dpy, saver_windows[screen]); XSync(dpy, False); } } /* Run the animation at the maximum frame rate in the time allotted. */ { double start_time = double_time(); double end_time = start_time + seconds; double prev = 0; double now; int frames = 0; double max = 1/60.0; /* max FPS */ while ((now = double_time()) < end_time) { double ratio = (end_time - now) / seconds; if (!out_p) ratio = 1-ratio; for (screen = 0; screen < nwindows; screen++) { if (!info[screen].enabled_p) continue; randr_whack_gamma (dpy, screen, &info[screen], ratio); } if (error_handler_hit_p) goto FAIL; if (user_active_p (app, dpy, out_p)) { status = 1; /* user activity status code */ goto DONE; } frames++; if (now < prev + max) usleep (1000000 * (prev + max - now)); prev = now; } if (verbose_p > 1) fprintf (stderr, "%s: %.0f FPS\n", blurb(), frames / (now - start_time)); } status = 0; /* completed fade with no user activity */ DONE: if (out_p) { for (screen = 0; screen < nwindows; screen++) { XClearWindow (dpy, saver_windows[screen]); XMapRaised (dpy, saver_windows[screen]); } XSync(dpy, False); } /* I can't explain this; without this delay, we get a flicker. I suppose there's some lossage with stale bits being in the hardware frame buffer or something, and this delay gives it time to flush out. This sucks! */ /* #### That comment was about XF86, not verified with randr. */ usleep(100000); /* 1/10th second */ for (screen = 0; screen < nscreens; screen++) randr_whack_gamma (dpy, screen, &info[screen], 1.0); XSync(dpy, False); FAIL: if (info) { for (screen = 0; screen < nscreens; screen++) { if (info[screen].gamma) XRRFreeGamma (info[screen].gamma); } free(info); } if (verbose_p > 1 && status) fprintf (stderr, "%s: randr fade %s failed\n", blurb(), (out_p ? "out" : "in")); return status; } static void randr_whack_gamma (Display *dpy, int screen, randr_gamma_info *info, float ratio) { XErrorHandler old_handler; XRRCrtcGamma *g2; int i; XSync (dpy, False); error_handler_hit_p = False; old_handler = XSetErrorHandler (ignore_all_errors_ehandler); if (ratio < 0) ratio = 0; if (ratio > 1) ratio = 1; g2 = XRRAllocGamma (info->gamma->size); for (i = 0; i < info->gamma->size; i++) { g2->red[i] = ratio * info->gamma->red[i]; g2->green[i] = ratio * info->gamma->green[i]; g2->blue[i] = ratio * info->gamma->blue[i]; } XRRSetCrtcGamma (dpy, info->crtc, g2); XRRFreeGamma (g2); XSync (dpy, False); XSetErrorHandler (old_handler); XSync (dpy, False); return 0; } #endif /* HAVE_RANDR_12 */ /**************************************************************************** XSHM screen-shot fading ****************************************************************************/ typedef struct { GC gc; Window window; Pixmap screenshot; XImage *src, *intermediate; } xshm_fade_info; static int xshm_whack (Display *, XShmSegmentInfo *, xshm_fade_info *, float ratio); /* Returns: 0: faded normally 1: canceled by user activity -1: unknown error */ static int xshm_fade (XtAppContext app, Display *dpy, Window *saver_windows, int nwindows, double seconds, Bool out_p, Bool from_desktop_p, fade_state *state) { int screen; int status = -1; xshm_fade_info *info = 0; XShmSegmentInfo shm_info; Window saver_window = 0; XErrorHandler old_handler = 0; XSync (dpy, False); old_handler = XSetErrorHandler (ignore_all_errors_ehandler); error_handler_hit_p = False; if (verbose_p > 1) fprintf (stderr, "%s: SHM fade %s\n", blurb(), (out_p ? "out" : "in")); info = (xshm_fade_info *) calloc(nwindows, sizeof(*info)); if (!info) goto FAIL; saver_window = find_screensaver_window (dpy, 0); if (!saver_window) goto FAIL; /* Retrieve a screenshot of the area covered by each window. Windows might not be mapped. Bug out and return -1 if we can't get one for some screen. */ for (screen = 0; screen < nwindows; screen++) { XWindowAttributes xgwa; Window root; XGCValues gcv; GC gc; unsigned long attrmask = 0; XSetWindowAttributes attrs; XGetWindowAttributes (dpy, saver_windows[screen], &xgwa); root = RootWindowOfScreen (xgwa.screen); info[screen].src = create_xshm_image (dpy, xgwa.visual, xgwa.depth, ZPixmap, &shm_info, xgwa.width, xgwa.height); if (!info[screen].src) goto FAIL; info[screen].intermediate = create_xshm_image (dpy, xgwa.visual, xgwa.depth, ZPixmap, &shm_info, xgwa.width, xgwa.height); if (!info[screen].intermediate) goto FAIL; if (!out_p) { /* If we are fading in, retrieve the saved screenshot from before we faded out. */ if (state->nscreens <= screen) goto FAIL; info[screen].screenshot = state->screenshots[screen]; } else { /* Create a pixmap and grab a screenshot into it. */ info[screen].screenshot = XCreatePixmap (dpy, root, xgwa.width, xgwa.height, xgwa.depth); if (!info[screen].screenshot) goto FAIL; gcv.function = GXcopy; gcv.subwindow_mode = IncludeInferiors; gc = XCreateGC (dpy, root, GCFunction | GCSubwindowMode, &gcv); XCopyArea (dpy, root, info[screen].screenshot, gc, xgwa.x, xgwa.y, xgwa.width, xgwa.height, 0, 0); XFreeGC (dpy, gc); } /* Create the fader window for the animation. */ attrmask = CWOverrideRedirect; attrs.override_redirect = True; info[screen].window = XCreateWindow (dpy, root, xgwa.x, xgwa.y, xgwa.width, xgwa.height, xgwa.border_width, xgwa.depth, InputOutput, xgwa.visual, attrmask, &attrs); if (!info[screen].window) goto FAIL; /* XSelectInput (dpy, info[screen].window, KeyPressMask | ButtonPressMask); */ /* Copy the screenshot pixmap to the source image */ if (! get_xshm_image (dpy, info[screen].screenshot, info[screen].src, 0, 0, ~0L, &shm_info)) goto FAIL; gcv.function = GXcopy; info[screen].gc = XCreateGC (dpy, info[screen].window, GCFunction, &gcv); } /* If we're fading out from the desktop, save our screen shots for later use. But not if we're fading out from the savers to black. In that case we don't want to overwrite the desktop screenshot with the current screenshot which is of the final frames of the just-killed graphics hacks. */ if (from_desktop_p) { if (!out_p) abort(); for (screen = 0; screen < state->nscreens; screen++) if (state->screenshots[screen]) XFreePixmap (dpy, state->screenshots[screen]); if (state->screenshots) free (state->screenshots); state->nscreens = nwindows; state->screenshots = calloc (nwindows, sizeof(*state->screenshots)); if (!state->screenshots) state->nscreens = 0; for (screen = 0; screen < state->nscreens; screen++) state->screenshots[screen] = info[screen].screenshot; } for (screen = 0; screen < nwindows; screen++) { if (out_p) /* Copy the screenshot to the fader window */ XSetWindowBackgroundPixmap (dpy, info[screen].window, info[screen].screenshot); else { XSetWindowBackgroundPixmap (dpy, info[screen].window, None); XSetWindowBackground (dpy, info[screen].window, BlackPixel (dpy, 0)); } XMapRaised (dpy, info[screen].window); /* Now that we have mapped the screenshot on the fader windows, take the saver windows off the screen. */ if (out_p) { XUnmapWindow (dpy, saver_windows[screen]); XClearWindow (dpy, saver_windows[screen]); } } /* Run the animation at the maximum frame rate in the time allotted. */ { double start_time = double_time(); double end_time = start_time + seconds; double prev = 0; double now; int frames = 0; double max = 1/60.0; /* max FPS */ while ((now = double_time()) < end_time) { double ratio = (end_time - now) / seconds; if (!out_p) ratio = 1-ratio; for (screen = 0; screen < nwindows; screen++) if (xshm_whack (dpy, &shm_info, &info[screen], ratio)) goto FAIL; if (error_handler_hit_p) goto FAIL; if (user_active_p (app, dpy, out_p)) { status = 1; /* user activity status code */ goto DONE; } frames++; if (now < prev + max) usleep (1000000 * (prev + max - now)); prev = now; } if (verbose_p > 1) fprintf (stderr, "%s: %.0f FPS\n", blurb(), frames / (now - start_time)); } status = 0; /* completed fade with no user activity */ DONE: /* If we're fading out, we have completed the transition from what was on the screen to black, using our fader windows. Now raise the saver windows and take the fader windows off the screen. Since they're both black, that will be imperceptible. */ if (out_p) { for (screen = 0; screen < nwindows; screen++) { XClearWindow (dpy, saver_windows[screen]); XMapRaised (dpy, saver_windows[screen]); if (info[screen].window) XUnmapWindow (dpy, info[screen].window); } } XSync (dpy, False); FAIL: /* After fading in, take the saver windows off the screen before destroying the occluding screenshot windows. */ if (!out_p) { for (screen = 0; screen < nwindows; screen++) { XUnmapWindow (dpy, saver_windows[screen]); XClearWindow (dpy, saver_windows[screen]); } } if (info) { for (screen = 0; screen < nwindows; screen++) { if (info[screen].src) destroy_xshm_image (dpy, info[screen].src, &shm_info); if (info[screen].intermediate) destroy_xshm_image (dpy, info[screen].intermediate, &shm_info); if (info[screen].window) XDestroyWindow (dpy, info[screen].window); if (info[screen].gc) XFreeGC (dpy, info[screen].gc); } free (info); } /* If fading in, delete the screenshot pixmaps, and the list of them. */ if (!out_p && saver_window) { for (screen = 0; screen < state->nscreens; screen++) if (state->screenshots[screen]) XFreePixmap (dpy, state->screenshots[screen]); if (state->screenshots) free (state->screenshots); state->nscreens = 0; state->screenshots = 0; } XSync (dpy, False); XSetErrorHandler (old_handler); if (error_handler_hit_p) status = -1; if (verbose_p > 1 && status) fprintf (stderr, "%s: SHM fade %s failed\n", blurb(), (out_p ? "out" : "in")); return status; } static int xshm_whack (Display *dpy, XShmSegmentInfo *shm_info, xshm_fade_info *info, float ratio) { unsigned char *inbits = (unsigned char *) info->src->data; unsigned char *outbits = (unsigned char *) info->intermediate->data; unsigned char *end = (outbits + info->intermediate->bytes_per_line * info->intermediate->height); unsigned char ramp[256]; int i; XSync (dpy, False); if (ratio < 0) ratio = 0; if (ratio > 1) ratio = 1; for (i = 0; i < sizeof(ramp); i++) ramp[i] = i * ratio; while (outbits < end) *outbits++ = ramp[*inbits++]; put_xshm_image (dpy, info->window, info->gc, info->intermediate, 0, 0, 0, 0, info->intermediate->width, info->intermediate->height, shm_info); XSync (dpy, False); return 0; }