Discussion:
[cairo] Problem in cairo + xcb where alpha blending is cumulative (becoming more opaque with each render)
Ryan Flannery
2018-09-26 05:51:57 UTC
Permalink
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();
}
Ryan Flannery
2018-09-27 04:28:08 UTC
Permalink
From what I can see, my issue boils down to CAIRO_OPERATOR_SOURCE not
behaving as I would expect, with the use of an xcb backed surface.
Namely, after doing a cairo_pop_group_to_source(), if I
cairo_set_operator(cairo,
CAIRO_OPERATOR_SOURCE), based on the docs I would expect the alpha channel
to be copied from the intermediate pattern/surface as-is to the
destination, with no blending. I actually observe this when the destination
is *not* an xcb derived surface (I'm playing with a png one now, using the
sample program I included).

With an xcb backed surface like in my original program, setting the
operator to CAIRO_OPERATOR_SOURCE results in a solid black background.

Specifically, my case is this:

// BEGIN render loop
cairo_push_group(cairo);
cairo_set_source_rgba(cairo, 0.8, 0, 0, 0.1);

// draw (lots of) stuff

// END render loop
cairo_pop_group_to_source(cairo);
cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
cairo_paint(cairo);
// reset operator to CAIRO_OPERATOR_OVER
xcb_flush(xcon);
sleep(1);

I've been looking into cairo-xcb / xcb specifics regarding this, but any
cluesticks that could be provided would be appreciated.

Cheers,
-Ryan


Hello,
Post by Ryan Flannery
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
My main render loop looks roughly like this (and I understand why this
// 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);
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
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.
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.
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
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
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
Uli Schlachter
2018-09-27 07:56:01 UTC
Permalink
Hi,
Post by Ryan Flannery
The raw transparency piece works fine and is
easy in cairo + xcb,
sorry to tell you this, but the problem is in the raw transparency part.
You only *think* that you have transparency. Also, "is easy" is the
total opposite of the truth. Again, sorry.

When a new window is mapped, its content is undefined. In practice this
means that the window contains whatever was visible at this position
before. Thus, you think you have transparency when you draw ontop of
this. However, this is wrong.

Try apply the attached patch move.patch. What this patch does is to wait
for the window to become visible and then it moves the window. You will
notice that together with the window, its content is moved. Thus, making
it obvious that your transparency does not work.

The black background that you see is the working case *if your window
had an alpha channel*. Because it does not have an alpha channel, you
only see black.

To create a transparent window in X11, first of all, the user has to be
using a compositing manager (compiz, compton, xcompmgr, ...). Without
this, there is no real transparency. The _NET_WM_CMs selection can be
used to figure out if a compositing manager is running.

Next, you need to create a window with a 32bit visual (and also tell
cairo that your window is using this visual). For this part, I refer you to:

https://stackoverflow.com/questions/3645632/how-to-create-a-window-with-a-bit-depth-of-32

The attached patch fix.patch gives me a window with either a black
background (if no compositing manager is running) or a transparent black
background (if a compositing manager is running; I used compton).

Cheers,
Uli
--
Sent from my Game Boy.
Ryan Flannery
2018-09-28 02:24:42 UTC
Permalink
Post by Uli Schlachter
Hi,
Post by Ryan Flannery
The raw transparency piece works fine and is
easy in cairo + xcb,
sorry to tell you this, but the problem is in the raw transparency part.
You only *think* that you have transparency. Also, "is easy" is the
total opposite of the truth. Again, sorry.
When a new window is mapped, its content is undefined. In practice this
means that the window contains whatever was visible at this position
before. Thus, you think you have transparency when you draw ontop of
this. However, this is wrong.
THANK YOU!

I was digging into a totally different path. After reading this, I dove
into what you described and now have a good handle on the issue. I can now
detect if it's supported and apply the alpha blending only when it is. Also
I learned what compositing window managers are and now have these sweet
transparent urxvt terminals :D (though I doubt I'll keep them).

Many thanks again. You saved me many more hours and headaches before I
would have arrived at this.
Try apply the attached patch move.patch. What this patch does is to wait
Post by Uli Schlachter
for the window to become visible and then it moves the window. You will
notice that together with the window, its content is moved. Thus, making
it obvious that your transparency does not work.
The black background that you see is the working case *if your window
had an alpha channel*. Because it does not have an alpha channel, you
only see black.
To create a transparent window in X11, first of all, the user has to be
using a compositing manager (compiz, compton, xcompmgr, ...). Without
this, there is no real transparency. The _NET_WM_CMs selection can be
used to figure out if a compositing manager is running.
Next, you need to create a window with a 32bit visual (and also tell
https://stackoverflow.com/questions/3645632/how-to-
create-a-window-with-a-bit-depth-of-32
The attached patch fix.patch gives me a window with either a black
background (if no compositing manager is running) or a transparent black
background (if a compositing manager is running; I used compton).
Thanks for the patch and stackoverflow link. That both solved the issue in
my example program and also cleaned up a couple things, like applying
OVERRIDE_REDIRECT when I create the window rather than later.

Many thanks,
-Ryan

Loading...