[cairo] Problem in cairo + xcb where alpha blending is cumulative (becoming more opaque with each render)

Ryan Flannery ryan.flannery at gmail.com
Wed Sep 26 05:51:57 UTC 2018


Hello,

I've recently been using cairo + xcb to build a new application. I've hit a
problem supporting transparency / alpha-blending, where I want to allow
users to set the background window with an alpha value and have the root
x11 window show through that. The raw transparency piece works fine and is
easy in cairo + xcb, but I'm struggling with my main render loop -- it
seems to add / accumulate the alpha values with each render, and doesn't
remove previously drawn content with alpha values. I have more details
below including a fully functioning prototype with my layout setup (also
below and in a gist), but here's what I've been trying after much
google'ing and searching the archives:

My main render loop looks roughly like this (and I understand why this
would accumulate opaqueness over time):

   // BEGIN render loop
   cairo_push_group(cairo);
   clear_background();

   // draw (lots of) stuff

   // END render loop
   cairo_pop_group_to_source(cairo);
   cairo_paint(cairo);
   xcb_flush(xcon);
   sleep(1);

where clear_background() is:

   cairo_set_source_rgba(cairo, 0.8, 0, 0, 0.1);
   cairo_paint(cairo);

With that loop, as-is, I can see why the alpha values are accumulating with
each render.

To compensate, I've tried the following based on the cairo site's
documentation, and browsing the archives, but with no success:

   1. cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); (and subsequent
resets to *_OVER)
      I've tried this before the cairo_paint() at the END of the loop, as
well as in the BEGIN loop (both before and after the cairo_push_group()).
      My understanding is that this copies the alpha from SOURCE as-is,
with no blending, but I'm not observing that in the results, regardless of
where I place it (and tweak the timing to confirm each step).
      Use of it *always* results in a black background, which makes me
think I have something larger out-of-whack.

   2. I've looked at other operators, but none seem applicable (and I've
tested most in various setups like the above).

   3. Using cairo_mask() / cairo_mask_surface() when using
cairo_pop_group() (the raw one, returning a cairo_pattern_t) to render
after the pop, but that also fails.

My ultimate goals are basically these:

   1. Have the window always reset to a consistent background of rgba =
0.8/0/0/0.1
   2. Don't 'accumulate' alpha artifacts from the widgets drawn in the loop
each time

My sample code below illustrates what I'm trying and my setup.
It draws a 200 x 200 pixel window (at x,y = 200,200).
It has an initial background of rgba = 0.8/0/0/0.1,
and a 50x50 pixel green square in the top left (rgba=0/1/0/0.5).
Every second it redraws, and moves the square towards the bottom right.

IDEALLY:
   1. The background is *always* rgba = 0.8/0/0/0.1
   2. Only one square (the most recent) is shown with rgba = 0/1/0/0.5

BUT what I observe (trying lots of variants):
   1. The background gets more opaque with each render, eventually becoming
solid
   2. The previous renders of the green square persist and aren't cleared

The code is below, along with build instructions, and available at:
   https://gist.github.com/ryanflannery/1649ce3dd45cb6e16088931262283386

Note the example includes a great deal of XCB setup, including logic to
handle tiling window managers (my use case). I doubt that's of interest
here, but I'm including it for completeness (but below the main cairo
logic).

Any feedback appreciated (on this or other conventions I have!)

Cheers,
-Ryan


/*
 * NOTE: This is a little long because I'm including all XCB setup I have
 * (including handling tiling window managers). It's below the main cairo
logic
 * for ease, but included for posterity. This is a subset of a larger
program
 * I'm working on.
 *
 * WHAT THIS IS:
 *    A sample program illustrating a problem I'm having with cairo/xcb,
where I
 *    want to have a window that supports transparency (alpha channel) for
the
 *    background color, and other components, with a regular re-draw cycle
 *    (every 1-second), but the alpha-channel portion of the render appears
 *    additive / cumulative. That is: every render increases the alpha
value,
 *    thus making it more opaque / less transparent every second, and
doesn't
 *    clear-out the previously rendered content.
 *
 *    I've read and tried a number of things with the cairo API (and xcb)
but
 *    cannot seem to figure it out.
 *
 * WHAT THIS PROGRAM DOES:
 *    It renders a 200 pixel by 200 pixel square, with a 1-pixel black
border,
 *    at the (200,200) (x,y) coordinate of an X11 display.
 *    It starts with a semi-transparent red background and a small seim-
 *    transparent green square drawn in the upper-left corner.
 *    Every second, it re-renders the display, re-drawing the backgrond and
 *    re-drawing the square slightly more to the lower-right corner
(advancing
 *    along the diagonal).
 *    The square is green pure green (r=0, g=1, b=0) with alpha = 0.5.
 *    The initial background color is r=0.8, g=0, b=0, alpha = 0.1.
 *
 * DESIRED END RESULT:
 *    1. The window ALWAYS has background r=0.8, g=0, b=0, a=0.1
 *    2. The green square is ERASED with each loop and only the most
recently
 *       drawn square is shown, with color r=0, g=1, b=0, a=0.5
 *
 * WHAT I OBSERVE:
 *    1. The first render is perfect
 *    2. Each subsequent render "adds" the alpha channel to the previous one
 *       - Thus the transparency is eliminated in a few iterations
 *    3. I have a good handle on *why* this is happening, but don't see how
 *       to correct it.
 *
 * THINGS I'VE TRIED:
 *    1. cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
 *       Called before the loop -and- the cairo_paint() after the
 *       cairo_pop_group_to_source() call (and reset it to *_OVER
afterwards)
 *    2. Moving the cairo_clear_background() call below to before (and
after)
 *       the cairo_push_group() call.
 *    3. In the #2 variant, wrapping that with calls to cairo_set_operator()
 *       with CAIRO_OPERATOR_SOURCE (and subsequent calls to reset to
_OVER).
 *    4. I've looked at other operators, but they don't seem applicable (and
 *       have tested most in many configurations).
 *    5. Also looked at using cairo_mask() / cairo_mask_surface() per
 *
https://stackoverflow.com/questions/34831744/draw-icon-with-transparency-in-xlib-and-cairo
 *    6. So much google'ing/duckduckgo'ing
 *
 * TO BUILD/RUN:
 *    compile:
 *       $(CC) cairo_example.c -c `pkg-config --cflags cairo` -o
cairo_example.o
 *    link:
 *       $(CC) cairo_example.o `pkg-config --libs cairo` -o cairo_example
 */

#include <err.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <xcb/xcb.h>
#include <xcb/xcb_icccm.h>
#include <cairo/cairo.h>
#include <cairo/cairo-xcb.h>

/*
 * XCB info. Full setup is included for posterity, but below main() for ease
 * of reading
 */
int               default_screen;
xcb_connection_t *xcon;
xcb_screen_t     *xscreen;
xcb_drawable_t   xwindow;
xcb_visualtype_t *xvisual;

void setup_xcb(); /* what actually does the xcb setup */

/* starting location & dimensions of window */
const int x = 200;
const int y = 200;
const int w = 200;
const int h = 200;

/* relevant cairo stuff */
cairo_t          *cairo;
cairo_surface_t  *surface;

/* effectively what i do to clear the background */
void
cairo_clear_background()
{
   cairo_set_source_rgba(cairo, 0.8, 0, 0, 0.1);
   cairo_paint(cairo);
}

int
main()
{
   /* xcb setup (all below main, for ease, but also posterity) */
   setup_xcb();

   /* cairo setup */
   surface = cairo_xcb_surface_create(
         xcon,
         xwindow,
         xvisual,
         w, h);

   cairo = cairo_create(surface);

   /* map window & first draw */
   xcb_map_window(xcon, xwindow);
   cairo_clear_background();
   xcb_flush(xcon);

   /* begin main draw loop */
   for (int i = 0; i < 11; i++) {

      /* START: create new group/buffer, set it's background  */
      cairo_push_group(cairo);
      cairo_clear_background();

      /* now do all my drawing... */
      cairo_set_source_rgba(cairo, 0, 1, 0, 0.5);
      cairo_rectangle(cairo,
            i * 10 + 10,   /* x */
            i * 10 + 10,   /* y */
            50, 50);       /* w, h */
      cairo_fill(cairo);

      /* END: pop group/buffer (exposing it) and render */
      cairo_pop_group_to_source(cairo);
      /*cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); XXX Makes the
background black (?) */
      cairo_paint(cairo);
      /*cairo_set_operator(cairo, CAIRO_OPERATOR_OVER); only done if th
eprevious cairo_set_operator() is done */
      xcb_flush(xcon);
      sleep(1);
   }

   /* cleanup */
   cairo_surface_destroy(surface);
   cairo_destroy(cairo);
   xcb_disconnect(xcon);

   return 0;
}

/* XCB Setup Stuff */

xcb_screen_t*
get_xscreen(xcb_connection_t *c, int screen)
{
   xcb_screen_iterator_t i = xcb_setup_roots_iterator(xcb_get_setup(c));;
   for (; i.rem; --screen, xcb_screen_next(&i)) {
      if (0 == screen)
         return i.data;
   }
   return NULL;
}

xcb_visualtype_t*
get_xvisual(xcb_screen_t *screen)
{
   xcb_depth_iterator_t i = xcb_screen_allowed_depths_iterator(screen);
   for (; i.rem; xcb_depth_next(&i)) {
      xcb_visualtype_iterator_t vi;
      vi = xcb_depth_visuals_iterator(i.data);
      for (; vi.rem; xcb_visualtype_next(&vi)) {
         if (screen->root_visual == vi.data->visual_id) {
            return vi.data;
         }
      }
   }

   return NULL;
}

/*
 * XCB setup to handle tiling window managers - this can (probably) be
safely
 * ignored. I'm only incuding it for completeness' sake.
 */
void
wm_hints()
{
   enum {
      NET_WM_XINFO_TYPE,
      NET_WM_XINFO_TYPE_DOCK,
      NET_WM_DESKTOP,
      NET_WM_STRUT_PARTIAL,
      NET_WM_STRUT,
      NET_WM_STATE,
      NET_WM_STATE_STICKY,
      NET_WM_STATE_ABOVE
   };

   static const char *atoms[] = {
      "_NET_WM_XINFO_TYPE",
      "_NET_WM_XINFO_TYPE_DOCK",
      "_NET_WM_DESKTOP",
      "_NET_WM_STRUT_PARTIAL",
      "_NET_WM_STRUT",
      "_NET_WM_STATE",
      "_NET_WM_STATE_STICKY",
      "_NET_WM_STATE_ABOVE"
   };
   const size_t natoms = sizeof(atoms)/sizeof(char*);

   xcb_intern_atom_cookie_t xcookies[natoms];
   xcb_atom_t               xatoms[natoms];
   xcb_intern_atom_reply_t *xatom_reply;
   size_t i;

   for (i = 0; i < natoms; i++)
      xcookies[i] = xcb_intern_atom(xcon, 0, strlen(atoms[i]), atoms[i]);

   for (i = 0; i < natoms; i++) {
      xatom_reply = xcb_intern_atom_reply(xcon, xcookies[i], NULL);
      if (!xatom_reply)
         errx(1, "%s: xcb atom reply failed for %s", __FUNCTION__,
atoms[i]);

      xatoms[i] = xatom_reply->atom;
      free(xatom_reply);
   }

   enum {
      left,           right,
      top,            bottom,
      left_start_y,   left_end_y,
      right_start_y,  right_end_y,
      top_start_x,    top_end_x,
      bottom_start_x, bottom_end_x
   };
   unsigned long struts[12] = { 0 };

   struts[top] = y + h;
   struts[top_start_x] = x;
   struts[top_end_x] = x + w;

xcb_change_property(xcon, XCB_PROP_MODE_REPLACE, xwindow,
         xatoms[NET_WM_XINFO_TYPE], XCB_ATOM_ATOM, 32, 1,
         &xatoms[NET_WM_XINFO_TYPE_DOCK]);
xcb_change_property(xcon, XCB_PROP_MODE_APPEND, xwindow,
         xatoms[NET_WM_STATE], XCB_ATOM_ATOM, 32, 2,
         &xatoms[NET_WM_STATE_STICKY]);
xcb_change_property(xcon, XCB_PROP_MODE_REPLACE, xwindow,
         xatoms[NET_WM_DESKTOP], XCB_ATOM_CARDINAL, 32, 1,
         (const uint32_t []){ -1 } );
xcb_change_property(xcon, XCB_PROP_MODE_REPLACE, xwindow,
         xatoms[NET_WM_STRUT_PARTIAL], XCB_ATOM_CARDINAL, 32, 12, struts);
xcb_change_property(xcon, XCB_PROP_MODE_REPLACE, xwindow,
         xatoms[NET_WM_STRUT], XCB_ATOM_CARDINAL, 32, 4, struts);

   /* remove window from window manager tabbing */
   const uint32_t val[] = { 1 };
   xcb_change_window_attributes(xcon, xwindow,
         XCB_CW_OVERRIDE_REDIRECT, val);
}

void
setup_xcb()
{
   xcon = xcb_connect(NULL, &default_screen);
   if (xcb_connection_has_error(xcon)) {
      xcb_disconnect(xcon);
      errx(1, "Failed to establish connection to X");
   }

   if (NULL == (xscreen = get_xscreen(xcon, default_screen)))
      errx(1, "Failed to retrieve X screen");

   if (NULL == (xvisual = get_xvisual(xscreen)))
      errx(1, "Failed to retrieve X visual context");

   static uint32_t valwin[2] = {
      XCB_NONE,
      XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS
   };

   xwindow = xcb_generate_id(xcon);
   xcb_create_window(
         xcon,
         XCB_COPY_FROM_PARENT,
         xwindow,
         xscreen->root,
         x, y,
         y, h,
         1,    /* border width */
         XCB_WINDOW_CLASS_INPUT_OUTPUT,
         xscreen->root_visual,
         XCB_CW_EVENT_MASK | XCB_CW_BACK_PIXMAP,
         valwin);

   wm_hints();
}
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.cairographics.org/archives/cairo/attachments/20180926/c745a6a9/attachment-0001.html>


More information about the cairo mailing list