Skip to content

fix: use 32-bit visual to avoid alpha channel lost#469

Merged
BLumia merged 1 commit into
linuxdeepin:masterfrom
BLumia:wine-tray
Jun 17, 2026
Merged

fix: use 32-bit visual to avoid alpha channel lost#469
BLumia merged 1 commit into
linuxdeepin:masterfrom
BLumia:wine-tray

Conversation

@BLumia

@BLumia BLumia commented Jun 16, 2026

Copy link
Copy Markdown
Member

Compability with wine tray so its background won't be black.

PMS: BUG-360339

Compability with wine tray so its background won't be black.

PMS: BUG-360339
Log:
@BLumia BLumia requested review from tsic404 and yixinshark June 16, 2026 09:39

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @BLumia, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@BLumia

BLumia commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

可使用下方的测试程序验证:

Details
/* tray_demo.c  
 * 编译: gcc tray_demo.c -o tray_demo -lX11 -lXrender
 * 运行: ./tray_demo
 *
 * 说明: 必须使用 XRender (XRenderComposite) 绘制 ARGB 内容,
 *       而不是 XPutImage。在 XWayland rootless 模式下,当 ARGB
 *       (depth=32) 托盘窗口被 reparent 到 depth=24 的托盘宿主窗口
 *       之后,使用 XPutImage 会导致 alpha 通道被丢弃(变为 0xFF),
 *       从而显示出黑色背景。XRender 走的是 Picture/PictOp 路径,
 *       会正确保留 alpha 通道。
 */  
#include <X11/Xlib.h>  
#include <X11/Xatom.h>  
#include <X11/Xutil.h>  
#include <X11/extensions/Xrender.h>
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>   
  
#define SYSTEM_TRAY_REQUEST_DOCK  0  
  
/* 找到当前屏幕的托盘宿主窗口 (_NET_SYSTEM_TRAY_S<n>) */  
static Window get_systray_owner(Display *dpy)  
{  
    char name[32];  
    snprintf(name, sizeof(name), "_NET_SYSTEM_TRAY_S%d", DefaultScreen(dpy));  
    Atom sel = XInternAtom(dpy, name, False);  
    return XGetSelectionOwner(dpy, sel);  
}  
  
/* 查找 32-bit TrueColor (ARGB) Visual */  
static int find_argb_visual(Display *dpy, XVisualInfo *out)  
{  
    XVisualInfo templ;  
    templ.screen = DefaultScreen(dpy);  
    templ.depth  = 32;  
    templ.class  = TrueColor;  
    int n = 0;  
    XVisualInfo *list = XGetVisualInfo(dpy,  
        VisualScreenMask | VisualDepthMask | VisualClassMask, &templ, &n);  
    if (!list || n == 0) return 0;  
    *out = list[0];  
    XFree(list);  
    return 1;  
}   
  
/* 使用 XRender 将预乘 ARGB 缓冲区绘制到窗口
 * 关键点: 必须通过 PictOpSrc 把 ARGB 像素合成到窗口的 Picture 上,
 *         而不能使用 XPutImage 直接写窗口(后者在 XWayland rootless
 *         下对被 reparent 的子窗口会丢失 alpha)。 */
static void draw_icon(Display *dpy, Window win, XRenderPictFormat *fmt,
                      Visual *vis, int w, int h)  
{
    /* 在临时 Pixmap 上构建预乘 ARGB 像素 */
    Pixmap pm = XCreatePixmap(dpy, win, w, h, 32);

    XRenderPictureAttributes pa;
    pa.subwindow_mode = IncludeInferiors;
    Picture pm_pic = XRenderCreatePicture(dpy, pm, fmt, CPSubwindowMode, &pa);

    /* 清空为完全透明 */
    XRenderColor clear = { .red = 0, .green = 0, .blue = 0, .alpha = 0 };
    XRenderFillRectangle(dpy, PictOpSrc, pm_pic, &clear, 0, 0, w, h);

    unsigned int *buf = calloc(w * h, sizeof(unsigned int));  
    if (!buf) {  
        XRenderFreePicture(dpy, pm_pic);
        XFreePixmap(dpy, pm);
        return;
    }   
  
    int cx = w / 2, cy = h / 2, r = w / 2 - 2;  
  
    for (int y = 0; y < h; y++) {  
        for (int x = 0; x < w; x++) {  
            int dx = x - cx, dy = y - cy;  
            if (dx*dx + dy*dy <= r*r) {  
                /*  
                 * 预乘 Alpha (pre-multiplied alpha):  
                 *   A=0xC0 (75% 不透明), R=0xFF, G=0x40, B=0x00  
                 *   存储格式: 0xAARRGGBB (little-endian 下即内存顺序 BB GG RR AA)  
                 *  
                 * 注意: X11 ARGB Visual 要求像素值为预乘 alpha,  
                 *       即 R_stored = R * A / 255,否则颜色会偏暗。  
                 */  
                unsigned char a = 0xC0;  
                unsigned char rv = (unsigned char)(0xFF * a / 255);  
                unsigned char gv = (unsigned char)(0x40 * a / 255);  
                unsigned char bv = 0;  
                buf[y * w + x] = ((unsigned int)a  << 24)  
                                | ((unsigned int)rv << 16)  
                                | ((unsigned int)gv <<  8)  
                                |  (unsigned int)bv;  
            }  
            /* else: alpha=0, 完全透明,保持 calloc 的 0 值 */  
        }  
    }  
  
    /* 把缓冲区写入 Pixmap (这一步是写 Pixmap 不是 Window, 不会丢 alpha) */
    GC gc = XCreateGC(dpy, pm, 0, NULL);
    XImage *img = XCreateImage(dpy, vis, 32, ZPixmap, 0,  
                                (char *)buf, w, h, 32, w * 4);  
    if (img) {
        XPutImage(dpy, pm, gc, img, 0, 0, 0, 0, w, h);
        img->data = NULL;   /* 阻止 XDestroyImage 释放我们的 buf */  
        XDestroyImage(img);  
    }
    XFreeGC(dpy, gc);
    free(buf);

    /* 使用 XRender 把 Pixmap 合成到 Window 上 */
    Picture win_pic = XRenderCreatePicture(dpy, win, fmt, 0, NULL);
    XRenderComposite(dpy, PictOpSrc, pm_pic, None, win_pic,
                     0, 0, 0, 0, 0, 0, w, h);
    XRenderFreePicture(dpy, win_pic);
    XRenderFreePicture(dpy, pm_pic);
    XFreePixmap(dpy, pm);
}   
  
int main(void)  
{  
    Display *dpy = XOpenDisplay(NULL);  
    if (!dpy) { fprintf(stderr, "Cannot open display\n"); return 1; }  
  
    int screen = DefaultScreen(dpy);  
    Window root = RootWindow(dpy, screen);  
  
    /* 1. 找托盘宿主 */  
    Window tray = get_systray_owner(dpy);  
    if (!tray) { fprintf(stderr, "No system tray found\n"); return 1; }  
    printf("Systray owner: 0x%lx\n", tray);  
  
    /* 2. 获取 ARGB Visual(透明的关键) */  
    XVisualInfo vinfo;  
    if (!find_argb_visual(dpy, &vinfo)) {  
        fprintf(stderr, "No 32-bit ARGB visual available\n");  
        return 1;  
    }  
    printf("ARGB visual id: 0x%lx\n", vinfo.visualid);  
  
    /* 获取该 Visual 对应的 XRenderPictFormat, 用于后续 Picture 创建 */
    XRenderPictFormat *fmt = XRenderFindVisualFormat(dpy, vinfo.visual);
    if (!fmt) {
        fprintf(stderr, "No XRender format for visual 0x%lx\n", vinfo.visualid);
        return 1;
    }

    /* 3. 为 ARGB Visual 创建专用 Colormap  
     *    (深度与 root 不同,不能用 DefaultColormap) */  
    Colormap cmap = XCreateColormap(dpy, root, vinfo.visual, AllocNone);  
  
    /* 4. 创建图标窗口  
     *    - background_pixel=0: 初始全透明  
     *    - border_pixel=0:     必须显式设置,否则 X 会用默认深度的颜色  
     *    - CWColormap:         绑定上面创建的 colormap */  
    int icon_w = 22, icon_h = 22;  
    XSetWindowAttributes wa;  
    wa.colormap      = cmap;  
    wa.border_pixel  = 0;  
    wa.background_pixel = 0;  
    wa.event_mask    = ExposureMask | ButtonPressMask | StructureNotifyMask;  
  
    Window icon_win = XCreateWindow(dpy, root,  
                                     0, 0, icon_w, icon_h, 0,  
                                     vinfo.depth, InputOutput, vinfo.visual,  
                                     CWColormap | CWBorderPixel |  
                                     CWBackPixel | CWEventMask,  
                                     &wa);  
  
    XStoreName(dpy, icon_win, "TrayDemo");  
  
    /* 5. 监听托盘宿主的销毁事件 */  
    XSelectInput(dpy, tray, StructureNotifyMask);  
  
    /* 6. 发送 SYSTEM_TRAY_REQUEST_DOCK 请求嵌入  
     *    协议与 Wine 的 X11DRV_SystrayDockInsert 完全一致 */  
    Atom opcode = XInternAtom(dpy, "_NET_SYSTEM_TRAY_OPCODE", False);  
    XEvent ev;  
    memset(&ev, 0, sizeof(ev));  
    ev.xclient.type         = ClientMessage;  
    ev.xclient.window       = tray;  
    ev.xclient.message_type = opcode;  
    ev.xclient.format       = 32;  
    ev.xclient.data.l[0]    = CurrentTime;  
    ev.xclient.data.l[1]    = SYSTEM_TRAY_REQUEST_DOCK;  
    ev.xclient.data.l[2]    = icon_win;  
    XSendEvent(dpy, tray, False, NoEventMask, &ev);  
    XFlush(dpy);  
    printf("Docked, icon window: 0x%lx\n", icon_win);  
  
    /* 7. 事件循环 */  
    while (1) {  
        XNextEvent(dpy, &ev);  
  
        switch (ev.type) {  
        case Expose:  
            if (ev.xexpose.count == 0)  
                draw_icon(dpy, icon_win, fmt, vinfo.visual, icon_w, icon_h);  
            break;  
  
        case ButtonPress:  
            printf("Tray icon clicked (button %d)\n", ev.xbutton.button);  
            break;  
  
        case DestroyNotify:  
            if (ev.xdestroywindow.window == tray) {  
                printf("Systray gone, exiting\n");  
                goto done;  
            }  
            break;  
        }  
    }  
  
done:  
    XDestroyWindow(dpy, icon_win);  
    XFreeColormap(dpy, cmap);  
    XCloseDisplay(dpy);  
    return 0;  
}

...或者...使用 wine。

另外,nm-applet 没问题是因为它走的 GDK 的方案实际会使用 XRenderComposite。可以用下面的示例“模拟” nm-applet 的行为。

Details
/* tray_demo_v3.c
 * Same as tray_demo.c but uses XRender (XRenderFillRectangles) instead
 * of XPutImage to paint the icon. This is closer to how GTK/cairo draws.
 *
 * Build: gcc tray_demo_v3.c -o tray_demo_v3 -lX11 -lXrender
 */
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xrender.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define SYSTEM_TRAY_REQUEST_DOCK  0

static Window get_systray_owner(Display *dpy)
{
    char name[32];
    snprintf(name, sizeof(name), "_NET_SYSTEM_TRAY_S%d", DefaultScreen(dpy));
    Atom sel = XInternAtom(dpy, name, False);
    return XGetSelectionOwner(dpy, sel);
}

static int find_argb_visual(Display *dpy, XVisualInfo *out)
{
    XVisualInfo templ;
    templ.screen = DefaultScreen(dpy);
    templ.depth  = 32;
    templ.class  = TrueColor;
    int n = 0;
    XVisualInfo *list = XGetVisualInfo(dpy,
        VisualScreenMask | VisualDepthMask | VisualClassMask, &templ, &n);
    if (!list || n == 0) return 0;
    *out = list[0];
    XFree(list);
    return 1;
}

/* Build the same disk icon but into an ARGB XRenderPicture-backed Pixmap,
 * then composite onto the window. We pre-multiply alpha as required. */
static void draw_icon_render(Display *dpy, Window win, XRenderPictFormat *fmt,
                             Visual *vis, int w, int h)
{
    Pixmap pm = XCreatePixmap(dpy, win, w, h, 32);

    XRenderPictureAttributes pa;
    pa.subwindow_mode = IncludeInferiors;
    Picture src_pic = XRenderCreatePicture(dpy, pm, fmt, CPSubwindowMode, &pa);

    /* Clear pixmap to fully transparent */
    XRenderColor clear = { .red = 0, .green = 0, .blue = 0, .alpha = 0 };
    XRenderFillRectangle(dpy, PictOpSrc, src_pic, &clear, 0, 0, w, h);

    /* Build the disk in ARGB premultiplied form on a temp buffer, push via XImage */
    unsigned int *buf = calloc(w * h, sizeof(unsigned int));
    if (!buf) { XFreePixmap(dpy, pm); XRenderFreePicture(dpy, src_pic); return; }

    int cx = w / 2, cy = h / 2, r = w / 2 - 2;
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            int dx = x - cx, dy = y - cy;
            if (dx*dx + dy*dy <= r*r) {
                unsigned char a = 0xC0;
                unsigned char rv = (unsigned char)(0xFF * a / 255);
                unsigned char gv = (unsigned char)(0x40 * a / 255);
                unsigned char bv = 0;
                buf[y * w + x] = ((unsigned int)a  << 24)
                                | ((unsigned int)rv << 16)
                                | ((unsigned int)gv <<  8)
                                |  (unsigned int)bv;
            }
        }
    }

    GC gc = XCreateGC(dpy, pm, 0, NULL);
    XImage *img = XCreateImage(dpy, vis, 32, ZPixmap, 0, (char *)buf, w, h, 32, w * 4);
    if (img) {
        XPutImage(dpy, pm, gc, img, 0, 0, 0, 0, w, h);
        img->data = NULL;
        XDestroyImage(img);
    }
    XFreeGC(dpy, gc);
    free(buf);

    /* Composite pixmap onto window via RENDER (PictOpSrc to overwrite) */
    Picture dst_pic = XRenderCreatePicture(dpy, win, fmt, 0, NULL);
    XRenderComposite(dpy, PictOpSrc, src_pic, None, dst_pic, 0, 0, 0, 0, 0, 0, w, h);
    XRenderFreePicture(dpy, dst_pic);
    XRenderFreePicture(dpy, src_pic);
    XFreePixmap(dpy, pm);
}

int main(void)
{
    Display *dpy = XOpenDisplay(NULL);
    if (!dpy) { fprintf(stderr, "Cannot open display\n"); return 1; }

    int screen = DefaultScreen(dpy);
    Window root = RootWindow(dpy, screen);

    Window tray = get_systray_owner(dpy);
    if (!tray) { fprintf(stderr, "No system tray found\n"); return 1; }
    fprintf(stderr, "Systray owner: 0x%lx\n", tray);

    XVisualInfo vinfo;
    if (!find_argb_visual(dpy, &vinfo)) {
        fprintf(stderr, "No 32-bit ARGB visual available\n");
        return 1;
    }
    fprintf(stderr, "ARGB visual id: 0x%lx\n", vinfo.visualid);

    /* Find the XRenderPictFormat for this visual */
    XRenderPictFormat *fmt = XRenderFindVisualFormat(dpy, vinfo.visual);
    if (!fmt) {
        fprintf(stderr, "No Render format for visual 0x%lx\n", vinfo.visualid);
        return 1;
    }
    fprintf(stderr, "Render PictFormat: type=%d depth=%d direct=(a:%d/%d r:%d/%d g:%d/%d b:%d/%d)\n",
            fmt->type, fmt->depth,
            fmt->direct.alpha, fmt->direct.alphaMask,
            fmt->direct.red, fmt->direct.redMask,
            fmt->direct.green, fmt->direct.greenMask,
            fmt->direct.blue, fmt->direct.blueMask);

    Colormap cmap = XCreateColormap(dpy, root, vinfo.visual, AllocNone);

    int icon_w = 22, icon_h = 22;
    XSetWindowAttributes wa;
    wa.colormap      = cmap;
    wa.border_pixel  = 0;
    wa.background_pixel = 0;
    wa.event_mask    = ExposureMask | ButtonPressMask | StructureNotifyMask;

    Window icon_win = XCreateWindow(dpy, root,
                                     0, 0, icon_w, icon_h, 0,
                                     vinfo.depth, InputOutput, vinfo.visual,
                                     CWColormap | CWBorderPixel |
                                     CWBackPixel | CWEventMask,
                                     &wa);

    XStoreName(dpy, icon_win, "TrayDemoV3");

    XSelectInput(dpy, tray, StructureNotifyMask);

    Atom opcode = XInternAtom(dpy, "_NET_SYSTEM_TRAY_OPCODE", False);
    XEvent ev;
    memset(&ev, 0, sizeof(ev));
    ev.xclient.type         = ClientMessage;
    ev.xclient.window       = tray;
    ev.xclient.message_type = opcode;
    ev.xclient.format       = 32;
    ev.xclient.data.l[0]    = CurrentTime;
    ev.xclient.data.l[1]    = SYSTEM_TRAY_REQUEST_DOCK;
    ev.xclient.data.l[2]    = icon_win;
    XSendEvent(dpy, tray, False, NoEventMask, &ev);
    XFlush(dpy);
    fprintf(stderr, "Docked, icon window: 0x%lx\n", icon_win);

    while (1) {
        XNextEvent(dpy, &ev);

        switch (ev.type) {
        case Expose:
            if (ev.xexpose.count == 0)
                draw_icon_render(dpy, icon_win, fmt, vinfo.visual, icon_w, icon_h);
            break;

        case ButtonPress:
            printf("Tray icon clicked (button %d)\n", ev.xbutton.button);
            break;

        case DestroyNotify:
            if (ev.xdestroywindow.window == tray) {
                printf("Systray gone, exiting\n");
                goto done;
            }
            break;
        }
    }

done:
    XDestroyWindow(dpy, icon_win);
    XFreeColormap(dpy, cmap);
    XCloseDisplay(dpy);
    return 0;
}

@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

★ 总体评分:90分

■ 【总体评价】

代码精准修复了X11托盘图标在XWayland rootless环境下alpha通道丢失及半透明像素偏亮的问题
逻辑严密且注释详尽,仅因存在未使用变量及数组硬编码问题扣10分

■ 【详细分析】

  • 1.语法逻辑(基本正确)✓

getWindowVisualDepth使用QSharedPointer正确管理了XCB返回的指针内存,getX11WindowImageNonComposite中针对32位visual的预乘alpha处理逻辑严密,且在构造QImage后立即调用copy()避免了后续xcb_image_destroy导致的悬空指针。convertFromNative中增加的visualDepth != 32判断有效处理了窗口visual为24位但获取图像depth为32位的边界情况。
潜在问题:getX11WindowImageNonComposite函数中声明了uint8_t imageDepth = image->depth;但后续代码从未使用该变量,会导致编译器警告。
建议:删除未使用的imageDepth变量声明。

  • 2.代码质量(良好)✓

修复代码包含了高质量的注释,清晰解释了遵循freedesktop spec的原因以及XWayland下alpha丢失的根因。变量命名如containerVisualcontainerDepth表意明确。
潜在问题:initX11resources中使用uint32_t values[5]配合索引i++动态填充数组,这种写法在后续维护中如果增加新的mask标志但忘记扩大数组大小,极易引发栈缓冲区溢出。
建议:使用std::vector<uint32_t>替代C风格数组,或根据条件分支直接赋值固定偏移量的元素,避免动态索引带来的隐患。

  • 3.代码性能(无性能问题)✓

getWindowVisualDepth中的同步XCB请求及getX11WindowImageNonComposite中使用pixel()遍历像素,在托盘图标这种极小尺寸(通常不大于64x64)且低频触发的场景下,开销完全可忽略。查找32位visual的双重循环因系统depths数量极少而不存在性能瓶颈。
建议:无需优化。

  • 4.代码安全(存在0个安全漏洞)✓

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个
代码在内存管理上表现优秀,所有通过xcb_image_get获取的底层指针均在QImage拷贝后通过xcb_image_destroy安全释放。XCB请求返回的指针均通过智能指针或空指针检查保护,不存在内存泄漏或空指针解引用风险。X11客户端通信局限于本地会话,不存在命令注入或越权风险。

  • 建议:保持当前的内存管理范式。

■ 【改进建议代码示例】

// xembedprotocolhandler.cpp - initX11resources() 中容器窗口创建优化
// 移除动态索引,使用更安全的条件赋值方式
uint32_t values[5];
uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL |
                XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK;
values[0] = 0;        // back_pixel: ARGB 下 alpha=0,完全透明
values[1] = 0;        // border_pixel: 必须显式设置,否则用默认深度颜色
values[2] = true;     // bypass WM
values[3] = XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_ENTER_WINDOW;

if (containerDepth == 32) {
    mask |= XCB_CW_COLORMAP;
    values[4] = cmap;
}

// util.cpp - getX11WindowImageNonComposite() 中移除未使用变量
QImage Util::getX11WindowImageNonComposite(const xcb_window_t& window)
{
    QSize size = getX11WindowGeometry(window).size();
    if (size.isEmpty()) {
        return QImage();
    }

    xcb_image_t *image = xcb_image_get(m_x11connection, window, 0, 0, size.width(), size.height(), 0xFFFFFFFF, XCB_IMAGE_FORMAT_Z_PIXMAP);

    if (!image) {
        return QImage();
    }

    uint8_t visualDepth = getWindowVisualDepth(window);
    // 已移除未使用的 uint8_t imageDepth = image->depth;

    if (visualDepth != 32) {
        QImage result = convertFromNative(image, visualDepth);
        if (isTransparentImage(result)) {
            return QImage();
        }
        return result;
    }
    // ... 后续逻辑保持不变
}

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: BLumia, tsic404

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@BLumia BLumia merged commit 3c4092a into linuxdeepin:master Jun 17, 2026
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants