From a93aba600b1c5d019b680b9f4ff3fa85d5d43a60 Mon Sep 17 00:00:00 2001 From: allusive-dev Date: Tue, 19 Sep 2023 17:47:33 +1000 Subject: Fixed broken files/code and other errors --- src/backend/backend.c | 494 +++++++++++ src/backend/backend.h | 290 +++++++ src/backend/backend_common.c | 480 ++++++++++ src/backend/backend_common.h | 78 ++ src/backend/driver.c | 82 ++ src/backend/driver.h | 62 ++ src/backend/dummy/dummy.c | 174 ++++ src/backend/gl/gl_common.c | 1922 +++++++++++++++++++++++++++++++++++++++++ src/backend/gl/gl_common.h | 220 +++++ src/backend/gl/glx.c | 648 ++++++++++++++ src/backend/gl/glx.h | 77 ++ src/backend/meson.build | 7 + src/backend/xrender/xrender.c | 779 +++++++++++++++++ 13 files changed, 5313 insertions(+) create mode 100644 src/backend/backend.c create mode 100644 src/backend/backend.h create mode 100644 src/backend/backend_common.c create mode 100644 src/backend/backend_common.h create mode 100644 src/backend/driver.c create mode 100644 src/backend/driver.h create mode 100644 src/backend/dummy/dummy.c create mode 100644 src/backend/gl/gl_common.c create mode 100644 src/backend/gl/gl_common.h create mode 100644 src/backend/gl/glx.c create mode 100644 src/backend/gl/glx.h create mode 100644 src/backend/meson.build create mode 100644 src/backend/xrender/xrender.c (limited to 'src/backend') diff --git a/src/backend/backend.c b/src/backend/backend.c new file mode 100644 index 0000000..b0e562a --- /dev/null +++ b/src/backend/backend.c @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#include +#include + +#include "backend/backend.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "log.h" +#include "region.h" +#include "types.h" +#include "win.h" +#include "x.h" + +extern struct backend_operations xrender_ops, dummy_ops; +#ifdef CONFIG_OPENGL +extern struct backend_operations glx_ops; +#endif + +struct backend_operations *backend_list[NUM_BKEND] = { + [BKEND_XRENDER] = &xrender_ops, + [BKEND_DUMMY] = &dummy_ops, +#ifdef CONFIG_OPENGL + [BKEND_GLX] = &glx_ops, +#endif +}; + +/** + * @param all_damage if true ignore damage and repaint the whole screen + */ +region_t get_damage(session_t *ps, bool all_damage) { + region_t region; + auto buffer_age_fn = ps->backend_data->ops->buffer_age; + int buffer_age = buffer_age_fn ? buffer_age_fn(ps->backend_data) : -1; + + if (all_damage) { + buffer_age = -1; + } + + pixman_region32_init(®ion); + if (buffer_age == -1 || buffer_age > ps->ndamage) { + pixman_region32_copy(®ion, &ps->screen_reg); + } else { + for (int i = 0; i < buffer_age; i++) { + auto curr = ((ps->damage - ps->damage_ring) + i) % ps->ndamage; + log_trace("damage index: %d, damage ring offset: %ld", i, curr); + dump_region(&ps->damage_ring[curr]); + pixman_region32_union(®ion, ®ion, &ps->damage_ring[curr]); + } + pixman_region32_intersect(®ion, ®ion, &ps->screen_reg); + } + return region; +} + +static void process_window_for_painting(session_t *ps, struct managed_win* w, void* win_image, + double additional_alpha, + region_t* reg_bound, region_t* reg_visible, + region_t* reg_paint, region_t* reg_paint_in_bound) { + // For window image processing, we don't have to limit the process + // region to damage for correctness. (see for + // details) + + // The visible region, in window local coordinates Although we + // don't limit process region to damage, we provide that info in + // reg_visible as a hint. Since window image data outside of the + // damage region won't be painted onto target + region_t reg_visible_local; + { + // The bounding shape, in window local coordinates + region_t reg_bound_local; + pixman_region32_init(®_bound_local); + pixman_region32_copy(®_bound_local, reg_bound); + pixman_region32_translate(®_bound_local, -w->g.x, -w->g.y); + + pixman_region32_init(®_visible_local); + pixman_region32_intersect(®_visible_local, + reg_visible, reg_paint); + pixman_region32_translate(®_visible_local, -w->g.x, + -w->g.y); + // Data outside of the bounding shape won't be visible, + // but it is not necessary to limit the image operations + // to the bounding shape yet. So pass that as the visible + // region, not the clip region. + pixman_region32_intersect( + ®_visible_local, ®_visible_local, ®_bound_local); + pixman_region32_fini(®_bound_local); + } + + auto new_img = ps->backend_data->ops->clone_image( + ps->backend_data, win_image, ®_visible_local); + auto reg_frame = win_get_region_frame_local_by_val(w); + double alpha = additional_alpha*w->opacity; + ps->backend_data->ops->set_image_property( + ps->backend_data, IMAGE_PROPERTY_OPACITY, new_img, &alpha); + ps->backend_data->ops->image_op( + ps->backend_data, IMAGE_OP_APPLY_ALPHA, new_img, ®_frame, + ®_visible_local, (double[]){w->frame_opacity}); + pixman_region32_fini(®_frame); + ps->backend_data->ops->compose(ps->backend_data, new_img, + w->g.x, w->g.y, + w->g.x + w->widthb, w->g.y + w->heightb, + reg_paint_in_bound, reg_visible); + ps->backend_data->ops->release_image(ps->backend_data, new_img); + pixman_region32_fini(®_visible_local); +} + +/// paint all windows +void paint_all_new(session_t *ps, struct managed_win *t, bool ignore_damage) { + if (ps->o.xrender_sync_fence) { + if (ps->xsync_exists && !x_fence_sync(ps->c, ps->sync_fence)) { + log_error("x_fence_sync failed, xrender-sync-fence will be " + "disabled from now on."); + xcb_sync_destroy_fence(ps->c, ps->sync_fence); + ps->sync_fence = XCB_NONE; + ps->o.xrender_sync_fence = false; + ps->xsync_exists = false; + } + } + // All painting will be limited to the damage, if _some_ of + // the paints bleed out of the damage region, it will destroy + // part of the image we want to reuse + region_t reg_damage; + if (!ignore_damage) { + reg_damage = get_damage(ps, ps->o.monitor_repaint || !ps->o.use_damage); + } else { + pixman_region32_init(®_damage); + pixman_region32_copy(®_damage, &ps->screen_reg); + } + + if (!pixman_region32_not_empty(®_damage)) { + pixman_region32_fini(®_damage); + return; + } + +#ifdef DEBUG_REPAINT + static struct timespec last_paint = {0}; +#endif + + // + // If use_damage is enabled, we MUST make sure only the damaged regions of the + // screen are ever touched by the compositor. The reason is that at the beginning + // of each render, we clear the damaged regions with the wallpaper, and nothing + // else. If later during the render we changed anything outside the damaged + // region, that won't be cleared by the next render, and will thus accumulate. + // (e.g. if shadow is drawn outside the damaged region, it will become thicker and + // thicker over time.) + + /// The adjusted damaged regions + region_t reg_paint; + assert(ps->o.blur_method != BLUR_METHOD_INVALID); + if (ps->o.blur_method != BLUR_METHOD_NONE && ps->backend_data->ops->get_blur_size) { + int blur_width, blur_height; + ps->backend_data->ops->get_blur_size(ps->backend_blur_context, + &blur_width, &blur_height); + + // The region of screen a given window influences will be smeared + // out by blur. With more windows on top of the given window, the + // influences region will be smeared out more. + // + // Also, blurring requires data slightly outside the area that needs + // to be blurred. The more semi-transparent windows are stacked on top + // of each other, the larger the area will be. + // + // Instead of accurately calculate how much bigger the damage + // region will be because of blur, we assume the worst case here. + // That is, the damaged window is at the bottom of the stack, and + // all other windows have semi-transparent background + int resize_factor = 1; + if (t) { + resize_factor = t->stacking_rank; + } + resize_region_in_place(®_damage, blur_width * resize_factor, + blur_height * resize_factor); + reg_paint = resize_region(®_damage, blur_width * resize_factor, + blur_height * resize_factor); + pixman_region32_intersect(®_paint, ®_paint, &ps->screen_reg); + pixman_region32_intersect(®_damage, ®_damage, &ps->screen_reg); + } else { + pixman_region32_init(®_paint); + pixman_region32_copy(®_paint, ®_damage); + } + + // A hint to backend, the region that will be visible on screen + // backend can optimize based on this info + region_t reg_visible; + pixman_region32_init(®_visible); + pixman_region32_copy(®_visible, &ps->screen_reg); + if (t && !ps->o.transparent_clipping) { + // Calculate the region upon which the root window (wallpaper) is to be + // painted based on the ignore region of the lowest window, if available + // + // NOTE If transparent_clipping is enabled, transparent windows are + // included in the reg_ignore, but we still want to have the wallpaper + // beneath them, so we don't use reg_ignore for wallpaper in that case. + pixman_region32_subtract(®_visible, ®_visible, t->reg_ignore); + } + + // Region on screen we don't want any shadows on + region_t reg_shadow_clip; + pixman_region32_init(®_shadow_clip); + + if (ps->backend_data->ops->prepare) { + ps->backend_data->ops->prepare(ps->backend_data, ®_paint); + } + + if (ps->root_image) { + ps->backend_data->ops->compose(ps->backend_data, ps->root_image, + 0, 0, ps->root_width, ps->root_height, + ®_paint, ®_visible); + } else { + ps->backend_data->ops->fill(ps->backend_data, (struct color){0, 0, 0, 1}, + ®_paint); + } + + // Windows are sorted from bottom to top + // Each window has a reg_ignore, which is the region obscured by all the windows + // on top of that window. This is used to reduce the number of pixels painted. + // + // Whether this is beneficial is to be determined XXX + for (auto w = t; w; w = w->prev_trans) { + pixman_region32_subtract(®_visible, &ps->screen_reg, w->reg_ignore); + assert(!(w->flags & WIN_FLAGS_IMAGE_ERROR)); + assert(!(w->flags & WIN_FLAGS_PIXMAP_STALE)); + assert(!(w->flags & WIN_FLAGS_PIXMAP_NONE)); + + // The bounding shape of the window, in global/target coordinates + // reminder: bounding shape contains the WM frame + auto reg_bound = win_get_bounding_shape_global_by_val(w); + + // The clip region for the current window, in global/target coordinates + // reg_paint_in_bound \in reg_paint + region_t reg_paint_in_bound; + pixman_region32_init(®_paint_in_bound); + pixman_region32_intersect(®_paint_in_bound, ®_bound, ®_paint); + if (ps->o.transparent_clipping) { + // + // If transparent_clipping is enabled, we need to be SURE that + // things are not drawn inside reg_ignore, because otherwise they + // will appear underneath transparent windows. + // So here we have make sure reg_paint_in_bound \in reg_visible + // There are a few other places below where this is needed as + // well. + pixman_region32_intersect(®_paint_in_bound, + ®_paint_in_bound, ®_visible); + } + + // Blur window background + /* TODO(yshui) since the backend might change the content of the window + * (e.g. with shaders), we should consult the backend whether the window + * is transparent or not. for now we will just rely on the force_win_blend + * option */ + auto real_win_mode = w->mode; + + if (w->blur_background && + (ps->o.force_win_blend || real_win_mode == WMODE_TRANS || + (ps->o.blur_background_frame && real_win_mode == WMODE_FRAME_TRANS))) { + // Minimize the region we try to blur, if the window + // itself is not opaque, only the frame is. + + double blur_opacity = 1; + if (w->opacity < (1.0 / MAX_ALPHA)) { + // Hide blur for fully transparent windows. + blur_opacity = 0; + } else if (w->state == WSTATE_MAPPING) { + // Gradually increase the blur intensity during + // fading in. + assert(w->opacity <= w->opacity_target); + blur_opacity = w->opacity / w->opacity_target; + } else if (w->state == WSTATE_UNMAPPING || + w->state == WSTATE_DESTROYING) { + // Gradually decrease the blur intensity during + // fading out. + assert(w->opacity <= w->opacity_target_old); + blur_opacity = w->opacity / w->opacity_target_old; + } else if (w->state == WSTATE_FADING) { + if (w->opacity < w->opacity_target && + w->opacity_target_old < (1.0 / MAX_ALPHA)) { + // Gradually increase the blur intensity during + // fading in. + assert(w->opacity <= w->opacity_target); + blur_opacity = w->opacity / w->opacity_target; + } else if (w->opacity > w->opacity_target && + w->opacity_target < (1.0 / MAX_ALPHA)) { + // Gradually decrease the blur intensity during + // fading out. + assert(w->opacity <= w->opacity_target_old); + blur_opacity = w->opacity / w->opacity_target_old; + } + } + assert(blur_opacity >= 0 && blur_opacity <= 1); + + if (real_win_mode == WMODE_TRANS || ps->o.force_win_blend) { + // We need to blur the bounding shape of the window + // (reg_paint_in_bound = reg_bound \cap reg_paint) + ps->backend_data->ops->blur( + ps->backend_data, blur_opacity, ps->backend_blur_context, + ®_paint_in_bound, ®_visible); + } else { + // Window itself is solid, we only need to blur the frame + // region + + // Readability assertions + assert(ps->o.blur_background_frame); + assert(real_win_mode == WMODE_FRAME_TRANS); + + auto reg_blur = win_get_region_frame_local_by_val(w); + pixman_region32_translate(®_blur, w->g.x, w->g.y); + // make sure reg_blur \in reg_paint + pixman_region32_intersect(®_blur, ®_blur, ®_paint); + if (ps->o.transparent_clipping) { + // ref: + pixman_region32_intersect(®_blur, ®_blur, + ®_visible); + } + ps->backend_data->ops->blur(ps->backend_data, blur_opacity, + ps->backend_blur_context, + ®_blur, ®_visible); + pixman_region32_fini(®_blur); + } + } + + // Draw shadow on target + if (w->shadow) { + assert(!(w->flags & WIN_FLAGS_SHADOW_NONE)); + // Clip region for the shadow + // reg_shadow \in reg_paint + auto reg_shadow = win_extents_by_val(w); + pixman_region32_intersect(®_shadow, ®_shadow, ®_paint); + if (!ps->o.wintype_option[w->window_type].full_shadow) { + pixman_region32_subtract(®_shadow, ®_shadow, ®_bound); + } + + // Mask out the region we don't want shadow on + if (pixman_region32_not_empty(&ps->shadow_exclude_reg)) { + pixman_region32_subtract(®_shadow, ®_shadow, + &ps->shadow_exclude_reg); + } + if (pixman_region32_not_empty(®_shadow_clip)) { + pixman_region32_subtract(®_shadow, ®_shadow, + ®_shadow_clip); + } + + if (ps->o.xinerama_shadow_crop && w->xinerama_scr >= 0 && + w->xinerama_scr < ps->xinerama_nscrs) { + // There can be a window where number of screens is + // updated, but the screen number attached to the windows + // have not. + // + // Window screen number will be updated eventually, so + // here we just check to make sure we don't access out of + // bounds. + pixman_region32_intersect( + ®_shadow, ®_shadow, + &ps->xinerama_scr_regs[w->xinerama_scr]); + } + + if (ps->o.transparent_clipping) { + // ref: + pixman_region32_intersect(®_shadow, ®_shadow, + ®_visible); + } + + assert(w->shadow_image); + ps->backend_data->ops->set_image_property( + ps->backend_data, IMAGE_PROPERTY_OPACITY, w->shadow_image, + &w->opacity); + ps->backend_data->ops->compose( + ps->backend_data, w->shadow_image, + w->g.x + w->shadow_dx, w->g.y + w->shadow_dy, + w->g.x + w->shadow_dx + w->shadow_width, + w->g.y + w->shadow_dy + w->shadow_height, + ®_shadow, ®_visible); + pixman_region32_fini(®_shadow); + } + + // Update image properties + { + double dim_opacity = 0.0; + if (w->dim) { + dim_opacity = ps->o.inactive_dim; + if (!ps->o.inactive_dim_fixed) { + dim_opacity *= w->opacity; + } + } + + ps->backend_data->ops->set_image_property( + ps->backend_data, IMAGE_PROPERTY_MAX_BRIGHTNESS, w->win_image, + &ps->o.max_brightness); + ps->backend_data->ops->set_image_property( + ps->backend_data, IMAGE_PROPERTY_INVERTED, w->win_image, + &w->invert_color); + ps->backend_data->ops->set_image_property( + ps->backend_data, IMAGE_PROPERTY_DIM_LEVEL, w->win_image, + &dim_opacity); + ps->backend_data->ops->set_image_property( + ps->backend_data, IMAGE_PROPERTY_OPACITY, w->win_image, &w->opacity); + } + + if (w->opacity * MAX_ALPHA < 1) { + // We don't need to paint the window body itself if it's + // completely transparent. + goto skip; + } + + if (w->clip_shadow_above) { + // Add window bounds to shadow-clip region + pixman_region32_union(®_shadow_clip, ®_shadow_clip, ®_bound); + } else { + // Remove overlapping window bounds from shadow-clip region + pixman_region32_subtract(®_shadow_clip, ®_shadow_clip, ®_bound); + } + + // Draw window on target + bool is_animating = 0 <= w->animation_progress && w->animation_progress < 1.0; + if (w->frame_opacity == 1 && !is_animating) { + ps->backend_data->ops->compose(ps->backend_data, w->win_image, + w->g.x, w->g.y, + w->g.x + w->widthb, w->g.y + w->heightb, + ®_paint_in_bound, ®_visible); + } else { + if (is_animating && w->old_win_image) { + assert(w->old_win_image); + + bool resizing = + w->g.width != w->pending_g.width || + w->g.height != w->pending_g.height; + + // Only animate opacity here if we are resizing + // a transparent window + process_window_for_painting(ps, w, w->win_image, + 1, + ®_bound, ®_visible, + ®_paint, ®_paint_in_bound); + + // Only do this if size changes as otherwise moving + // transparent windows will flicker and if you just + // move so slightly they will keep flickering + if (resizing) { + process_window_for_painting(ps, w, w->old_win_image, + 1.0 - w->animation_progress, + ®_bound, ®_visible, + ®_paint, ®_paint_in_bound); + } + + } else { + process_window_for_painting(ps, w, w->win_image, + 1, + ®_bound, ®_visible, + ®_paint, ®_paint_in_bound); + } + } + skip: + pixman_region32_fini(®_bound); + pixman_region32_fini(®_paint_in_bound); + } + pixman_region32_fini(®_paint); + pixman_region32_fini(®_shadow_clip); + + if (ps->o.monitor_repaint) { + const struct color DEBUG_COLOR = {0.5, 0, 0, 0.5}; + auto reg_damage_debug = get_damage(ps, false); + ps->backend_data->ops->fill(ps->backend_data, DEBUG_COLOR, ®_damage_debug); + pixman_region32_fini(®_damage_debug); + } + + // Move the head of the damage ring + ps->damage = ps->damage - 1; + if (ps->damage < ps->damage_ring) { + ps->damage = ps->damage_ring + ps->ndamage - 1; + } + pixman_region32_clear(ps->damage); + + if (ps->backend_data->ops->present) { + // Present the rendered scene + // Vsync is done here + ps->backend_data->ops->present(ps->backend_data, ®_damage); + } + + pixman_region32_fini(®_damage); + +#ifdef DEBUG_REPAINT + struct timespec now = get_time_timespec(); + struct timespec diff = {0}; + timespec_subtract(&diff, &now, &last_paint); + log_trace("[ %5ld:%09ld ] ", diff.tv_sec, diff.tv_nsec); + last_paint = now; + log_trace("paint:"); + for (win *w = t; w; w = w->prev_trans) + log_trace(" %#010lx", w->id); +#endif +} + +// vim: set noet sw=8 ts=8 : diff --git a/src/backend/backend.h b/src/backend/backend.h new file mode 100644 index 0000000..ae107d3 --- /dev/null +++ b/src/backend/backend.h @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018, Yuxuan Shui + +#pragma once + +#include + +#include "compiler.h" +#include "config.h" +#include "driver.h" +#include "kernel.h" +#include "region.h" +#include "types.h" +#include "x.h" + +typedef struct session session_t; +struct managed_win; + +struct ev_loop; +struct backend_operations; + +typedef struct backend_base { + struct backend_operations *ops; + xcb_connection_t *c; + xcb_window_t root; + struct ev_loop *loop; + + /// Whether the backend can accept new render request at the moment + bool busy; + // ... +} backend_t; + +typedef void (*backend_ready_callback_t)(void *); + +// When image properties are actually applied to the image, they are applied in a +// particular order: +// +// Color inversion -> Dimming -> Opacity multiply -> Limit maximum brightness +enum image_properties { + // Whether the color of the image is inverted + // 1 boolean, default: false + IMAGE_PROPERTY_INVERTED, + // How much the image is dimmed + // 1 double, default: 0 + IMAGE_PROPERTY_DIM_LEVEL, + // Image opacity, i.e. an alpha value multiplied to the alpha channel + // 1 double, default: 1 + IMAGE_PROPERTY_OPACITY, + // The effective size of the image, the image will be tiled to fit. + // 2 int, default: the actual size of the image + IMAGE_PROPERTY_EFFECTIVE_SIZE, + // Limit how bright image can be. The image brightness is estimated by averaging + // the pixels in the image, and dimming will be applied to scale the average + // brightness down to the max brightness value. + // 1 double, default: 1 + IMAGE_PROPERTY_MAX_BRIGHTNESS, +}; + +enum image_operations { + // Multiply the alpha channel by the argument + IMAGE_OP_APPLY_ALPHA, +}; + +struct gaussian_blur_args { + int size; + double deviation; +}; + +struct box_blur_args { + int size; +}; + +struct kernel_blur_args { + struct conv **kernels; + int kernel_count; +}; + +struct dual_kawase_blur_args { + int size; + int strength; +}; + +struct backend_operations { + // =========== Initialization =========== + + /// Initialize the backend, prepare for rendering to the target window. + /// Here is how you should choose target window: + /// 1) if ps->overlay is not XCB_NONE, use that + /// 2) use ps->root otherwise + // TODO(yshui) make the target window a parameter + backend_t *(*init)(session_t *)attr_nonnull(1); + void (*deinit)(backend_t *backend_data) attr_nonnull(1); + + /// Called when rendering will be stopped for an unknown amount of + /// time (e.g. when screen is unredirected). Free some resources. + /// + /// Optional, not yet used + void (*pause)(backend_t *backend_data, session_t *ps); + + /// Called before rendering is resumed + /// + /// Optional, not yet used + void (*resume)(backend_t *backend_data, session_t *ps); + + /// Called when root property changed, returns the new + /// backend_data. Even if the backend_data changed, all + /// the existing image data returned by this backend should + /// remain valid. + /// + /// Optional + void *(*root_change)(backend_t *backend_data, session_t *ps); + + // =========== Rendering ============ + + // NOTE: general idea about reg_paint/reg_op vs reg_visible is that reg_visible is + // merely a hint. Ignoring reg_visible entirely don't affect the correctness of + // the operation performed. OTOH reg_paint/reg_op is part of the parameters of the + // operation, and must be honored in order to complete the operation correctly. + + // NOTE: due to complications introduced by use-damage and blur, the rendering API + // is a bit weird. The idea is, `compose` and `blur` have to update a temporary + // buffer, because `blur` requires data from an area slightly larger than the area + // that will be visible. So the area outside the visible area has to be rendered, + // but we have to discard the result (because the result of blurring that area + // will be wrong). That's why we cannot render into the back buffer directly. + // After rendering is done, `present` is called to update a portion of the actual + // back buffer, then present it to the target (or update the target directly, + // if not back buffered). + + /// Called before when a new frame starts. + /// + /// Optional + void (*prepare)(backend_t *backend_data, const region_t *reg_damage); + + /** + * Paint the content of an image onto the rendering buffer + * + * @param backend_data the backend data + * @param image_data the image to paint + * @param dst_x1, dst_y1 the top left corner of the image in the target + * @param dst_x2, dst_y2 the top right corner of the image in the target + * @param reg_paint the clip region, in target coordinates + * @param reg_visible the visible region, in target coordinates + */ + void (*compose)(backend_t *backend_data, void *image_data, + int dst_x1, int dst_y1, int dst_x2, int dst_y2, + const region_t *reg_paint, const region_t *reg_visible); + + /// Fill rectangle of the rendering buffer, mostly for debug purposes, optional. + void (*fill)(backend_t *backend_data, struct color, const region_t *clip); + + /// Blur a given region of the rendering buffer. + bool (*blur)(backend_t *backend_data, double opacity, void *blur_ctx, + const region_t *reg_blur, const region_t *reg_visible) + attr_nonnull(1, 3, 4, 5); + + /// Update part of the back buffer with the rendering buffer, then present the + /// back buffer onto the target window (if not back buffered, update part of the + /// target window directly). + /// + /// Optional, if NULL, indicates the backend doesn't have render output + /// + /// @param region part of the target that should be updated + void (*present)(backend_t *backend_data, const region_t *region) attr_nonnull(1, 2); + + /** + * Bind a X pixmap to the backend's internal image data structure. + * + * @param backend_data backend data + * @param pixmap X pixmap to bind + * @param fmt information of the pixmap's visual + * @param owned whether the ownership of the pixmap is transfered to the backend + * @return backend internal data structure bound with this pixmap + */ + void *(*bind_pixmap)(backend_t *backend_data, xcb_pixmap_t pixmap, + struct xvisual_info fmt, bool owned); + + /// Create a shadow image based on the parameters + /// Default implementation: default_backend_render_shadow + void *(*render_shadow)(backend_t *backend_data, int width, int height, + const conv *kernel, double r, double g, double b, double a); + + // ============ Resource management =========== + + /// Free resources associated with an image data structure + void (*release_image)(backend_t *backend_data, void *img_data) attr_nonnull(1, 2); + + // =========== Query =========== + + /// Return if image is not completely opaque. + /// + /// This function is needed because some backend might change the content of the + /// window (e.g. when using a custom shader with the glx backend), so only the + /// backend knows if an image is transparent. + bool (*is_image_transparent)(backend_t *backend_data, void *image_data) + attr_nonnull(1, 2); + + /// Get the age of the buffer content we are currently rendering ontop + /// of. The buffer that has just been `present`ed has a buffer age of 1. + /// Everytime `present` is called, buffers get older. Return -1 if the + /// buffer is empty. + /// + /// Optional + int (*buffer_age)(backend_t *backend_data); + + /// The maximum number buffer_age might return. + int max_buffer_age; + + // =========== Post-processing ============ + + /* TODO(yshui) Consider preserving the order of image ops. + * Currently in both backends, the image ops are applied lazily when needed. + * However neither backends preserve the order of image ops, they just applied all + * pending lazy ops in a pre-determined fixed order, regardless in which order + * they were originally applied. This might lead to inconsistencies.*/ + + /** + * Change image properties + * + * @param backend_data backend data + * @param prop the property to change + * @param image_data an image data structure returned by the backend + * @param args property value + * @return whether the operation is successful + */ + bool (*set_image_property)(backend_t *backend_data, enum image_properties prop, + void *image_data, void *args); + + /** + * Manipulate an image. Image properties are untouched. + * + * @param backend_data backend data + * @param op the operation to perform + * @param image_data an image data structure returned by the backend + * @param reg_op the clip region, define the part of the image to be + * operated on. + * @param reg_visible define the part of the image that will eventually + * be visible on target. this is a hint to the backend + * for optimization purposes. + * @param args extra arguments, operation specific + * @return whether the operation is successful + */ + bool (*image_op)(backend_t *backend_data, enum image_operations op, void *image_data, + const region_t *reg_op, const region_t *reg_visible, void *args); + + /** + * Read the color of the pixel at given position of the given image. Image + * properties have no effect. + * + * @param backend_data backend_data + * @param image_data an image data structure previously returned by the + * backend. the image to read pixel from. + * @param x, y coordinate of the pixel to read + * @param[out] color the color of the pixel + * @return whether the operation is successful + */ + bool (*read_pixel)(backend_t *backend_data, void *image_data, int x, int y, + struct color *output); + + /// Create another instance of the `image_data`. All `image_op` and + /// `set_image_property` calls on the returned image should not affect the + /// original image + void *(*clone_image)(backend_t *base, const void *image_data, + const region_t *reg_visible); + + /// Create a blur context that can be used to call `blur` + void *(*create_blur_context)(backend_t *base, enum blur_method, void *args); + /// Destroy a blur context + void (*destroy_blur_context)(backend_t *base, void *ctx); + /// Get how many pixels outside of the blur area is needed for blur + void (*get_blur_size)(void *blur_context, int *width, int *height); + + // =========== Hooks ============ + /// Let the backend hook into the event handling queue + /// Not implemented yet + void (*set_ready_callback)(backend_t *, backend_ready_callback_t cb); + /// Called right after the core has handled its events. + /// Not implemented yet + void (*handle_events)(backend_t *); + // =========== Misc ============ + /// Return the driver that is been used by the backend + enum driver (*detect_driver)(backend_t *backend_data); + + void (*diagnostics)(backend_t *backend_data); +}; + +extern struct backend_operations *backend_list[]; + +void paint_all_new(session_t *ps, struct managed_win *const t, bool ignore_damage) + attr_nonnull(1); diff --git a/src/backend/backend_common.c b/src/backend/backend_common.c new file mode 100644 index 0000000..c0377d3 --- /dev/null +++ b/src/backend/backend_common.c @@ -0,0 +1,480 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#include +#include +#include +#include +#include + +#include "backend/backend.h" +#include "backend/backend_common.h" +#include "common.h" +#include "config.h" +#include "kernel.h" +#include "log.h" +#include "utils.h" +#include "win.h" +#include "x.h" + +/** + * Generate a 1x1 Picture of a particular color. + */ +xcb_render_picture_t solid_picture(xcb_connection_t *c, xcb_drawable_t d, bool argb, + double a, double r, double g, double b) { + xcb_pixmap_t pixmap; + xcb_render_picture_t picture; + xcb_render_create_picture_value_list_t pa; + xcb_render_color_t col; + xcb_rectangle_t rect; + + pixmap = x_create_pixmap(c, argb ? 32 : 8, d, 1, 1); + if (!pixmap) + return XCB_NONE; + + pa.repeat = 1; + picture = x_create_picture_with_standard_and_pixmap( + c, argb ? XCB_PICT_STANDARD_ARGB_32 : XCB_PICT_STANDARD_A_8, pixmap, + XCB_RENDER_CP_REPEAT, &pa); + + if (!picture) { + xcb_free_pixmap(c, pixmap); + return XCB_NONE; + } + + col.alpha = (uint16_t)(a * 0xffff); + col.red = (uint16_t)(r * 0xffff); + col.green = (uint16_t)(g * 0xffff); + col.blue = (uint16_t)(b * 0xffff); + + rect.x = 0; + rect.y = 0; + rect.width = 1; + rect.height = 1; + + xcb_render_fill_rectangles(c, XCB_RENDER_PICT_OP_SRC, picture, col, 1, &rect); + xcb_free_pixmap(c, pixmap); + + return picture; +} + +xcb_image_t * +make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, int height) { + /* + * We classify shadows into 4 kinds of regions + * r = shadow radius + * (0, 0) is the top left of the window itself + * -r r width-r width+r + * -r +-----+---------+-----+ + * | 1 | 2 | 1 | + * r +-----+---------+-----+ + * | 2 | 3 | 2 | + * height-r +-----+---------+-----+ + * | 1 | 2 | 1 | + * height+r +-----+---------+-----+ + */ + xcb_image_t *ximage; + const double *shadow_sum = kernel->rsum; + assert(shadow_sum); + // We only support square kernels for shadow + assert(kernel->w == kernel->h); + int d = kernel->w; + int r = d / 2; + int swidth = width + r * 2, sheight = height + r * 2; + + assert(d % 2 == 1); + assert(d > 0); + + ximage = xcb_image_create_native(c, to_u16_checked(swidth), to_u16_checked(sheight), + XCB_IMAGE_FORMAT_Z_PIXMAP, 8, 0, 0, NULL); + if (!ximage) { + log_error("failed to create an X image"); + return 0; + } + + unsigned char *data = ximage->data; + long sstride = ximage->stride; + + // If the window body is smaller than the kernel, we do convolution directly + if (width < r * 2 && height < r * 2) { + for (int y = 0; y < sheight; y++) { + for (int x = 0; x < swidth; x++) { + double sum = sum_kernel_normalized( + kernel, d - x - 1, d - y - 1, width, height); + data[y * sstride + x] = (uint8_t)(sum * 255.0); + } + } + return ximage; + } + + if (height < r * 2) { + // Implies width >= r * 2 + // If the window height is smaller than the kernel, we divide + // the window like this: + // -r r width-r width+r + // +------+-------------+------+ + // | | | | + // +------+-------------+------+ + for (int y = 0; y < sheight; y++) { + for (int x = 0; x < r * 2; x++) { + double sum = sum_kernel_normalized(kernel, d - x - 1, + d - y - 1, d, height) * + 255.0; + data[y * sstride + x] = (uint8_t)sum; + data[y * sstride + swidth - x - 1] = (uint8_t)sum; + } + } + for (int y = 0; y < sheight; y++) { + double sum = + sum_kernel_normalized(kernel, 0, d - y - 1, d, height) * 255.0; + memset(&data[y * sstride + r * 2], (uint8_t)sum, + (size_t)(width - 2 * r)); + } + return ximage; + } + if (width < r * 2) { + // Similarly, for width smaller than kernel + for (int y = 0; y < r * 2; y++) { + for (int x = 0; x < swidth; x++) { + double sum = sum_kernel_normalized(kernel, d - x - 1, + d - y - 1, width, d) * + 255.0; + data[y * sstride + x] = (uint8_t)sum; + data[(sheight - y - 1) * sstride + x] = (uint8_t)sum; + } + } + for (int x = 0; x < swidth; x++) { + double sum = + sum_kernel_normalized(kernel, d - x - 1, 0, width, d) * 255.0; + for (int y = r * 2; y < height; y++) { + data[y * sstride + x] = (uint8_t)sum; + } + } + return ximage; + } + + // Implies: width >= r * 2 && height >= r * 2 + + // Fill part 3 + for (int y = r; y < height + r; y++) { + memset(data + sstride * y + r, (uint8_t)(255 * opacity), (size_t)width); + } + + // Part 1 + for (int y = 0; y < r * 2; y++) { + for (int x = 0; x < r * 2; x++) { + double tmpsum = shadow_sum[y * d + x] * opacity * 255.0; + data[y * sstride + x] = (uint8_t)tmpsum; + data[(sheight - y - 1) * sstride + x] = (uint8_t)tmpsum; + data[(sheight - y - 1) * sstride + (swidth - x - 1)] = (uint8_t)tmpsum; + data[y * sstride + (swidth - x - 1)] = (uint8_t)tmpsum; + } + } + + // Part 2, top/bottom + for (int y = 0; y < r * 2; y++) { + double tmpsum = shadow_sum[d * y + d - 1] * opacity * 255.0; + memset(&data[y * sstride + r * 2], (uint8_t)tmpsum, (size_t)(width - r * 2)); + memset(&data[(sheight - y - 1) * sstride + r * 2], (uint8_t)tmpsum, + (size_t)(width - r * 2)); + } + + // Part 2, left/right + for (int x = 0; x < r * 2; x++) { + double tmpsum = shadow_sum[d * (d - 1) + x] * opacity * 255.0; + for (int y = r * 2; y < height; y++) { + data[y * sstride + x] = (uint8_t)tmpsum; + data[y * sstride + (swidth - x - 1)] = (uint8_t)tmpsum; + } + } + + return ximage; +} + +/** + * Generate shadow Picture for a window. + */ +bool build_shadow(xcb_connection_t *c, xcb_drawable_t d, double opacity, const int width, + const int height, const conv *kernel, xcb_render_picture_t shadow_pixel, + xcb_pixmap_t *pixmap, xcb_render_picture_t *pict) { + xcb_image_t *shadow_image = NULL; + xcb_pixmap_t shadow_pixmap = XCB_NONE, shadow_pixmap_argb = XCB_NONE; + xcb_render_picture_t shadow_picture = XCB_NONE, shadow_picture_argb = XCB_NONE; + xcb_gcontext_t gc = XCB_NONE; + + shadow_image = make_shadow(c, kernel, opacity, width, height); + if (!shadow_image) { + log_error("Failed to make shadow"); + return false; + } + + shadow_pixmap = x_create_pixmap(c, 8, d, shadow_image->width, shadow_image->height); + shadow_pixmap_argb = + x_create_pixmap(c, 32, d, shadow_image->width, shadow_image->height); + + if (!shadow_pixmap || !shadow_pixmap_argb) { + log_error("Failed to create shadow pixmaps"); + goto shadow_picture_err; + } + + shadow_picture = x_create_picture_with_standard_and_pixmap( + c, XCB_PICT_STANDARD_A_8, shadow_pixmap, 0, NULL); + shadow_picture_argb = x_create_picture_with_standard_and_pixmap( + c, XCB_PICT_STANDARD_ARGB_32, shadow_pixmap_argb, 0, NULL); + if (!shadow_picture || !shadow_picture_argb) { + goto shadow_picture_err; + } + + gc = x_new_id(c); + xcb_create_gc(c, gc, shadow_pixmap, 0, NULL); + + // We need to make room for protocol metadata in the request. The metadata should + // be 24 bytes plus padding, let's be generous and give it 1kb + auto maximum_image_size = xcb_get_maximum_request_length(c) * 4 - 1024; + auto maximum_row = + to_u16_checked(clamp(maximum_image_size / shadow_image->stride, 0, UINT16_MAX)); + if (maximum_row <= 0) { + // TODO(yshui) Upload image with XShm + log_error("X server request size limit is too restrictive, or the shadow " + "image is too wide for us to send a single row of the shadow " + "image. Shadow size: %dx%d", + width, height); + goto shadow_picture_err; + } + + for (uint32_t row = 0; row < shadow_image->height; row += maximum_row) { + auto batch_height = maximum_row; + if (batch_height > shadow_image->height - row) { + batch_height = to_u16_checked(shadow_image->height - row); + } + + uint32_t offset = row * shadow_image->stride / sizeof(*shadow_image->data); + xcb_put_image(c, (uint8_t)shadow_image->format, shadow_pixmap, gc, + shadow_image->width, batch_height, 0, to_i16_checked(row), + 0, shadow_image->depth, shadow_image->stride * batch_height, + shadow_image->data + offset); + } + + xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, shadow_pixel, shadow_picture, + shadow_picture_argb, 0, 0, 0, 0, 0, 0, shadow_image->width, + shadow_image->height); + + *pixmap = shadow_pixmap_argb; + *pict = shadow_picture_argb; + + xcb_free_gc(c, gc); + xcb_image_destroy(shadow_image); + xcb_free_pixmap(c, shadow_pixmap); + xcb_render_free_picture(c, shadow_picture); + + return true; + +shadow_picture_err: + if (shadow_image) { + xcb_image_destroy(shadow_image); + } + if (shadow_pixmap) { + xcb_free_pixmap(c, shadow_pixmap); + } + if (shadow_pixmap_argb) { + xcb_free_pixmap(c, shadow_pixmap_argb); + } + if (shadow_picture) { + xcb_render_free_picture(c, shadow_picture); + } + if (shadow_picture_argb) { + xcb_render_free_picture(c, shadow_picture_argb); + } + if (gc) { + xcb_free_gc(c, gc); + } + + return false; +} + +void * +default_backend_render_shadow(backend_t *backend_data, int width, int height, + const conv *kernel, double r, double g, double b, double a) { + xcb_pixmap_t shadow_pixel = solid_picture(backend_data->c, backend_data->root, + true, 1, r, g, b), + shadow = XCB_NONE; + xcb_render_picture_t pict = XCB_NONE; + + if (!build_shadow(backend_data->c, backend_data->root, a, width, height, kernel, + shadow_pixel, &shadow, &pict)) { + return NULL; + } + + auto visual = x_get_visual_for_standard(backend_data->c, XCB_PICT_STANDARD_ARGB_32); + void *ret = backend_data->ops->bind_pixmap( + backend_data, shadow, x_get_visual_info(backend_data->c, visual), true); + xcb_render_free_picture(backend_data->c, pict); + return ret; +} + +static struct conv **generate_box_blur_kernel(struct box_blur_args *args, int *kernel_count) { + int r = args->size * 2 + 1; + assert(r > 0); + auto ret = ccalloc(2, struct conv *); + ret[0] = cvalloc(sizeof(struct conv) + sizeof(double) * (size_t)r); + ret[1] = cvalloc(sizeof(struct conv) + sizeof(double) * (size_t)r); + ret[0]->w = r; + ret[0]->h = 1; + ret[1]->w = 1; + ret[1]->h = r; + for (int i = 0; i < r; i++) { + ret[0]->data[i] = 1; + ret[1]->data[i] = 1; + } + *kernel_count = 2; + return ret; +} + +static struct conv ** +generate_gaussian_blur_kernel(struct gaussian_blur_args *args, int *kernel_count) { + int r = args->size * 2 + 1; + assert(r > 0); + auto ret = ccalloc(2, struct conv *); + ret[0] = cvalloc(sizeof(struct conv) + sizeof(double) * (size_t)r); + ret[1] = cvalloc(sizeof(struct conv) + sizeof(double) * (size_t)r); + ret[0]->w = r; + ret[0]->h = 1; + ret[1]->w = 1; + ret[1]->h = r; + for (int i = 0; i <= args->size; i++) { + ret[0]->data[i] = ret[0]->data[r - i - 1] = + 1.0 / (sqrt(2.0 * M_PI) * args->deviation) * + exp(-(args->size - i) * (args->size - i) / + (2 * args->deviation * args->deviation)); + ret[1]->data[i] = ret[1]->data[r - i - 1] = ret[0]->data[i]; + } + *kernel_count = 2; + return ret; +} + +/// Generate blur kernels for gaussian and box blur methods. Generated kernel is not +/// normalized, and the center element will always be 1. +struct conv **generate_blur_kernel(enum blur_method method, void *args, int *kernel_count) { + switch (method) { + case BLUR_METHOD_BOX: return generate_box_blur_kernel(args, kernel_count); + case BLUR_METHOD_GAUSSIAN: + return generate_gaussian_blur_kernel(args, kernel_count); + default: break; + } + return NULL; +} + +/// Generate kernel parameters for dual-kawase blur method. Falls back on approximating +/// standard gauss radius if strength is zero or below. +struct dual_kawase_params *generate_dual_kawase_params(void *args) { + struct dual_kawase_blur_args *blur_args = args; + static const struct { + int iterations; /// Number of down- and upsample iterations + float offset; /// Sample offset in half-pixels + int min_radius; /// Approximate gauss-blur with at least this + /// radius and std-deviation + } strength_levels[20] = { + {.iterations = 1, .offset = 1.25f, .min_radius = 1}, // LVL 1 + {.iterations = 1, .offset = 2.25f, .min_radius = 6}, // LVL 2 + {.iterations = 2, .offset = 2.00f, .min_radius = 11}, // LVL 3 + {.iterations = 2, .offset = 3.00f, .min_radius = 17}, // LVL 4 + {.iterations = 2, .offset = 4.25f, .min_radius = 24}, // LVL 5 + {.iterations = 3, .offset = 2.50f, .min_radius = 32}, // LVL 6 + {.iterations = 3, .offset = 3.25f, .min_radius = 40}, // LVL 7 + {.iterations = 3, .offset = 4.25f, .min_radius = 51}, // LVL 8 + {.iterations = 3, .offset = 5.50f, .min_radius = 67}, // LVL 9 + {.iterations = 4, .offset = 3.25f, .min_radius = 83}, // LVL 10 + {.iterations = 4, .offset = 4.00f, .min_radius = 101}, // LVL 11 + {.iterations = 4, .offset = 5.00f, .min_radius = 123}, // LVL 12 + {.iterations = 4, .offset = 6.00f, .min_radius = 148}, // LVL 13 + {.iterations = 4, .offset = 7.25f, .min_radius = 178}, // LVL 14 + {.iterations = 4, .offset = 8.25f, .min_radius = 208}, // LVL 15 + {.iterations = 5, .offset = 4.50f, .min_radius = 236}, // LVL 16 + {.iterations = 5, .offset = 5.25f, .min_radius = 269}, // LVL 17 + {.iterations = 5, .offset = 6.25f, .min_radius = 309}, // LVL 18 + {.iterations = 5, .offset = 7.25f, .min_radius = 357}, // LVL 19 + {.iterations = 5, .offset = 8.50f, .min_radius = 417}, // LVL 20 + }; + + auto params = ccalloc(1, struct dual_kawase_params); + params->iterations = 0; + params->offset = 1.0f; + + if (blur_args->strength <= 0 && blur_args->size) { + // find highest level that approximates blur-strength with the selected + // gaussian blur-radius + int lvl = 1; + while (strength_levels[lvl - 1].min_radius < blur_args->size && lvl < 20) { + ++lvl; + } + blur_args->strength = lvl; + } + if (blur_args->strength <= 0) { + // default value + blur_args->strength = 5; + } + + assert(blur_args->strength > 0 && blur_args->strength <= 20); + params->iterations = strength_levels[blur_args->strength - 1].iterations; + params->offset = strength_levels[blur_args->strength - 1].offset; + + // Expand sample area to cover the smallest texture / highest selected iteration: + // - Smallest texture dimensions are halved `iterations`-times + // - Upsample needs pixels two-times `offset` away from the border + // - Plus one for interpolation differences + params->expand = (1 << params->iterations) * 2 * (int)ceil(params->offset) + 1; + + return params; +} + +void *default_clone_image(backend_t *base attr_unused, const void *image_data, + const region_t *reg_visible attr_unused) { + auto new_img = ccalloc(1, struct backend_image); + *new_img = *(struct backend_image *)image_data; + new_img->inner->refcount++; + return new_img; +} + +bool default_set_image_property(backend_t *base attr_unused, enum image_properties op, + void *image_data, void *arg) { + struct backend_image *tex = image_data; + int *iargs = arg; + bool *bargs = arg; + double *dargs = arg; + switch (op) { + case IMAGE_PROPERTY_INVERTED: tex->color_inverted = bargs[0]; break; + case IMAGE_PROPERTY_DIM_LEVEL: tex->dim = dargs[0]; break; + case IMAGE_PROPERTY_OPACITY: tex->opacity = dargs[0]; break; + case IMAGE_PROPERTY_EFFECTIVE_SIZE: + // texture is already set to repeat, so nothing else we need to do + tex->ewidth = iargs[0]; + tex->eheight = iargs[1]; + break; + case IMAGE_PROPERTY_MAX_BRIGHTNESS: tex->max_brightness = dargs[0]; break; + } + + return true; +} + +bool default_is_image_transparent(backend_t *base attr_unused, void *image_data) { + struct backend_image *img = image_data; + return img->opacity < 1 || img->inner->has_alpha; +} + +struct backend_image *default_new_backend_image(int w, int h) { + auto ret = ccalloc(1, struct backend_image); + ret->opacity = 1; + ret->dim = 0; + ret->max_brightness = 1; + ret->eheight = h; + ret->ewidth = w; + ret->color_inverted = false; + return ret; +} + +void init_backend_base(struct backend_base *base, session_t *ps) { + base->c = ps->c; + base->loop = ps->loop; + base->root = ps->root; + base->busy = false; + base->ops = NULL; +} diff --git a/src/backend/backend_common.h b/src/backend/backend_common.h new file mode 100644 index 0000000..5c9c806 --- /dev/null +++ b/src/backend/backend_common.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#pragma once + +#include +#include + +#include + +#include "backend.h" +#include "config.h" +#include "region.h" + +typedef struct session session_t; +typedef struct win win; +typedef struct conv conv; +typedef struct backend_base backend_t; +struct backend_operations; + +struct dual_kawase_params { + /// Number of downsample passes + int iterations; + /// Pixel offset for down- and upsample + float offset; + /// Save area around blur target (@ref resize_width, @ref resize_height) + int expand; +}; + +struct backend_image_inner_base { + int refcount; + bool has_alpha; +}; + +struct backend_image { + // Backend dependent inner image data + struct backend_image_inner_base *inner; + double opacity; + double dim; + double max_brightness; + // Effective size of the image + int ewidth, eheight; + bool color_inverted; +}; + +bool build_shadow(xcb_connection_t *, xcb_drawable_t, double opacity, int width, + int height, const conv *kernel, xcb_render_picture_t shadow_pixel, + xcb_pixmap_t *pixmap, xcb_render_picture_t *pict); + +xcb_render_picture_t solid_picture(xcb_connection_t *, xcb_drawable_t, bool argb, + double a, double r, double g, double b); + +xcb_image_t * +make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, int height); + +/// The default implementation of `is_win_transparent`, it simply looks at win::mode. So +/// this is not suitable for backends that alter the content of windows +bool default_is_win_transparent(void *, win *, void *); + +/// The default implementation of `is_frame_transparent`, it uses win::frame_opacity. Same +/// caveat as `default_is_win_transparent` applies. +bool default_is_frame_transparent(void *, win *, void *); + +void * +default_backend_render_shadow(backend_t *backend_data, int width, int height, + const conv *kernel, double r, double g, double b, double a); + +void init_backend_base(struct backend_base *base, session_t *ps); + +struct conv **generate_blur_kernel(enum blur_method method, void *args, int *kernel_count); +struct dual_kawase_params *generate_dual_kawase_params(void *args); + +void *default_clone_image(backend_t *base, const void *image_data, const region_t *reg); +void *default_resize_image(backend_t *base, const void *image_data, uint16_t desired_width, + uint16_t desired_height, const region_t *reg); +bool default_is_image_transparent(backend_t *base attr_unused, void *image_data); +bool default_set_image_property(backend_t *base attr_unused, enum image_properties op, + void *image_data, void *arg); +struct backend_image *default_new_backend_image(int w, int h); diff --git a/src/backend/driver.c b/src/backend/driver.c new file mode 100644 index 0000000..a41d2fd --- /dev/null +++ b/src/backend/driver.c @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#include +#include + +#include +#include + +#include "backend/backend.h" +#include "backend/driver.h" +#include "common.h" +#include "compiler.h" +#include "log.h" + +/// Apply driver specified global workarounds. It's safe to call this multiple times. +void apply_driver_workarounds(struct session *ps, enum driver driver) { + if (driver & DRIVER_NVIDIA) { + // setenv("__GL_YIELD", "usleep", true); + setenv("__GL_MaxFramesAllowed", "1", true); + ps->o.xrender_sync_fence = true; + } +} + +enum driver detect_driver(xcb_connection_t *c, backend_t *backend_data, xcb_window_t window) { + enum driver ret = 0; + // First we try doing backend agnostic detection using RANDR + // There's no way to query the X server about what driver is loaded, so RANDR is + // our best shot. + auto randr_version = xcb_randr_query_version_reply( + c, xcb_randr_query_version(c, XCB_RANDR_MAJOR_VERSION, XCB_RANDR_MINOR_VERSION), + NULL); + if (randr_version && + (randr_version->major_version > 1 || randr_version->minor_version >= 4)) { + auto r = xcb_randr_get_providers_reply( + c, xcb_randr_get_providers(c, window), NULL); + if (r == NULL) { + log_warn("Failed to get RANDR providers"); + free(randr_version); + return 0; + } + + auto providers = xcb_randr_get_providers_providers(r); + for (auto i = 0; i < xcb_randr_get_providers_providers_length(r); i++) { + auto r2 = xcb_randr_get_provider_info_reply( + c, xcb_randr_get_provider_info(c, providers[i], r->timestamp), NULL); + if (r2 == NULL) { + continue; + } + if (r2->num_outputs == 0) { + free(r2); + continue; + } + + auto name_len = xcb_randr_get_provider_info_name_length(r2); + assert(name_len >= 0); + auto name = + strndup(xcb_randr_get_provider_info_name(r2), (size_t)name_len); + if (strcasestr(name, "modesetting") != NULL) { + ret |= DRIVER_MODESETTING; + } else if (strcasestr(name, "Radeon") != NULL) { + // Be conservative, add both radeon drivers + ret |= DRIVER_AMDGPU | DRIVER_RADEON; + } else if (strcasestr(name, "NVIDIA") != NULL) { + ret |= DRIVER_NVIDIA; + } else if (strcasestr(name, "nouveau") != NULL) { + ret |= DRIVER_NOUVEAU; + } else if (strcasestr(name, "Intel") != NULL) { + ret |= DRIVER_INTEL; + } + free(name); + free(r2); + } + free(r); + } + free(randr_version); + + // If the backend supports driver detection, use that as well + if (backend_data && backend_data->ops->detect_driver) { + ret |= backend_data->ops->detect_driver(backend_data); + } + return ret; +} diff --git a/src/backend/driver.h b/src/backend/driver.h new file mode 100644 index 0000000..a37cda3 --- /dev/null +++ b/src/backend/driver.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#pragma once + +#include +#include +#include + +#include "utils.h" + +struct session; +struct backend_base; + +// A list of known driver quirks: +// * NVIDIA driver doesn't like seeing the same pixmap under different +// ids, so avoid naming the pixmap again when it didn't actually change. + +/// A list of possible drivers. +/// The driver situation is a bit complicated. There are two drivers we care about: the +/// DDX, and the OpenGL driver. They are usually paired, but not always, since there is +/// also the generic modesetting driver. +/// This enum represents _both_ drivers. +enum driver { + DRIVER_AMDGPU = 1, // AMDGPU for DDX, radeonsi for OpenGL + DRIVER_RADEON = 2, // ATI for DDX, mesa r600 for OpenGL + DRIVER_FGLRX = 4, + DRIVER_NVIDIA = 8, + DRIVER_NOUVEAU = 16, + DRIVER_INTEL = 32, + DRIVER_MODESETTING = 64, +}; + +static const char *driver_names[] = { + "AMDGPU", "Radeon", "fglrx", "NVIDIA", "nouveau", "Intel", "modesetting", +}; + +/// Return a list of all drivers currently in use by the X server. +/// Note, this is a best-effort test, so no guarantee all drivers will be detected. +enum driver detect_driver(xcb_connection_t *, struct backend_base *, xcb_window_t); + +/// Apply driver specified global workarounds. It's safe to call this multiple times. +void apply_driver_workarounds(struct session *ps, enum driver); + +// Print driver names to stdout, for diagnostics +static inline void print_drivers(enum driver drivers) { + const char *seen_drivers[ARR_SIZE(driver_names)]; + int driver_count = 0; + for (size_t i = 0; i < ARR_SIZE(driver_names); i++) { + if (drivers & (1ul << i)) { + seen_drivers[driver_count++] = driver_names[i]; + } + } + + if (driver_count > 0) { + printf("%s", seen_drivers[0]); + for (int i = 1; i < driver_count; i++) { + printf(", %s", seen_drivers[i]); + } + } + printf("\n"); +} diff --git a/src/backend/dummy/dummy.c b/src/backend/dummy/dummy.c new file mode 100644 index 0000000..a057b97 --- /dev/null +++ b/src/backend/dummy/dummy.c @@ -0,0 +1,174 @@ +#include +#include + +#include "backend/backend.h" +#include "backend/backend_common.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "log.h" +#include "region.h" +#include "types.h" +#include "uthash_extra.h" +#include "utils.h" +#include "x.h" + +struct dummy_image { + xcb_pixmap_t pixmap; + bool transparent; + int *refcount; + UT_hash_handle hh; +}; + +struct dummy_data { + struct backend_base base; + struct dummy_image *images; +}; + +struct backend_base *dummy_init(struct session *ps attr_unused) { + auto ret = (struct backend_base *)ccalloc(1, struct dummy_data); + ret->c = ps->c; + ret->loop = ps->loop; + ret->root = ps->root; + ret->busy = false; + return ret; +} + +void dummy_deinit(struct backend_base *data) { + auto dummy = (struct dummy_data *)data; + HASH_ITER2(dummy->images, img) { + log_warn("Backend image for pixmap %#010x is not freed", img->pixmap); + HASH_DEL(dummy->images, img); + free(img->refcount); + free(img); + } + free(dummy); +} + +static void dummy_check_image(struct backend_base *base, const struct dummy_image *img) { + auto dummy = (struct dummy_data *)base; + struct dummy_image *tmp = NULL; + HASH_FIND_INT(dummy->images, &img->pixmap, tmp); + if (!tmp) { + log_warn("Using an invalid (possibly freed) image"); + assert(false); + } + assert(*tmp->refcount > 0); +} + +void dummy_compose(struct backend_base *base, void *image, int dst_x1 attr_unused, + int dst_y1 attr_unused, int dst_x2 attr_unused, int dst_y2 attr_unused, + const region_t *reg_paint attr_unused, const region_t *reg_visible attr_unused) { + dummy_check_image(base, image); +} + +void dummy_fill(struct backend_base *backend_data attr_unused, struct color c attr_unused, + const region_t *clip attr_unused) { +} + +bool dummy_blur(struct backend_base *backend_data attr_unused, double opacity attr_unused, + void *blur_ctx attr_unused, const region_t *reg_blur attr_unused, + const region_t *reg_visible attr_unused) { + return true; +} + +void *dummy_bind_pixmap(struct backend_base *base, xcb_pixmap_t pixmap, + struct xvisual_info fmt, bool owned attr_unused) { + auto dummy = (struct dummy_data *)base; + struct dummy_image *img = NULL; + HASH_FIND_INT(dummy->images, &pixmap, img); + if (img) { + (*img->refcount)++; + return img; + } + + img = ccalloc(1, struct dummy_image); + img->pixmap = pixmap; + img->transparent = fmt.alpha_size != 0; + img->refcount = ccalloc(1, int); + *img->refcount = 1; + + HASH_ADD_INT(dummy->images, pixmap, img); + return (void *)img; +} + +void dummy_release_image(backend_t *base, void *image) { + auto dummy = (struct dummy_data *)base; + auto img = (struct dummy_image *)image; + assert(*img->refcount > 0); + (*img->refcount)--; + if (*img->refcount == 0) { + HASH_DEL(dummy->images, img); + free(img->refcount); + free(img); + } +} + +bool dummy_is_image_transparent(struct backend_base *base, void *image) { + auto img = (struct dummy_image *)image; + dummy_check_image(base, img); + return img->transparent; +} + +int dummy_buffer_age(struct backend_base *base attr_unused) { + return 2; +} + +bool dummy_image_op(struct backend_base *base, enum image_operations op attr_unused, + void *image, const region_t *reg_op attr_unused, + const region_t *reg_visible attr_unused, void *args attr_unused) { + dummy_check_image(base, image); + return true; +} + +bool dummy_set_image_property(struct backend_base *base, enum image_properties prop attr_unused, + void *image, void *arg attr_unused) { + dummy_check_image(base, image); + return true; +} + +void *dummy_clone_image(struct backend_base *base, const void *image, + const region_t *reg_visible attr_unused) { + auto img = (const struct dummy_image *)image; + dummy_check_image(base, img); + (*img->refcount)++; + return (void *)img; +} + +void *dummy_create_blur_context(struct backend_base *base attr_unused, + enum blur_method method attr_unused, void *args attr_unused) { + static int dummy_context; + return &dummy_context; +} + +void dummy_destroy_blur_context(struct backend_base *base attr_unused, void *ctx attr_unused) { +} + +void dummy_get_blur_size(void *ctx attr_unused, int *width, int *height) { + // These numbers are arbitrary, to make sure the reisze_region code path is + // covered. + *width = 5; + *height = 5; +} + +struct backend_operations dummy_ops = { + .init = dummy_init, + .deinit = dummy_deinit, + .compose = dummy_compose, + .fill = dummy_fill, + .blur = dummy_blur, + .bind_pixmap = dummy_bind_pixmap, + .render_shadow = default_backend_render_shadow, + .release_image = dummy_release_image, + .is_image_transparent = dummy_is_image_transparent, + .buffer_age = dummy_buffer_age, + .max_buffer_age = 5, + + .image_op = dummy_image_op, + .clone_image = dummy_clone_image, + .set_image_property = dummy_set_image_property, + .create_blur_context = dummy_create_blur_context, + .destroy_blur_context = dummy_destroy_blur_context, + .get_blur_size = dummy_get_blur_size, + +}; diff --git a/src/backend/gl/gl_common.c b/src/backend/gl/gl_common.c new file mode 100644 index 0000000..8cc5a05 --- /dev/null +++ b/src/backend/gl/gl_common.c @@ -0,0 +1,1922 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#include +#include +#include +#include +#include +#include +#include // for xcb_render_fixed_t, XXX + +#include "backend/backend.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "kernel.h" +#include "log.h" +#include "region.h" +#include "string_utils.h" +#include "types.h" +#include "utils.h" + +#include "backend/backend_common.h" +#include "backend/gl/gl_common.h" + +#define GLSL(version, ...) "#version " #version "\n" #__VA_ARGS__ +#define QUOTE(...) #__VA_ARGS__ + +static const GLuint vert_coord_loc = 0; +static const GLuint vert_in_texcoord_loc = 1; + +struct gl_blur_context { + enum blur_method method; + gl_blur_shader_t *blur_shader; + + /// Temporary textures used for blurring + GLuint *blur_textures; + int blur_texture_count; + /// Temporary fbos used for blurring + GLuint *blur_fbos; + int blur_fbo_count; + + /// Cached dimensions of each blur_texture. They are the same size as the target, + /// so they are always big enough without resizing. + /// Turns out calling glTexImage to resize is expensive, so we avoid that. + struct texture_size { + int width; + int height; + } * texture_sizes; + + /// Cached dimensions of the offscreen framebuffer. It's the same size as the + /// target but is expanded in either direction by resize_width / resize_height. + int fb_width, fb_height; + + /// How much do we need to resize the damaged region for blurring. + int resize_width, resize_height; + + int npasses; +}; + +static GLint glGetUniformLocationChecked(GLuint p, const char *name) { + auto ret = glGetUniformLocation(p, name); + if (ret < 0) { + log_info("Failed to get location of uniform '%s'. This is normal when " + "using custom shaders.", + name); + } + return ret; +} + +GLuint gl_create_shader(GLenum shader_type, const char *shader_str) { + log_trace("===\n%s\n===", shader_str); + + bool success = false; + GLuint shader = glCreateShader(shader_type); + if (!shader) { + log_error("Failed to create shader with type %#x.", shader_type); + goto end; + } + glShaderSource(shader, 1, &shader_str, NULL); + glCompileShader(shader); + + // Get shader status + { + GLint status = GL_FALSE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (GL_FALSE == status) { + GLint log_len = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_len); + if (log_len) { + char log[log_len + 1]; + glGetShaderInfoLog(shader, log_len, NULL, log); + log_error("Failed to compile shader with type %d: %s", + shader_type, log); + } + goto end; + } + } + + success = true; + +end: + if (shader && !success) { + glDeleteShader(shader); + shader = 0; + } + + return shader; +} + +GLuint gl_create_program(const GLuint *const shaders, int nshaders) { + bool success = false; + GLuint program = glCreateProgram(); + if (!program) { + log_error("Failed to create program."); + goto end; + } + + for (int i = 0; i < nshaders; ++i) + glAttachShader(program, shaders[i]); + glLinkProgram(program); + + // Get program status + { + GLint status = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &status); + if (GL_FALSE == status) { + GLint log_len = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_len); + if (log_len) { + char log[log_len + 1]; + glGetProgramInfoLog(program, log_len, NULL, log); + log_error("Failed to link program: %s", log); + } + goto end; + } + } + success = true; + +end: + if (program) { + for (int i = 0; i < nshaders; ++i) + glDetachShader(program, shaders[i]); + } + if (program && !success) { + glDeleteProgram(program); + program = 0; + } + + return program; +} + +/** + * @brief Create a program from vertex and fragment shader strings. + */ +GLuint gl_create_program_from_str(const char *vert_shader_str, const char *frag_shader_str) { + GLuint vert_shader = 0; + GLuint frag_shader = 0; + GLuint prog = 0; + + if (vert_shader_str) + vert_shader = gl_create_shader(GL_VERTEX_SHADER, vert_shader_str); + if (frag_shader_str) + frag_shader = gl_create_shader(GL_FRAGMENT_SHADER, frag_shader_str); + + { + GLuint shaders[2]; + int count = 0; + if (vert_shader) { + shaders[count++] = vert_shader; + } + if (frag_shader) { + shaders[count++] = frag_shader; + } + if (count) { + prog = gl_create_program(shaders, count); + } + } + + if (vert_shader) + glDeleteShader(vert_shader); + if (frag_shader) + glDeleteShader(frag_shader); + + return prog; +} + +static void gl_free_prog_main(gl_win_shader_t *pprogram) { + if (!pprogram) + return; + if (pprogram->prog) { + glDeleteProgram(pprogram->prog); + pprogram->prog = 0; + } +} + +/* + * @brief Implements recursive part of gl_average_texture_color. + * + * @note In order to reduce number of textures which needs to be + * allocated and deleted during this recursive render + * we reuse the same two textures for render source and + * destination simply by alterating between them. + * Unfortunately on first iteration source_texture might + * be read-only. In this case we will select auxiliary_texture as + * destination_texture in order not to touch that read-only source + * texture in following render iteration. + * Otherwise we simply will switch source and destination textures + * between each other on each render iteration. + */ +static GLuint +_gl_average_texture_color(backend_t *base, GLuint source_texture, GLuint destination_texture, + GLuint auxiliary_texture, GLuint fbo, int width, int height) { + const int max_width = 1; + const int max_height = 1; + const int from_width = next_power_of_two(width); + const int from_height = next_power_of_two(height); + const int to_width = from_width > max_width ? from_width / 2 : from_width; + const int to_height = from_height > max_height ? from_height / 2 : from_height; + + // Prepare coordinates + GLint coord[] = { + // top left + 0, 0, // vertex coord + 0, 0, // texture coord + + // top right + to_width, 0, // vertex coord + width, 0, // texture coord + + // bottom right + to_width, to_height, // vertex coord + width, height, // texture coord + + // bottom left + 0, to_height, // vertex coord + 0, height, // texture coord + }; + glBufferSubData(GL_ARRAY_BUFFER, 0, (long)sizeof(*coord) * 16, coord); + + // Prepare framebuffer for new render iteration + glBindTexture(GL_TEXTURE_2D, destination_texture); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + destination_texture, 0); + gl_check_fb_complete(GL_FRAMEBUFFER); + + // Bind source texture as downscaling shader uniform input + glBindTexture(GL_TEXTURE_2D, source_texture); + + // Render into framebuffer + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL); + + // Have we downscaled enough? + GLuint result; + if (to_width > max_width || to_height > max_height) { + GLuint new_source_texture = destination_texture; + GLuint new_destination_texture = + auxiliary_texture != 0 ? auxiliary_texture : source_texture; + result = _gl_average_texture_color(base, new_source_texture, + new_destination_texture, 0, fbo, + to_width, to_height); + } else { + result = destination_texture; + } + + return result; +} + +/* + * @brief Builds a 1x1 texture which has color corresponding to the average of all + * pixels of img by recursively rendering into texture of quorter the size (half + * width and half height). + * Returned texture must not be deleted, since it's owned by the gl_image. It will be + * deleted when the gl_image is released. + */ +static GLuint gl_average_texture_color(backend_t *base, struct backend_image *img) { + auto gd = (struct gl_data *)base; + auto inner = (struct gl_texture *)img->inner; + + // Prepare textures which will be used for destination and source of rendering + // during downscaling. + const int texture_count = ARR_SIZE(inner->auxiliary_texture); + if (!inner->auxiliary_texture[0]) { + assert(!inner->auxiliary_texture[1]); + glGenTextures(texture_count, inner->auxiliary_texture); + glActiveTexture(GL_TEXTURE0); + for (int i = 0; i < texture_count; i++) { + glBindTexture(GL_TEXTURE_2D, inner->auxiliary_texture[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, + (GLint[]){0, 0, 0, 0}); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, inner->width, + inner->height, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL); + } + } + + // Prepare framebuffer used for rendering and bind it + GLuint fbo; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + + // Enable shaders + glUseProgram(gd->brightness_shader.prog); + glUniform2f(glGetUniformLocationChecked(gd->brightness_shader.prog, "texsize"), + (GLfloat)inner->width, (GLfloat)inner->height); + + // Prepare vertex attributes + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + GLuint bo[2]; + glGenBuffers(2, bo); + glBindBuffer(GL_ARRAY_BUFFER, bo[0]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); + glEnableVertexAttribArray(vert_coord_loc); + glEnableVertexAttribArray(vert_in_texcoord_loc); + glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, + sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + + // Allocate buffers for render input + GLint coord[16] = {0}; + GLuint indices[] = {0, 1, 2, 2, 3, 0}; + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * 16, coord, GL_DYNAMIC_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * 6, indices, + GL_STATIC_DRAW); + + // Do actual recursive render to 1x1 texture + GLuint result_texture = _gl_average_texture_color( + base, inner->texture, inner->auxiliary_texture[0], + inner->auxiliary_texture[1], fbo, inner->width, inner->height); + + // Cleanup vertex attributes + glDisableVertexAttribArray(vert_coord_loc); + glDisableVertexAttribArray(vert_in_texcoord_loc); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDeleteBuffers(2, bo); + glBindVertexArray(0); + glDeleteVertexArrays(1, &vao); + + // Cleanup shaders + glUseProgram(0); + + // Cleanup framebuffers + glDeleteFramebuffers(1, &fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glDrawBuffer(GL_BACK); + + // Cleanup render textures + glBindTexture(GL_TEXTURE_2D, 0); + + gl_check_err(); + + return result_texture; +} + +/** + * Render a region with texture data. + * + * @param ptex the texture + * @param target the framebuffer to render into + * @param dst_x,dst_y the top left corner of region where this texture + * should go. In OpenGL coordinate system (important!). + * @param reg_tgt the clip region, in Xorg coordinate system + * @param reg_visible ignored + */ +static void _gl_compose(backend_t *base, struct backend_image *img, GLuint target, + GLint *coord, GLuint *indices, int nrects) { + auto gd = (struct gl_data *)base; + auto inner = (struct gl_texture *)img->inner; + if (!img || !inner->texture) { + log_error("Missing texture."); + return; + } + + GLuint brightness = 0; + if (img->max_brightness < 1.0) { + brightness = gl_average_texture_color(base, img); + } + + assert(gd->win_shader.prog); + glUseProgram(gd->win_shader.prog); + if (gd->win_shader.unifm_opacity >= 0) { + glUniform1f(gd->win_shader.unifm_opacity, (float)img->opacity); + } + if (gd->win_shader.unifm_invert_color >= 0) { + glUniform1i(gd->win_shader.unifm_invert_color, img->color_inverted); + } + if (gd->win_shader.unifm_tex >= 0) { + glUniform1i(gd->win_shader.unifm_tex, 0); + } + if (gd->win_shader.unifm_dim >= 0) { + glUniform1f(gd->win_shader.unifm_dim, (float)img->dim); + } + if (gd->win_shader.unifm_brightness >= 0) { + glUniform1i(gd->win_shader.unifm_brightness, 1); + } + if (gd->win_shader.unifm_max_brightness >= 0) { + glUniform1f(gd->win_shader.unifm_max_brightness, (float)img->max_brightness); + } + + // log_trace("Draw: %d, %d, %d, %d -> %d, %d (%d, %d) z %d\n", + // x, y, width, height, dx, dy, ptex->width, ptex->height, z); + + // Bind texture + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, brightness); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, inner->texture); + + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + GLuint bo[2]; + glGenBuffers(2, bo); + glBindBuffer(GL_ARRAY_BUFFER, bo[0]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * nrects * 16, coord, GL_STATIC_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * nrects * 6, + indices, GL_STATIC_DRAW); + + glEnableVertexAttribArray(vert_coord_loc); + glEnableVertexAttribArray(vert_in_texcoord_loc); + glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, + sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target); + glDrawElements(GL_TRIANGLES, nrects * 6, GL_UNSIGNED_INT, NULL); + + glDisableVertexAttribArray(vert_coord_loc); + glDisableVertexAttribArray(vert_in_texcoord_loc); + glBindVertexArray(0); + glDeleteVertexArrays(1, &vao); + + // Cleanup + glBindTexture(GL_TEXTURE_2D, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glDrawBuffer(GL_BACK); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDeleteBuffers(2, bo); + + glUseProgram(0); + + gl_check_err(); + + return; +} + +/// Convert rectangles in X coordinates to OpenGL vertex and texture coordinates +/// @param[in] nrects, rects rectangles +/// @param[in] dst_x, dst_y origin of the OpenGL texture, affect the calculated texture +/// coordinates +/// @param[in] texture_height height of the OpenGL texture +/// @param[in] root_height height of the back buffer +/// @param[in] y_inverted whether the texture is y inverted +/// @param[out] coord, indices output +static void +x_rect_to_coords(int nrects, const rect_t *rects, int dst_x, int dst_y, int texture_height, + int root_height, bool y_inverted, GLint *coord, GLuint *indices) { + dst_y = root_height - dst_y; + if (y_inverted) { + dst_y -= texture_height; + } + + for (int i = 0; i < nrects; i++) { + // Y-flip. Note after this, crect.y1 > crect.y2 + rect_t crect = rects[i]; + crect.y1 = root_height - crect.y1; + crect.y2 = root_height - crect.y2; + + // Calculate texture coordinates + // (texture_x1, texture_y1), texture coord for the _bottom left_ corner + GLint texture_x1 = crect.x1 - dst_x, texture_y1 = crect.y2 - dst_y, + texture_x2 = texture_x1 + (crect.x2 - crect.x1), + texture_y2 = texture_y1 + (crect.y1 - crect.y2); + + // X pixmaps might be Y inverted, invert the texture coordinates + if (y_inverted) { + texture_y1 = texture_height - texture_y1; + texture_y2 = texture_height - texture_y2; + } + + // Vertex coordinates + auto vx1 = crect.x1; + auto vy1 = crect.y2; + auto vx2 = crect.x2; + auto vy2 = crect.y1; + + // log_trace("Rect %d: %f, %f, %f, %f -> %d, %d, %d, %d", + // ri, rx, ry, rxe, rye, rdx, rdy, rdxe, rdye); + + memcpy(&coord[i * 16], + ((GLint[][2]){ + {vx1, vy1}, + {texture_x1, texture_y1}, + {vx2, vy1}, + {texture_x2, texture_y1}, + {vx2, vy2}, + {texture_x2, texture_y2}, + {vx1, vy2}, + {texture_x1, texture_y2}, + }), + sizeof(GLint[2]) * 8); + + GLuint u = (GLuint)(i * 4); + memcpy(&indices[i * 6], + ((GLuint[]){u + 0, u + 1, u + 2, u + 2, u + 3, u + 0}), + sizeof(GLuint) * 6); + } +} + +// TODO(yshui) make use of reg_visible +void gl_compose(backend_t *base, void *image_data, + int dst_x1, int dst_y1, int dst_x2, int dst_y2, + const region_t *reg_tgt, const region_t *reg_visible attr_unused) { + auto gd = (struct gl_data *)base; + struct backend_image *img = image_data; + auto inner = (struct gl_texture *)img->inner; + + // Painting + int nrects; + const rect_t *rects; + rects = pixman_region32_rectangles((region_t *)reg_tgt, &nrects); + if (!nrects) { + // Nothing to paint + return; + } + + // Until we start to use glClipControl, reg_tgt, dst_x and dst_y and + // in a different coordinate system than the one OpenGL uses. + // OpenGL window coordinate (or NDC) has the origin at the lower left of the + // screen, with y axis pointing up; Xorg has the origin at the upper left of the + // screen, with y axis pointing down. We have to do some coordinate conversion in + // this function + + auto coord = ccalloc(nrects * 16, GLint); + auto indices = ccalloc(nrects * 6, GLuint); + x_rect_to_coords(nrects, rects, dst_x1, dst_y1, inner->height, gd->height, + inner->y_inverted, coord, indices); + + // Interpolate the texture coordinates into the specified range + for (unsigned int i = 2; i < 16; i+=4) { + coord[i+0] = lerp_range(0, dst_x2 - dst_x1, 0, inner->width, coord[i+0]); + coord[i+1] = lerp_range(0, dst_y2 - dst_y1, 0, inner->height, coord[i+1]); + } + + _gl_compose(base, img, gd->back_fbo, coord, indices, nrects); + + free(indices); + free(coord); +} + +/** + * Blur contents in a particular region. + */ +bool gl_kernel_blur(backend_t *base, double opacity, void *ctx, const rect_t *extent, + const GLuint vao[2], const int vao_nelems[2]) { + auto bctx = (struct gl_blur_context *)ctx; + auto gd = (struct gl_data *)base; + + int dst_y_fb_coord = bctx->fb_height - extent->y2; + + int curr = 0; + for (int i = 0; i < bctx->npasses; ++i) { + const gl_blur_shader_t *p = &bctx->blur_shader[i]; + assert(p->prog); + + assert(bctx->blur_textures[curr]); + + // The origin to use when sampling from the source texture + GLint texorig_x = extent->x1, texorig_y = dst_y_fb_coord; + GLint tex_width, tex_height; + GLuint src_texture; + + if (i == 0) { + src_texture = gd->back_texture; + tex_width = gd->width; + tex_height = gd->height; + } else { + src_texture = bctx->blur_textures[curr]; + auto src_size = bctx->texture_sizes[curr]; + tex_width = src_size.width; + tex_height = src_size.height; + } + + glBindTexture(GL_TEXTURE_2D, src_texture); + glUseProgram(p->prog); + glUniform2f(p->unifm_pixel_norm, 1.0f / (GLfloat)tex_width, + 1.0f / (GLfloat)tex_height); + + // The number of indices in the selected vertex array + GLsizei nelems; + + if (i < bctx->npasses - 1) { + assert(bctx->blur_fbos[0]); + assert(bctx->blur_textures[!curr]); + + // not last pass, draw into framebuffer, with resized regions + glBindVertexArray(vao[1]); + nelems = vao_nelems[1]; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[0]); + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, bctx->blur_textures[!curr], 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + if (!gl_check_fb_complete(GL_FRAMEBUFFER)) { + return false; + } + + glUniform1f(p->unifm_opacity, 1.0); + } else { + // last pass, draw directly into the back buffer, with origin + // regions + glBindVertexArray(vao[0]); + nelems = vao_nelems[0]; + glBindFramebuffer(GL_FRAMEBUFFER, gd->back_fbo); + + glUniform1f(p->unifm_opacity, (float)opacity); + } + + glUniform2f(p->texorig_loc, (GLfloat)texorig_x, (GLfloat)texorig_y); + glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + + // XXX use multiple draw calls is probably going to be slow than + // just simply blur the whole area. + + curr = !curr; + } + + return true; +} + +bool gl_dual_kawase_blur(backend_t *base, double opacity, void *ctx, const rect_t *extent, + const GLuint vao[2], const int vao_nelems[2]) { + auto bctx = (struct gl_blur_context *)ctx; + auto gd = (struct gl_data *)base; + + int dst_y_fb_coord = bctx->fb_height - extent->y2; + + int iterations = bctx->blur_texture_count; + int scale_factor = 1; + + // Kawase downsample pass + const gl_blur_shader_t *down_pass = &bctx->blur_shader[0]; + assert(down_pass->prog); + glUseProgram(down_pass->prog); + + glUniform2f(down_pass->texorig_loc, (GLfloat)extent->x1, (GLfloat)dst_y_fb_coord); + + for (int i = 0; i < iterations; ++i) { + // Scale output width / height by half in each iteration + scale_factor <<= 1; + + GLuint src_texture; + int tex_width, tex_height; + + if (i == 0) { + // first pass: copy from back buffer + src_texture = gd->back_texture; + tex_width = gd->width; + tex_height = gd->height; + } else { + // copy from previous pass + src_texture = bctx->blur_textures[i - 1]; + auto src_size = bctx->texture_sizes[i - 1]; + tex_width = src_size.width; + tex_height = src_size.height; + } + + assert(src_texture); + assert(bctx->blur_fbos[i]); + + glBindTexture(GL_TEXTURE_2D, src_texture); + glBindVertexArray(vao[1]); + auto nelems = vao_nelems[1]; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i]); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + + glUniform1f(down_pass->scale_loc, (GLfloat)scale_factor); + + glUniform2f(down_pass->unifm_pixel_norm, 1.0f / (GLfloat)tex_width, + 1.0f / (GLfloat)tex_height); + + glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + } + + // Kawase upsample pass + const gl_blur_shader_t *up_pass = &bctx->blur_shader[1]; + assert(up_pass->prog); + glUseProgram(up_pass->prog); + + glUniform2f(up_pass->texorig_loc, (GLfloat)extent->x1, (GLfloat)dst_y_fb_coord); + + for (int i = iterations - 1; i >= 0; --i) { + // Scale output width / height back by two in each iteration + scale_factor >>= 1; + + const GLuint src_texture = bctx->blur_textures[i]; + assert(src_texture); + + // Calculate normalized half-width/-height of a src pixel + auto src_size = bctx->texture_sizes[i]; + int tex_width = src_size.width; + int tex_height = src_size.height; + + // The number of indices in the selected vertex array + GLsizei nelems; + + glBindTexture(GL_TEXTURE_2D, src_texture); + if (i > 0) { + assert(bctx->blur_fbos[i - 1]); + + // not last pass, draw into next framebuffer + glBindVertexArray(vao[1]); + nelems = vao_nelems[1]; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i - 1]); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + + glUniform1f(up_pass->unifm_opacity, (GLfloat)1); + } else { + // last pass, draw directly into the back buffer + glBindVertexArray(vao[0]); + nelems = vao_nelems[0]; + glBindFramebuffer(GL_FRAMEBUFFER, gd->back_fbo); + + glUniform1f(up_pass->unifm_opacity, (GLfloat)opacity); + } + + glUniform1f(up_pass->scale_loc, (GLfloat)scale_factor); + glUniform2f(up_pass->unifm_pixel_norm, 1.0f / (GLfloat)tex_width, + 1.0f / (GLfloat)tex_height); + + glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + } + + return true; +} + +bool gl_blur(backend_t *base, double opacity, void *ctx, const region_t *reg_blur, + const region_t *reg_visible attr_unused) { + auto bctx = (struct gl_blur_context *)ctx; + auto gd = (struct gl_data *)base; + + bool ret = false; + + if (gd->width != bctx->fb_width || gd->height != bctx->fb_height) { + // Resize the temporary textures used for blur in case the root + // size changed + bctx->fb_width = gd->width; + bctx->fb_height = gd->height; + + for (int i = 0; i < bctx->blur_texture_count; ++i) { + auto tex_size = bctx->texture_sizes + i; + if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { + // Use smaller textures for each iteration (quarter of the + // previous texture) + tex_size->width = 1 + ((bctx->fb_width - 1) >> (i + 1)); + tex_size->height = 1 + ((bctx->fb_height - 1) >> (i + 1)); + } else { + tex_size->width = bctx->fb_width; + tex_size->height = bctx->fb_height; + } + + glBindTexture(GL_TEXTURE_2D, bctx->blur_textures[i]); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, tex_size->width, + tex_size->height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); + + if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { + // Attach texture to FBO target + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i]); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + bctx->blur_textures[i], 0); + if (!gl_check_fb_complete(GL_FRAMEBUFFER)) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return false; + } + } + } + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + + // Remainder: regions are in Xorg coordinates + auto reg_blur_resized = + resize_region(reg_blur, bctx->resize_width, bctx->resize_height); + const rect_t *extent = pixman_region32_extents((region_t *)reg_blur), + *extent_resized = pixman_region32_extents(®_blur_resized); + int width = extent->x2 - extent->x1, height = extent->y2 - extent->y1; + if (width == 0 || height == 0) { + return true; + } + + int nrects, nrects_resized; + const rect_t *rects = pixman_region32_rectangles((region_t *)reg_blur, &nrects), + *rects_resized = + pixman_region32_rectangles(®_blur_resized, &nrects_resized); + if (!nrects || !nrects_resized) { + return true; + } + + auto coord = ccalloc(nrects * 16, GLint); + auto indices = ccalloc(nrects * 6, GLuint); + x_rect_to_coords(nrects, rects, extent_resized->x1, extent_resized->y2, + bctx->fb_height, gd->height, false, coord, indices); + + auto coord_resized = ccalloc(nrects_resized * 16, GLint); + auto indices_resized = ccalloc(nrects_resized * 6, GLuint); + x_rect_to_coords(nrects_resized, rects_resized, extent_resized->x1, + extent_resized->y2, bctx->fb_height, bctx->fb_height, false, + coord_resized, indices_resized); + pixman_region32_fini(®_blur_resized); + + GLuint vao[2]; + glGenVertexArrays(2, vao); + GLuint bo[4]; + glGenBuffers(4, bo); + + glBindVertexArray(vao[0]); + glBindBuffer(GL_ARRAY_BUFFER, bo[0]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * nrects * 16, coord, GL_STATIC_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * nrects * 6, + indices, GL_STATIC_DRAW); + glEnableVertexAttribArray(vert_coord_loc); + glEnableVertexAttribArray(vert_in_texcoord_loc); + glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, + sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + + glBindVertexArray(vao[1]); + glBindBuffer(GL_ARRAY_BUFFER, bo[2]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[3]); + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord_resized) * nrects_resized * 16, + coord_resized, GL_STATIC_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, + (long)sizeof(*indices_resized) * nrects_resized * 6, indices_resized, + GL_STATIC_DRAW); + glEnableVertexAttribArray(vert_coord_loc); + glEnableVertexAttribArray(vert_in_texcoord_loc); + glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, + sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + + int vao_nelems[2] = {nrects * 6, nrects_resized * 6}; + + if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { + ret = gl_dual_kawase_blur(base, opacity, ctx, extent_resized, vao, vao_nelems); + } else { + ret = gl_kernel_blur(base, opacity, ctx, extent_resized, vao, vao_nelems); + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDeleteBuffers(4, bo); + glBindVertexArray(0); + glDeleteVertexArrays(2, vao); + glUseProgram(0); + + free(indices); + free(coord); + free(indices_resized); + free(coord_resized); + + gl_check_err(); + return ret; +} + +// clang-format off +const char *vertex_shader = GLSL(330, + uniform mat4 projection; + uniform float scale = 1.0; + uniform vec2 texorig; + layout(location = 0) in vec2 coord; + layout(location = 1) in vec2 in_texcoord; + out vec2 texcoord; + void main() { + gl_Position = projection * vec4(coord, 0, scale); + texcoord = in_texcoord + texorig; + } +); +// clang-format on + +/** + * Load a GLSL main program from shader strings. + */ +static int gl_win_shader_from_string(const char *vshader_str, const char *fshader_str, + gl_win_shader_t *ret) { + // Build program + ret->prog = gl_create_program_from_str(vshader_str, fshader_str); + if (!ret->prog) { + log_error("Failed to create GLSL program."); + return -1; + } + + // Get uniform addresses + ret->unifm_opacity = glGetUniformLocationChecked(ret->prog, "opacity"); + ret->unifm_invert_color = glGetUniformLocationChecked(ret->prog, "invert_color"); + ret->unifm_tex = glGetUniformLocationChecked(ret->prog, "tex"); + ret->unifm_dim = glGetUniformLocationChecked(ret->prog, "dim"); + ret->unifm_brightness = glGetUniformLocationChecked(ret->prog, "brightness"); + ret->unifm_max_brightness = + glGetUniformLocationChecked(ret->prog, "max_brightness"); + + gl_check_err(); + + return true; +} + +/** + * Callback to run on root window size change. + */ +void gl_resize(struct gl_data *gd, int width, int height) { + GLint viewport_dimensions[2]; + glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); + + gd->height = height; + gd->width = width; + + assert(viewport_dimensions[0] >= gd->width); + assert(viewport_dimensions[1] >= gd->height); + + glBindTexture(GL_TEXTURE_2D, gd->back_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_BGR, + GL_UNSIGNED_BYTE, NULL); + + gl_check_err(); +} + +// clang-format off +static const char dummy_frag[] = GLSL(330, + uniform sampler2D tex; + in vec2 texcoord; + void main() { + gl_FragColor = texelFetch(tex, ivec2(texcoord.xy), 0); + } +); + +static const char fill_frag[] = GLSL(330, + uniform vec4 color; + void main() { + gl_FragColor = color; + } +); + +static const char fill_vert[] = GLSL(330, + layout(location = 0) in vec2 in_coord; + uniform mat4 projection; + void main() { + gl_Position = projection * vec4(in_coord, 0, 1); + } +); + +static const char interpolating_frag[] = GLSL(330, + uniform sampler2D tex; + in vec2 texcoord; + void main() { + gl_FragColor = vec4(texture2D(tex, vec2(texcoord.xy), 0).rgb, 1); + } +); + +static const char interpolating_vert[] = GLSL(330, + uniform mat4 projection; + uniform vec2 texsize; + layout(location = 0) in vec2 in_coord; + layout(location = 1) in vec2 in_texcoord; + out vec2 texcoord; + void main() { + gl_Position = projection * vec4(in_coord, 0, 1); + texcoord = in_texcoord / texsize; + } +); +// clang-format on + +/// Fill a given region in bound framebuffer. +/// @param[in] y_inverted whether the y coordinates in `clip` should be inverted +static void _gl_fill(backend_t *base, struct color c, const region_t *clip, GLuint target, + int height, bool y_inverted) { + static const GLuint fill_vert_in_coord_loc = 0; + int nrects; + const rect_t *rect = pixman_region32_rectangles((region_t *)clip, &nrects); + auto gd = (struct gl_data *)base; + + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + GLuint bo[2]; + glGenBuffers(2, bo); + glUseProgram(gd->fill_shader.prog); + glUniform4f(gd->fill_shader.color_loc, (GLfloat)c.red, (GLfloat)c.green, + (GLfloat)c.blue, (GLfloat)c.alpha); + glEnableVertexAttribArray(fill_vert_in_coord_loc); + glBindBuffer(GL_ARRAY_BUFFER, bo[0]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); + + auto coord = ccalloc(nrects * 8, GLint); + auto indices = ccalloc(nrects * 6, GLuint); + for (int i = 0; i < nrects; i++) { + GLint y1 = y_inverted ? height - rect[i].y2 : rect[i].y1, + y2 = y_inverted ? height - rect[i].y1 : rect[i].y2; + // clang-format off + memcpy(&coord[i * 8], + ((GLint[][2]){ + {rect[i].x1, y1}, {rect[i].x2, y1}, + {rect[i].x2, y2}, {rect[i].x1, y2}}), + sizeof(GLint[2]) * 4); + // clang-format on + indices[i * 6 + 0] = (GLuint)i * 4 + 0; + indices[i * 6 + 1] = (GLuint)i * 4 + 1; + indices[i * 6 + 2] = (GLuint)i * 4 + 2; + indices[i * 6 + 3] = (GLuint)i * 4 + 2; + indices[i * 6 + 4] = (GLuint)i * 4 + 3; + indices[i * 6 + 5] = (GLuint)i * 4 + 0; + } + glBufferData(GL_ARRAY_BUFFER, nrects * 8 * (long)sizeof(*coord), coord, GL_STREAM_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, nrects * 6 * (long)sizeof(*indices), + indices, GL_STREAM_DRAW); + + glVertexAttribPointer(fill_vert_in_coord_loc, 2, GL_INT, GL_FALSE, + sizeof(*coord) * 2, (void *)0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target); + glDrawElements(GL_TRIANGLES, nrects * 6, GL_UNSIGNED_INT, NULL); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDisableVertexAttribArray(fill_vert_in_coord_loc); + glBindVertexArray(0); + glDeleteVertexArrays(1, &vao); + + glDeleteBuffers(2, bo); + free(indices); + free(coord); + + gl_check_err(); +} + +void gl_fill(backend_t *base, struct color c, const region_t *clip) { + auto gd = (struct gl_data *)base; + return _gl_fill(base, c, clip, gd->back_fbo, gd->height, true); +} + +static void gl_release_image_inner(backend_t *base, struct gl_texture *inner) { + auto gd = (struct gl_data *)base; + gd->release_user_data(base, inner); + assert(inner->user_data == NULL); + + glDeleteTextures(1, &inner->texture); + glDeleteTextures(2, inner->auxiliary_texture); + free(inner); + gl_check_err(); +} + +void gl_release_image(backend_t *base, void *image_data) { + struct backend_image *wd = image_data; + auto inner = (struct gl_texture *)wd->inner; + inner->refcount--; + assert(inner->refcount >= 0); + if (inner->refcount == 0) { + gl_release_image_inner(base, inner); + } + free(wd); +} + +static inline void gl_free_blur_shader(gl_blur_shader_t *shader) { + if (shader->prog) { + glDeleteProgram(shader->prog); + } + + shader->prog = 0; +} + +void gl_destroy_blur_context(backend_t *base attr_unused, void *ctx) { + auto bctx = (struct gl_blur_context *)ctx; + // Free GLSL shaders/programs + for (int i = 0; i < bctx->npasses; ++i) { + gl_free_blur_shader(&bctx->blur_shader[i]); + } + free(bctx->blur_shader); + + if (bctx->blur_texture_count && bctx->blur_textures) { + glDeleteTextures(bctx->blur_texture_count, bctx->blur_textures); + free(bctx->blur_textures); + } + if (bctx->blur_texture_count && bctx->texture_sizes) { + free(bctx->texture_sizes); + } + if (bctx->blur_fbo_count && bctx->blur_fbos) { + glDeleteFramebuffers(bctx->blur_fbo_count, bctx->blur_fbos); + free(bctx->blur_fbos); + } + + bctx->blur_texture_count = 0; + bctx->blur_fbo_count = 0; + + free(bctx); + + gl_check_err(); +} + +/** + * Initialize GL blur filters. + */ +bool gl_create_kernel_blur_context(void *blur_context, GLfloat *projection, + enum blur_method method, void *args) { + bool success = false; + auto ctx = (struct gl_blur_context *)blur_context; + + struct conv **kernels; + + int nkernels; + ctx->method = BLUR_METHOD_KERNEL; + if (method == BLUR_METHOD_KERNEL) { + nkernels = ((struct kernel_blur_args *)args)->kernel_count; + kernels = ((struct kernel_blur_args *)args)->kernels; + } else { + kernels = generate_blur_kernel(method, args, &nkernels); + } + + if (!nkernels) { + ctx->method = BLUR_METHOD_NONE; + return true; + } + + // Specify required textures and FBOs + ctx->blur_texture_count = 2; + ctx->blur_fbo_count = 1; + + ctx->blur_shader = ccalloc(max2(2, nkernels), gl_blur_shader_t); + + char *lc_numeric_old = strdup(setlocale(LC_NUMERIC, NULL)); + // Enforce LC_NUMERIC locale "C" here to make sure decimal point is sane + // Thanks to hiciu for reporting. + setlocale(LC_NUMERIC, "C"); + + // clang-format off + static const char *FRAG_SHADER_BLUR = GLSL(330, + %s\n // other extension pragmas + uniform sampler2D tex_src; + uniform vec2 pixel_norm; + uniform float opacity; + in vec2 texcoord; + out vec4 out_color; + void main() { + vec2 uv = texcoord * pixel_norm; + vec4 sum = vec4(0.0, 0.0, 0.0, 0.0); + %s //body of the convolution + out_color = sum / float(%.7g) * opacity; + } + ); + static const char *FRAG_SHADER_BLUR_ADD = QUOTE( + sum += float(%.7g) * texture2D(tex_src, uv + pixel_norm * vec2(%.7g, %.7g)); + ); + // clang-format on + + const char *shader_add = FRAG_SHADER_BLUR_ADD; + char *extension = strdup(""); + + for (int i = 0; i < nkernels; i++) { + auto kern = kernels[i]; + // Build shader + int width = kern->w, height = kern->h; + int nele = width * height; + // '%.7g' is at most 14 characters, inserted 3 times + size_t body_len = (strlen(shader_add) + 42) * (uint)nele; + char *shader_body = ccalloc(body_len, char); + char *pc = shader_body; + + // Make use of the linear interpolation hardware by sampling 2 pixels with + // one texture access by sampling between both pixels based on their + // relative weight. Easiest done in a single dimension as 2D bilinear + // filtering would raise additional constraints on the kernels. Therefore + // only use interpolation along the larger dimension. + double sum = 0.0; + if (width > height) { + // use interpolation in x dimension (width) + for (int j = 0; j < height; ++j) { + for (int k = 0; k < width; k += 2) { + double val1, val2; + val1 = kern->data[j * width + k]; + val2 = (k + 1 < width) + ? kern->data[j * width + k + 1] + : 0; + + double combined_weight = val1 + val2; + if (combined_weight == 0) { + continue; + } + sum += combined_weight; + + double offset_x = + k + (val2 / combined_weight) - (width / 2); + double offset_y = j - (height / 2); + pc += snprintf( + pc, body_len - (ulong)(pc - shader_body), + shader_add, combined_weight, offset_x, offset_y); + assert(pc < shader_body + body_len); + } + } + } else { + // use interpolation in y dimension (height) + for (int j = 0; j < height; j += 2) { + for (int k = 0; k < width; ++k) { + double val1, val2; + val1 = kern->data[j * width + k]; + val2 = (j + 1 < height) + ? kern->data[(j + 1) * width + k] + : 0; + + double combined_weight = val1 + val2; + if (combined_weight == 0) { + continue; + } + sum += combined_weight; + + double offset_x = k - (width / 2); + double offset_y = + j + (val2 / combined_weight) - (height / 2); + pc += snprintf( + pc, body_len - (ulong)(pc - shader_body), + shader_add, combined_weight, offset_x, offset_y); + assert(pc < shader_body + body_len); + } + } + } + + auto pass = ctx->blur_shader + i; + size_t shader_len = strlen(FRAG_SHADER_BLUR) + strlen(extension) + + strlen(shader_body) + 10 /* sum */ + + 1 /* null terminator */; + char *shader_str = ccalloc(shader_len, char); + auto real_shader_len = snprintf(shader_str, shader_len, FRAG_SHADER_BLUR, + extension, shader_body, sum); + CHECK(real_shader_len >= 0); + CHECK((size_t)real_shader_len < shader_len); + free(shader_body); + + // Build program + pass->prog = gl_create_program_from_str(vertex_shader, shader_str); + free(shader_str); + if (!pass->prog) { + log_error("Failed to create GLSL program."); + success = false; + goto out; + } + glBindFragDataLocation(pass->prog, 0, "out_color"); + + // Get uniform addresses + pass->unifm_pixel_norm = + glGetUniformLocationChecked(pass->prog, "pixel_norm"); + pass->unifm_opacity = glGetUniformLocationChecked(pass->prog, "opacity"); + pass->texorig_loc = glGetUniformLocationChecked(pass->prog, "texorig"); + + // Setup projection matrix + glUseProgram(pass->prog); + int pml = glGetUniformLocationChecked(pass->prog, "projection"); + glUniformMatrix4fv(pml, 1, false, projection); + glUseProgram(0); + + ctx->resize_width += kern->w / 2; + ctx->resize_height += kern->h / 2; + } + + if (nkernels == 1) { + // Generate an extra null pass so we don't need special code path for + // the single pass case + auto pass = &ctx->blur_shader[1]; + pass->prog = gl_create_program_from_str(vertex_shader, dummy_frag); + pass->unifm_pixel_norm = -1; + pass->unifm_opacity = -1; + pass->texorig_loc = glGetUniformLocationChecked(pass->prog, "texorig"); + + // Setup projection matrix + glUseProgram(pass->prog); + int pml = glGetUniformLocationChecked(pass->prog, "projection"); + glUniformMatrix4fv(pml, 1, false, projection); + glUseProgram(0); + + ctx->npasses = 2; + } else { + ctx->npasses = nkernels; + } + + success = true; +out: + if (method != BLUR_METHOD_KERNEL) { + // We generated the blur kernels, so we need to free them + for (int i = 0; i < nkernels; i++) { + free(kernels[i]); + } + free(kernels); + } + + free(extension); + // Restore LC_NUMERIC + setlocale(LC_NUMERIC, lc_numeric_old); + free(lc_numeric_old); + + return success; +} + +bool gl_create_dual_kawase_blur_context(void *blur_context, GLfloat *projection, + enum blur_method method, void *args) { + bool success = false; + auto ctx = (struct gl_blur_context *)blur_context; + + ctx->method = method; + + auto blur_params = generate_dual_kawase_params(args); + + // Specify required textures and FBOs + ctx->blur_texture_count = blur_params->iterations; + ctx->blur_fbo_count = blur_params->iterations; + + ctx->resize_width += blur_params->expand; + ctx->resize_height += blur_params->expand; + + ctx->npasses = 2; + ctx->blur_shader = ccalloc(ctx->npasses, gl_blur_shader_t); + + char *lc_numeric_old = strdup(setlocale(LC_NUMERIC, NULL)); + // Enforce LC_NUMERIC locale "C" here to make sure decimal point is sane + // Thanks to hiciu for reporting. + setlocale(LC_NUMERIC, "C"); + + // Dual-kawase downsample shader / program + auto down_pass = ctx->blur_shader; + { + // clang-format off + static const char *FRAG_SHADER_DOWN = GLSL(330, + uniform sampler2D tex_src; + uniform float scale = 1.0; + uniform vec2 pixel_norm; + in vec2 texcoord; + out vec4 out_color; + void main() { + vec2 offset = %.7g * pixel_norm; + vec2 uv = texcoord * pixel_norm * (2.0 / scale); + vec4 sum = texture2D(tex_src, uv) * 4.0; + sum += texture2D(tex_src, uv - vec2(0.5, 0.5) * offset); + sum += texture2D(tex_src, uv + vec2(0.5, 0.5) * offset); + sum += texture2D(tex_src, uv + vec2(0.5, -0.5) * offset); + sum += texture2D(tex_src, uv - vec2(0.5, -0.5) * offset); + out_color = sum / 8.0; + } + ); + // clang-format on + + // Build shader + size_t shader_len = + strlen(FRAG_SHADER_DOWN) + 10 /* offset */ + 1 /* null terminator */; + char *shader_str = ccalloc(shader_len, char); + auto real_shader_len = + snprintf(shader_str, shader_len, FRAG_SHADER_DOWN, blur_params->offset); + CHECK(real_shader_len >= 0); + CHECK((size_t)real_shader_len < shader_len); + + // Build program + down_pass->prog = gl_create_program_from_str(vertex_shader, shader_str); + free(shader_str); + if (!down_pass->prog) { + log_error("Failed to create GLSL program."); + success = false; + goto out; + } + glBindFragDataLocation(down_pass->prog, 0, "out_color"); + + // Get uniform addresses + down_pass->unifm_pixel_norm = + glGetUniformLocationChecked(down_pass->prog, "pixel_norm"); + down_pass->texorig_loc = + glGetUniformLocationChecked(down_pass->prog, "texorig"); + down_pass->scale_loc = + glGetUniformLocationChecked(down_pass->prog, "scale"); + + // Setup projection matrix + glUseProgram(down_pass->prog); + int pml = glGetUniformLocationChecked(down_pass->prog, "projection"); + glUniformMatrix4fv(pml, 1, false, projection); + glUseProgram(0); + } + + // Dual-kawase upsample shader / program + auto up_pass = ctx->blur_shader + 1; + { + // clang-format off + static const char *FRAG_SHADER_UP = GLSL(330, + uniform sampler2D tex_src; + uniform float scale = 1.0; + uniform vec2 pixel_norm; + uniform float opacity; + in vec2 texcoord; + out vec4 out_color; + void main() { + vec2 offset = %.7g * pixel_norm; + vec2 uv = texcoord * pixel_norm / (2 * scale); + vec4 sum = texture2D(tex_src, uv + vec2(-1.0, 0.0) * offset); + sum += texture2D(tex_src, uv + vec2(-0.5, 0.5) * offset) * 2.0; + sum += texture2D(tex_src, uv + vec2(0.0, 1.0) * offset); + sum += texture2D(tex_src, uv + vec2(0.5, 0.5) * offset) * 2.0; + sum += texture2D(tex_src, uv + vec2(1.0, 0.0) * offset); + sum += texture2D(tex_src, uv + vec2(0.5, -0.5) * offset) * 2.0; + sum += texture2D(tex_src, uv + vec2(0.0, -1.0) * offset); + sum += texture2D(tex_src, uv + vec2(-0.5, -0.5) * offset) * 2.0; + out_color = sum / 12.0 * opacity; + } + ); + // clang-format on + + // Build shader + size_t shader_len = + strlen(FRAG_SHADER_UP) + 10 /* offset */ + 1 /* null terminator */; + char *shader_str = ccalloc(shader_len, char); + auto real_shader_len = + snprintf(shader_str, shader_len, FRAG_SHADER_UP, blur_params->offset); + CHECK(real_shader_len >= 0); + CHECK((size_t)real_shader_len < shader_len); + + // Build program + up_pass->prog = gl_create_program_from_str(vertex_shader, shader_str); + free(shader_str); + if (!up_pass->prog) { + log_error("Failed to create GLSL program."); + success = false; + goto out; + } + glBindFragDataLocation(up_pass->prog, 0, "out_color"); + + // Get uniform addresses + up_pass->unifm_pixel_norm = + glGetUniformLocationChecked(up_pass->prog, "pixel_norm"); + up_pass->unifm_opacity = + glGetUniformLocationChecked(up_pass->prog, "opacity"); + up_pass->texorig_loc = + glGetUniformLocationChecked(up_pass->prog, "texorig"); + up_pass->scale_loc = glGetUniformLocationChecked(up_pass->prog, "scale"); + + // Setup projection matrix + glUseProgram(up_pass->prog); + int pml = glGetUniformLocationChecked(up_pass->prog, "projection"); + glUniformMatrix4fv(pml, 1, false, projection); + glUseProgram(0); + } + + success = true; +out: + free(blur_params); + + if (!success) { + ctx = NULL; + } + + // Restore LC_NUMERIC + setlocale(LC_NUMERIC, lc_numeric_old); + free(lc_numeric_old); + + return success; +} + +void *gl_create_blur_context(backend_t *base, enum blur_method method, void *args) { + bool success; + auto gd = (struct gl_data *)base; + + auto ctx = ccalloc(1, struct gl_blur_context); + + if (!method || method >= BLUR_METHOD_INVALID) { + ctx->method = BLUR_METHOD_NONE; + return ctx; + } + + // Set projection matrix to gl viewport dimensions so we can use screen + // coordinates for all vertices + // Note: OpenGL matrices are column major + GLint viewport_dimensions[2]; + glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); + GLfloat projection_matrix[4][4] = {{2.0f / (GLfloat)viewport_dimensions[0], 0, 0, 0}, + {0, 2.0f / (GLfloat)viewport_dimensions[1], 0, 0}, + {0, 0, 0, 0}, + {-1, -1, 0, 1}}; + + if (method == BLUR_METHOD_DUAL_KAWASE) { + success = gl_create_dual_kawase_blur_context(ctx, projection_matrix[0], + method, args); + } else { + success = + gl_create_kernel_blur_context(ctx, projection_matrix[0], method, args); + } + if (!success || ctx->method == BLUR_METHOD_NONE) { + goto out; + } + + // Texture size will be defined by gl_blur + ctx->blur_textures = ccalloc(ctx->blur_texture_count, GLuint); + ctx->texture_sizes = ccalloc(ctx->blur_texture_count, struct texture_size); + glGenTextures(ctx->blur_texture_count, ctx->blur_textures); + + for (int i = 0; i < ctx->blur_texture_count; ++i) { + glBindTexture(GL_TEXTURE_2D, ctx->blur_textures[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + // Generate FBO and textures when needed + ctx->blur_fbos = ccalloc(ctx->blur_fbo_count, GLuint); + glGenFramebuffers(ctx->blur_fbo_count, ctx->blur_fbos); + + for (int i = 0; i < ctx->blur_fbo_count; ++i) { + if (!ctx->blur_fbos[i]) { + log_error("Failed to generate framebuffer objects for blur"); + success = false; + goto out; + } + } + +out: + if (!success) { + gl_destroy_blur_context(&gd->base, ctx); + ctx = NULL; + } + + gl_check_err(); + return ctx; +} + +void gl_get_blur_size(void *blur_context, int *width, int *height) { + auto ctx = (struct gl_blur_context *)blur_context; + *width = ctx->resize_width; + *height = ctx->resize_height; +} + +// clang-format off +const char *win_shader_glsl = GLSL(330, + uniform float opacity; + uniform float dim; + uniform bool invert_color; + in vec2 texcoord; + uniform sampler2D tex; + uniform sampler2D brightness; + uniform float max_brightness; + + void main() { + vec4 c = texelFetch(tex, ivec2(texcoord), 0); + if (invert_color) { + c = vec4(c.aaa - c.rgb, c.a); + } + c = vec4(c.rgb * (1.0 - dim), c.a) * opacity; + + vec3 rgb_brightness = texelFetch(brightness, ivec2(0, 0), 0).rgb; + // Ref: https://en.wikipedia.org/wiki/Relative_luminance + float brightness = rgb_brightness.r * 0.21 + + rgb_brightness.g * 0.72 + + rgb_brightness.b * 0.07; + if (brightness > max_brightness) + c.rgb = c.rgb * (max_brightness / brightness); + + gl_FragColor = c; + } +); + +const char *present_vertex_shader = GLSL(330, + uniform mat4 projection; + layout(location = 0) in vec2 coord; + out vec2 texcoord; + void main() { + gl_Position = projection * vec4(coord, 0, 1); + texcoord = coord; + } +); +// clang-format on + +bool gl_init(struct gl_data *gd, session_t *ps) { + // Initialize GLX data structure + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + + glEnable(GL_BLEND); + // X pixmap is in premultiplied alpha, so we might just as well use it too. + // Thanks to derhass for help. + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + // Initialize stencil buffer + glDisable(GL_STENCIL_TEST); + glStencilMask(0x1); + glStencilFunc(GL_EQUAL, 0x1, 0x1); + + // Set gl viewport to the maximum supported size so we won't have to worry about + // it later on when the screen is resized. The corresponding projection matrix can + // be set now and won't have to be updated. Since fragments outside the target + // buffer are skipped anyways, this should have no impact on performance. + GLint viewport_dimensions[2]; + glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); + glViewport(0, 0, viewport_dimensions[0], viewport_dimensions[1]); + + // Clear screen + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + glGenFramebuffers(1, &gd->back_fbo); + glGenTextures(1, &gd->back_texture); + if (!gd->back_fbo || !gd->back_texture) { + log_error("Failed to generate a framebuffer object"); + return false; + } + + glBindTexture(GL_TEXTURE_2D, gd->back_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + + // Set projection matrix to gl viewport dimensions so we can use screen + // coordinates for all vertices + // Note: OpenGL matrices are column major + GLfloat projection_matrix[4][4] = {{2.0f / (GLfloat)viewport_dimensions[0], 0, 0, 0}, + {0, 2.0f / (GLfloat)viewport_dimensions[1], 0, 0}, + {0, 0, 0, 0}, + {-1, -1, 0, 1}}; + + // Initialize shaders + gl_win_shader_from_string(vertex_shader, win_shader_glsl, &gd->win_shader); + int pml = glGetUniformLocationChecked(gd->win_shader.prog, "projection"); + glUseProgram(gd->win_shader.prog); + glUniformMatrix4fv(pml, 1, false, projection_matrix[0]); + glUseProgram(0); + + gd->fill_shader.prog = gl_create_program_from_str(fill_vert, fill_frag); + gd->fill_shader.color_loc = glGetUniformLocation(gd->fill_shader.prog, "color"); + pml = glGetUniformLocationChecked(gd->fill_shader.prog, "projection"); + glUseProgram(gd->fill_shader.prog); + glUniformMatrix4fv(pml, 1, false, projection_matrix[0]); + glUseProgram(0); + + gd->present_prog = gl_create_program_from_str(present_vertex_shader, dummy_frag); + if (!gd->present_prog) { + log_error("Failed to create the present shader"); + return false; + } + pml = glGetUniformLocationChecked(gd->present_prog, "projection"); + glUseProgram(gd->present_prog); + glUniform1i(glGetUniformLocationChecked(gd->present_prog, "tex"), 0); + glUniformMatrix4fv(pml, 1, false, projection_matrix[0]); + glUseProgram(0); + + gd->brightness_shader.prog = + gl_create_program_from_str(interpolating_vert, interpolating_frag); + if (!gd->brightness_shader.prog) { + log_error("Failed to create the brightness shader"); + return false; + } + pml = glGetUniformLocationChecked(gd->brightness_shader.prog, "projection"); + glUseProgram(gd->brightness_shader.prog); + glUniform1i(glGetUniformLocationChecked(gd->brightness_shader.prog, "tex"), 0); + glUniformMatrix4fv(pml, 1, false, projection_matrix[0]); + glUseProgram(0); + + // Set up the size of the back texture + gl_resize(gd, ps->root_width, ps->root_height); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gd->back_fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + gd->back_texture, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + if (!gl_check_fb_complete(GL_FRAMEBUFFER)) { + return false; + } + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + + gd->logger = gl_string_marker_logger_new(); + if (gd->logger) { + log_add_target_tls(gd->logger); + } + + const char *vendor = (const char *)glGetString(GL_VENDOR); + log_debug("GL_VENDOR = %s", vendor); + if (strcmp(vendor, "NVIDIA Corporation") == 0) { + log_info("GL vendor is NVIDIA, don't use glFinish"); + gd->is_nvidia = true; + } else { + gd->is_nvidia = false; + } + + return true; +} + +void gl_deinit(struct gl_data *gd) { + gl_free_prog_main(&gd->win_shader); + + if (gd->logger) { + log_remove_target_tls(gd->logger); + gd->logger = NULL; + } + + gl_check_err(); +} + +GLuint gl_new_texture(GLenum target) { + GLuint texture; + glGenTextures(1, &texture); + if (!texture) { + log_error("Failed to generate texture"); + return 0; + } + + glBindTexture(target, texture); + glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_REPEAT); + glBindTexture(target, 0); + + return texture; +} + +/// Actually duplicate a texture into a new one, if this texture is shared +static inline void gl_image_decouple(backend_t *base, struct backend_image *img) { + if (img->inner->refcount == 1) { + return; + } + auto gd = (struct gl_data *)base; + auto inner = (struct gl_texture *)img->inner; + auto new_tex = ccalloc(1, struct gl_texture); + + new_tex->texture = gl_new_texture(GL_TEXTURE_2D); + new_tex->y_inverted = true; + new_tex->height = inner->height; + new_tex->width = inner->width; + new_tex->refcount = 1; + new_tex->user_data = gd->decouple_texture_user_data(base, inner->user_data); + + glBindTexture(GL_TEXTURE_2D, new_tex->texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, new_tex->width, new_tex->height, 0, + GL_BGRA, GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, 0); + + assert(gd->present_prog); + glUseProgram(gd->present_prog); + glBindTexture(GL_TEXTURE_2D, inner->texture); + + GLuint fbo; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + new_tex->texture, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + gl_check_fb_complete(GL_DRAW_FRAMEBUFFER); + + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + + // clang-format off + GLint coord[] = { + // top left + 0, 0, // vertex coord + 0, 0, // texture coord + + // top right + new_tex->width, 0, // vertex coord + new_tex->width, 0, // texture coord + + // bottom right + new_tex->width, new_tex->height, + new_tex->width, new_tex->height, + + // bottom left + 0, new_tex->height, + 0, new_tex->height, + }; + // clang-format on + GLuint indices[] = {0, 1, 2, 2, 3, 0}; + + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + GLuint bo[2]; + glGenBuffers(2, bo); + glBindBuffer(GL_ARRAY_BUFFER, bo[0]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * 16, coord, GL_STATIC_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * 6, indices, + GL_STATIC_DRAW); + + glEnableVertexAttribArray(vert_coord_loc); + glEnableVertexAttribArray(vert_in_texcoord_loc); + glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, + sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL); + + glDisableVertexAttribArray(vert_coord_loc); + glDisableVertexAttribArray(vert_in_texcoord_loc); + glBindVertexArray(0); + glDeleteVertexArrays(1, &vao); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDeleteBuffers(2, bo); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + + glBindTexture(GL_TEXTURE_2D, 0); + glUseProgram(0); + + gl_check_err(); + + img->inner = (struct backend_image_inner_base *)new_tex; + inner->refcount--; +} + +static void gl_image_apply_alpha(backend_t *base, struct backend_image *img, + const region_t *reg_op, double alpha) { + // Result color = 0 (GL_ZERO) + alpha (GL_CONSTANT_ALPHA) * original color + auto inner = (struct gl_texture *)img->inner; + glBlendFunc(GL_ZERO, GL_CONSTANT_ALPHA); + glBlendColor(0, 0, 0, (GLclampf)alpha); + GLuint fbo; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + inner->texture, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + _gl_fill(base, (struct color){0, 0, 0, 0}, reg_op, fbo, 0, false); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); +} + +void gl_present(backend_t *base, const region_t *region) { + auto gd = (struct gl_data *)base; + + int nrects; + const rect_t *rect = pixman_region32_rectangles((region_t *)region, &nrects); + auto coord = ccalloc(nrects * 8, GLint); + auto indices = ccalloc(nrects * 6, GLuint); + for (int i = 0; i < nrects; i++) { + // clang-format off + memcpy(&coord[i * 8], + ((GLint[]){rect[i].x1, gd->height - rect[i].y2, + rect[i].x2, gd->height - rect[i].y2, + rect[i].x2, gd->height - rect[i].y1, + rect[i].x1, gd->height - rect[i].y1}), + sizeof(GLint) * 8); + // clang-format on + + GLuint u = (GLuint)(i * 4); + memcpy(&indices[i * 6], + ((GLuint[]){u + 0, u + 1, u + 2, u + 2, u + 3, u + 0}), + sizeof(GLuint) * 6); + } + + glUseProgram(gd->present_prog); + glBindTexture(GL_TEXTURE_2D, gd->back_texture); + + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + GLuint bo[2]; + glGenBuffers(2, bo); + glEnableVertexAttribArray(vert_coord_loc); + glBindBuffer(GL_ARRAY_BUFFER, bo[0]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(GLint) * nrects * 8, coord, GL_STREAM_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(GLuint) * nrects * 6, indices, + GL_STREAM_DRAW); + + glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 2, NULL); + glDrawElements(GL_TRIANGLES, nrects * 6, GL_UNSIGNED_INT, NULL); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glBindVertexArray(0); + glDeleteBuffers(2, bo); + glDeleteVertexArrays(1, &vao); + + free(coord); + free(indices); +} + +bool gl_image_op(backend_t *base, enum image_operations op, void *image_data, + const region_t *reg_op, const region_t *reg_visible attr_unused, void *arg) { + struct backend_image *tex = image_data; + switch (op) { + case IMAGE_OP_APPLY_ALPHA: + gl_image_decouple(base, tex); + assert(tex->inner->refcount == 1); + gl_image_apply_alpha(base, tex, reg_op, *(double *)arg); + break; + } + + return true; +} + +bool gl_read_pixel(backend_t *base attr_unused, void *image_data, int x, int y, + struct color *output) { + struct backend_image *tex = image_data; + auto inner = (struct gl_texture *)tex->inner; + GLfloat color[4]; + glReadPixels(x, inner->y_inverted ? inner->height - y : y, 1, 1, GL_RGBA, + GL_FLOAT, color); + output->alpha = color[3]; + output->red = color[0]; + output->green = color[1]; + output->blue = color[2]; + + bool ret = glGetError() == GL_NO_ERROR; + gl_clear_err(); + return ret; +} diff --git a/src/backend/gl/gl_common.h b/src/backend/gl/gl_common.h new file mode 100644 index 0000000..b1d93b0 --- /dev/null +++ b/src/backend/gl/gl_common.h @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#pragma once +#include +#include +#include +#include + +#include "backend/backend.h" +#include "log.h" +#include "region.h" + +#define CASESTRRET(s) \ + case s: return #s + +// Program and uniforms for window shader +typedef struct { + GLuint prog; + GLint unifm_opacity; + GLint unifm_invert_color; + GLint unifm_tex; + GLint unifm_dim; + GLint unifm_brightness; + GLint unifm_max_brightness; +} gl_win_shader_t; + +// Program and uniforms for brightness shader +typedef struct { + GLuint prog; +} gl_brightness_shader_t; + +// Program and uniforms for blur shader +typedef struct { + GLuint prog; + GLint unifm_pixel_norm; + GLint unifm_opacity; + GLint texorig_loc; + GLint scale_loc; +} gl_blur_shader_t; + +typedef struct { + GLuint prog; + GLint color_loc; +} gl_fill_shader_t; + +/// @brief Wrapper of a binded GLX texture. +struct gl_texture { + int refcount; + bool has_alpha; + GLuint texture; + int width, height; + bool y_inverted; + + // Textures for auxiliary uses. + GLuint auxiliary_texture[2]; + void *user_data; +}; + +struct gl_data { + backend_t base; + // If we are using proprietary NVIDIA driver + bool is_nvidia; + // Height and width of the root window + int height, width; + gl_win_shader_t win_shader; + gl_brightness_shader_t brightness_shader; + gl_fill_shader_t fill_shader; + GLuint back_texture, back_fbo; + GLuint present_prog; + + /// Called when an gl_texture is decoupled from the texture it refers. Returns + /// the decoupled user_data + void *(*decouple_texture_user_data)(backend_t *base, void *user_data); + + /// Release the user data attached to a gl_texture + void (*release_user_data)(backend_t *base, struct gl_texture *); + + struct log_target *logger; +}; + +typedef struct session session_t; + +#define GL_PROG_MAIN_INIT \ + { .prog = 0, .unifm_opacity = -1, .unifm_invert_color = -1, .unifm_tex = -1, } + +GLuint gl_create_shader(GLenum shader_type, const char *shader_str); +GLuint gl_create_program(const GLuint *const shaders, int nshaders); +GLuint gl_create_program_from_str(const char *vert_shader_str, const char *frag_shader_str); + +/** + * @brief Render a region with texture data. + */ +void gl_compose(backend_t *, void *ptex, + int dst_x1, int dst_y1, int dst_x2, int dst_y2, + const region_t *reg_tgt, const region_t *reg_visible); + +void gl_resize(struct gl_data *, int width, int height); + +bool gl_init(struct gl_data *gd, session_t *); +void gl_deinit(struct gl_data *gd); + +GLuint gl_new_texture(GLenum target); + +bool gl_image_op(backend_t *base, enum image_operations op, void *image_data, + const region_t *reg_op, const region_t *reg_visible, void *arg); + +void gl_release_image(backend_t *base, void *image_data); + +void *gl_clone(backend_t *base, const void *image_data, const region_t *reg_visible); + +bool gl_blur(backend_t *base, double opacity, void *, const region_t *reg_blur, + const region_t *reg_visible); +void *gl_create_blur_context(backend_t *base, enum blur_method, void *args); +void gl_destroy_blur_context(backend_t *base, void *ctx); +void gl_get_blur_size(void *blur_context, int *width, int *height); + +void gl_fill(backend_t *base, struct color, const region_t *clip); + +void gl_present(backend_t *base, const region_t *); +bool gl_read_pixel(backend_t *base, void *image_data, int x, int y, struct color *output); + +static inline void gl_delete_texture(GLuint texture) { + glDeleteTextures(1, &texture); +} + +/** + * Get a textual representation of an OpenGL error. + */ +static inline const char *gl_get_err_str(GLenum err) { + switch (err) { + CASESTRRET(GL_NO_ERROR); + CASESTRRET(GL_INVALID_ENUM); + CASESTRRET(GL_INVALID_VALUE); + CASESTRRET(GL_INVALID_OPERATION); + CASESTRRET(GL_INVALID_FRAMEBUFFER_OPERATION); + CASESTRRET(GL_OUT_OF_MEMORY); + CASESTRRET(GL_STACK_UNDERFLOW); + CASESTRRET(GL_STACK_OVERFLOW); + CASESTRRET(GL_FRAMEBUFFER_UNDEFINED); + CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT); + CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT); + CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER); + CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER); + CASESTRRET(GL_FRAMEBUFFER_UNSUPPORTED); + CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE); + CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS); + } + return NULL; +} + +/** + * Check for GLX error. + * + * http://blog.nobel-joergensen.com/2013/01/29/debugging-opengl-using-glgeterror/ + */ +static inline void gl_check_err_(const char *func, int line) { + GLenum err = GL_NO_ERROR; + + while (GL_NO_ERROR != (err = glGetError())) { + const char *errtext = gl_get_err_str(err); + if (errtext) { + log_printf(tls_logger, LOG_LEVEL_ERROR, func, + "GLX error at line %d: %s", line, errtext); + } else { + log_printf(tls_logger, LOG_LEVEL_ERROR, func, + "GLX error at line %d: %d", line, err); + } + } +} + +static inline void gl_clear_err(void) { + while (glGetError() != GL_NO_ERROR) + ; +} + +#define gl_check_err() gl_check_err_(__func__, __LINE__) + +/** + * Check for GL framebuffer completeness. + */ +static inline bool gl_check_fb_complete_(const char *func, int line, GLenum fb) { + GLenum status = glCheckFramebufferStatus(fb); + + if (status == GL_FRAMEBUFFER_COMPLETE) { + return true; + } + + const char *stattext = gl_get_err_str(status); + if (stattext) { + log_printf(tls_logger, LOG_LEVEL_ERROR, func, + "Framebuffer attachment failed at line %d: %s", line, stattext); + } else { + log_printf(tls_logger, LOG_LEVEL_ERROR, func, + "Framebuffer attachment failed at line %d: %d", line, status); + } + + return false; +} + +#define gl_check_fb_complete(fb) gl_check_fb_complete_(__func__, __LINE__, (fb)) + +/** + * Check if a GLX extension exists. + */ +static inline bool gl_has_extension(const char *ext) { + int nexts = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &nexts); + for (int i = 0; i < nexts || !nexts; i++) { + const char *exti = (const char *)glGetStringi(GL_EXTENSIONS, (GLuint)i); + if (exti == NULL) { + break; + } + if (strcmp(ext, exti) == 0) { + return true; + } + } + gl_clear_err(); + log_info("Missing GL extension %s.", ext); + return false; +} diff --git a/src/backend/gl/glx.c b/src/backend/gl/glx.c new file mode 100644 index 0000000..1397d19 --- /dev/null +++ b/src/backend/gl/glx.c @@ -0,0 +1,648 @@ +// SPDX-License-Identifier: MIT +/* + * Compton - a compositor for X11 + * + * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard + * + * Copyright (c) 2011-2013, Christopher Jeffrey + * Copyright (c) 2019 Yuxuan Shui + * See LICENSE-mit for more information. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "backend/backend.h" +#include "backend/backend_common.h" +#include "backend/gl/gl_common.h" +#include "backend/gl/glx.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "log.h" +#include "picom.h" +#include "region.h" +#include "utils.h" +#include "win.h" +#include "x.h" + +struct _glx_pixmap { + GLXPixmap glpixmap; + xcb_pixmap_t pixmap; + bool owned; +}; + +struct _glx_data { + struct gl_data gl; + Display *display; + int screen; + xcb_window_t target_win; + GLXContext ctx; +}; + +#define glXGetFBConfigAttribChecked(a, b, attr, c) \ + do { \ + if (glXGetFBConfigAttrib(a, b, attr, c)) { \ + log_info("Cannot get FBConfig attribute " #attr); \ + continue; \ + } \ + } while (0) + +struct glx_fbconfig_info *glx_find_fbconfig(Display *dpy, int screen, struct xvisual_info m) { + log_debug("Looking for FBConfig for RGBA%d%d%d%d, depth %d", m.red_size, + m.blue_size, m.green_size, m.alpha_size, m.visual_depth); + + int ncfg; + // clang-format off + GLXFBConfig *cfg = + glXChooseFBConfig(dpy, screen, (int[]){ + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT, + GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, + GLX_X_RENDERABLE, true, + GLX_FRAMEBUFFER_SRGB_CAPABLE_EXT, (GLint)GLX_DONT_CARE, + GLX_BUFFER_SIZE, m.red_size + m.green_size + + m.blue_size + m.alpha_size, + GLX_RED_SIZE, m.red_size, + GLX_BLUE_SIZE, m.blue_size, + GLX_GREEN_SIZE, m.green_size, + GLX_ALPHA_SIZE, m.alpha_size, + GLX_STENCIL_SIZE, 0, + GLX_DEPTH_SIZE, 0, + 0 + }, &ncfg); + // clang-format on + + int texture_tgts, y_inverted, texture_fmt; + bool found = false; + int min_cost = INT_MAX; + GLXFBConfig ret; + for (int i = 0; i < ncfg; i++) { + int depthbuf, stencil, doublebuf, bufsize; + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BUFFER_SIZE, &bufsize); + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_DEPTH_SIZE, &depthbuf); + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_STENCIL_SIZE, &stencil); + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_DOUBLEBUFFER, &doublebuf); + if (depthbuf + stencil + bufsize * (doublebuf + 1) >= min_cost) { + continue; + } + int red, green, blue; + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_RED_SIZE, &red); + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BLUE_SIZE, &blue); + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_GREEN_SIZE, &green); + if (red != m.red_size || green != m.green_size || blue != m.blue_size) { + // Color size doesn't match, this cannot work + continue; + } + + int rgb, rgba; + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_RGB_EXT, &rgb); + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_RGBA_EXT, &rgba); + if (!rgb && !rgba) { + log_info("FBConfig is neither RGBA nor RGB, we cannot " + "handle this setup."); + continue; + } + + int visual; + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_VISUAL_ID, &visual); + if (m.visual_depth != -1 && + x_get_visual_depth(XGetXCBConnection(dpy), (xcb_visualid_t)visual) != + m.visual_depth) { + // FBConfig and the correspondent X Visual might not have the same + // depth. (e.g. 32 bit FBConfig with a 24 bit Visual). This is + // quite common, seen in both open source and proprietary drivers. + // + // If the FBConfig has a matching depth but its visual doesn't, we + // still cannot use it. + continue; + } + + // All check passed, we are using this one. + found = true; + ret = cfg[i]; + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_TARGETS_EXT, + &texture_tgts); + glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_Y_INVERTED_EXT, &y_inverted); + + // Prefer the texture format with matching alpha, with the other one as + // fallback + if (m.alpha_size) { + texture_fmt = rgba ? GLX_TEXTURE_FORMAT_RGBA_EXT + : GLX_TEXTURE_FORMAT_RGB_EXT; + } else { + texture_fmt = + rgb ? GLX_TEXTURE_FORMAT_RGB_EXT : GLX_TEXTURE_FORMAT_RGBA_EXT; + } + min_cost = depthbuf + stencil + bufsize * (doublebuf + 1); + } + free(cfg); + if (!found) { + return NULL; + } + + auto info = cmalloc(struct glx_fbconfig_info); + info->cfg = ret; + info->texture_tgts = texture_tgts; + info->texture_fmt = texture_fmt; + info->y_inverted = y_inverted; + return info; +} + +/** + * Free a glx_texture_t. + */ +static void glx_release_image(backend_t *base, struct gl_texture *tex) { + struct _glx_data *gd = (void *)base; + + struct _glx_pixmap *p = tex->user_data; + // Release binding + if (p->glpixmap && tex->texture) { + glBindTexture(GL_TEXTURE_2D, tex->texture); + glXReleaseTexImageEXT(gd->display, p->glpixmap, GLX_FRONT_LEFT_EXT); + glBindTexture(GL_TEXTURE_2D, 0); + } + + // Free GLX Pixmap + if (p->glpixmap) { + glXDestroyPixmap(gd->display, p->glpixmap); + p->glpixmap = 0; + } + + if (p->owned) { + xcb_free_pixmap(base->c, p->pixmap); + p->pixmap = XCB_NONE; + } + + free(p); + tex->user_data = NULL; +} + +/** + * Destroy GLX related resources. + */ +void glx_deinit(backend_t *base) { + struct _glx_data *gd = (void *)base; + + gl_deinit(&gd->gl); + + // Destroy GLX context + if (gd->ctx) { + glXMakeCurrent(gd->display, None, NULL); + glXDestroyContext(gd->display, gd->ctx); + gd->ctx = 0; + } + + free(gd); +} + +static void *glx_decouple_user_data(backend_t *base attr_unused, void *ud attr_unused) { + auto ret = cmalloc(struct _glx_pixmap); + ret->owned = false; + ret->glpixmap = 0; + ret->pixmap = 0; + return ret; +} + +static bool glx_set_swap_interval(int interval, Display *dpy, GLXDrawable drawable) { + bool vsync_enabled = false; + if (glxext.has_GLX_MESA_swap_control) { + vsync_enabled = (glXSwapIntervalMESA((uint)interval) == 0); + } + if (!vsync_enabled && glxext.has_GLX_SGI_swap_control) { + vsync_enabled = (glXSwapIntervalSGI(interval) == 0); + } + if (!vsync_enabled && glxext.has_GLX_EXT_swap_control) { + // glXSwapIntervalEXT doesn't return if it's successful + glXSwapIntervalEXT(dpy, drawable, interval); + vsync_enabled = true; + } + return vsync_enabled; +} + +/** + * Initialize OpenGL. + */ +static backend_t *glx_init(session_t *ps) { + bool success = false; + glxext_init(ps->dpy, ps->scr); + auto gd = ccalloc(1, struct _glx_data); + init_backend_base(&gd->gl.base, ps); + + gd->display = ps->dpy; + gd->screen = ps->scr; + gd->target_win = session_get_target_window(ps); + + XVisualInfo *pvis = NULL; + + // Check for GLX extension + if (!ps->glx_exists) { + log_error("No GLX extension."); + goto end; + } + + // Get XVisualInfo + int nitems = 0; + XVisualInfo vreq = {.visualid = ps->vis}; + pvis = XGetVisualInfo(ps->dpy, VisualIDMask, &vreq, &nitems); + if (!pvis) { + log_error("Failed to acquire XVisualInfo for current visual."); + goto end; + } + + // Ensure the visual is double-buffered + int value = 0; + if (glXGetConfig(ps->dpy, pvis, GLX_USE_GL, &value) || !value) { + log_error("Root visual is not a GL visual."); + goto end; + } + + if (glXGetConfig(ps->dpy, pvis, GLX_STENCIL_SIZE, &value) || !value) { + log_error("Root visual lacks stencil buffer."); + goto end; + } + + if (glXGetConfig(ps->dpy, pvis, GLX_DOUBLEBUFFER, &value) || !value) { + log_error("Root visual is not a double buffered GL visual."); + goto end; + } + + if (glXGetConfig(ps->dpy, pvis, GLX_RGBA, &value) || !value) { + log_error("Root visual is a color index visual, not supported"); + goto end; + } + + if (!glxext.has_GLX_EXT_texture_from_pixmap) { + log_error("GLX_EXT_texture_from_pixmap is not supported by your driver"); + goto end; + } + + if (!glxext.has_GLX_ARB_create_context) { + log_error("GLX_ARB_create_context is not supported by your driver"); + goto end; + } + + // Find a fbconfig with visualid matching the one from the target win, so we can + // be sure that the fbconfig is compatible with our target window. + int ncfgs; + GLXFBConfig *cfg = glXGetFBConfigs(gd->display, gd->screen, &ncfgs); + bool found = false; + for (int i = 0; i < ncfgs; i++) { + int visualid; + glXGetFBConfigAttribChecked(gd->display, cfg[i], GLX_VISUAL_ID, &visualid); + if ((VisualID)visualid != pvis->visualid) { + continue; + } + + gd->ctx = glXCreateContextAttribsARB(ps->dpy, cfg[i], 0, true, + (int[]){ + GLX_CONTEXT_MAJOR_VERSION_ARB, + 3, + GLX_CONTEXT_MINOR_VERSION_ARB, + 3, + GLX_CONTEXT_PROFILE_MASK_ARB, + GLX_CONTEXT_CORE_PROFILE_BIT_ARB, + 0, + }); + free(cfg); + + if (!gd->ctx) { + log_error("Failed to get GLX context."); + goto end; + } + found = true; + break; + } + + if (!found) { + log_error("Couldn't find a suitable fbconfig for the target window"); + goto end; + } + + // Attach GLX context + GLXDrawable tgt = gd->target_win; + if (!glXMakeCurrent(ps->dpy, tgt, gd->ctx)) { + log_error("Failed to attach GLX context."); + goto end; + } + + if (!gl_init(&gd->gl, ps)) { + log_error("Failed to setup OpenGL"); + goto end; + } + + gd->gl.decouple_texture_user_data = glx_decouple_user_data; + gd->gl.release_user_data = glx_release_image; + + if (ps->o.vsync) { + if (!glx_set_swap_interval(1, ps->dpy, tgt)) { + log_error("Failed to enable vsync."); + } + } else { + glx_set_swap_interval(0, ps->dpy, tgt); + } + + success = true; + +end: + if (pvis) { + XFree(pvis); + } + + if (!success) { + glx_deinit(&gd->gl.base); + return NULL; + } + + return &gd->gl.base; +} + +static void * +glx_bind_pixmap(backend_t *base, xcb_pixmap_t pixmap, struct xvisual_info fmt, bool owned) { + struct _glx_data *gd = (void *)base; + struct _glx_pixmap *glxpixmap = NULL; + // Retrieve pixmap parameters, if they aren't provided + if (fmt.visual_depth > OPENGL_MAX_DEPTH) { + log_error("Requested depth %d higher than max possible depth %d.", + fmt.visual_depth, OPENGL_MAX_DEPTH); + return false; + } + + if (fmt.visual_depth < 0) { + log_error("Pixmap %#010x with invalid depth %d", pixmap, fmt.visual_depth); + return false; + } + + auto r = xcb_get_geometry_reply(base->c, xcb_get_geometry(base->c, pixmap), NULL); + if (!r) { + log_error("Invalid pixmap %#010x", pixmap); + return NULL; + } + + log_trace("Binding pixmap %#010x", pixmap); + auto wd = ccalloc(1, struct backend_image); + wd->max_brightness = 1; + auto inner = ccalloc(1, struct gl_texture); + inner->width = wd->ewidth = r->width; + inner->height = wd->eheight = r->height; + wd->inner = (struct backend_image_inner_base *)inner; + free(r); + + auto fbcfg = glx_find_fbconfig(gd->display, gd->screen, fmt); + if (!fbcfg) { + log_error("Couldn't find FBConfig with requested visual %x", fmt.visual); + goto err; + } + + // Choose a suitable texture target for our pixmap. + // Refer to GLX_EXT_texture_om_pixmap spec to see what are the mean + // of the bits in texture_tgts + if (!(fbcfg->texture_tgts & GLX_TEXTURE_2D_BIT_EXT)) { + log_error("Cannot bind pixmap to GL_TEXTURE_2D, giving up"); + goto err; + } + + log_debug("depth %d, rgba %d", fmt.visual_depth, + (fbcfg->texture_fmt == GLX_TEXTURE_FORMAT_RGBA_EXT)); + + GLint attrs[] = { + GLX_TEXTURE_FORMAT_EXT, + fbcfg->texture_fmt, + GLX_TEXTURE_TARGET_EXT, + GLX_TEXTURE_2D_EXT, + 0, + }; + + inner->y_inverted = fbcfg->y_inverted; + + glxpixmap = cmalloc(struct _glx_pixmap); + glxpixmap->pixmap = pixmap; + glxpixmap->glpixmap = glXCreatePixmap(gd->display, fbcfg->cfg, pixmap, attrs); + glxpixmap->owned = owned; + free(fbcfg); + + if (!glxpixmap->glpixmap) { + log_error("Failed to create glpixmap for pixmap %#010x", pixmap); + goto err; + } + + log_trace("GLXPixmap %#010lx", glxpixmap->glpixmap); + + // Create texture + inner->user_data = glxpixmap; + inner->texture = gl_new_texture(GL_TEXTURE_2D); + inner->has_alpha = fmt.alpha_size != 0; + wd->opacity = 1; + wd->color_inverted = false; + wd->dim = 0; + wd->inner->refcount = 1; + glBindTexture(GL_TEXTURE_2D, inner->texture); + glXBindTexImageEXT(gd->display, glxpixmap->glpixmap, GLX_FRONT_LEFT_EXT, NULL); + glBindTexture(GL_TEXTURE_2D, 0); + + gl_check_err(); + return wd; +err: + if (glxpixmap && glxpixmap->glpixmap) { + glXDestroyPixmap(gd->display, glxpixmap->glpixmap); + } + free(glxpixmap); + + if (owned) { + xcb_free_pixmap(base->c, pixmap); + } + free(wd); + return NULL; +} + +static void glx_present(backend_t *base, const region_t *region attr_unused) { + struct _glx_data *gd = (void *)base; + gl_present(base, region); + glXSwapBuffers(gd->display, gd->target_win); + if (!gd->gl.is_nvidia) { + glFinish(); + } +} + +static int glx_buffer_age(backend_t *base) { + if (!glxext.has_GLX_EXT_buffer_age) { + return -1; + } + + struct _glx_data *gd = (void *)base; + unsigned int val; + glXQueryDrawable(gd->display, gd->target_win, GLX_BACK_BUFFER_AGE_EXT, &val); + return (int)val ?: -1; +} + +static void glx_diagnostics(backend_t *base) { + struct _glx_data *gd = (void *)base; + bool warn_software_rendering = false; + const char *software_renderer_names[] = {"llvmpipe", "SWR", "softpipe"}; + auto glx_vendor = glXGetClientString(gd->display, GLX_VENDOR); + printf("* Driver vendors:\n"); + printf(" * GLX: %s\n", glx_vendor); + printf(" * GL: %s\n", glGetString(GL_VENDOR)); + + auto gl_renderer = (const char *)glGetString(GL_RENDERER); + printf("* GL renderer: %s\n", gl_renderer); + if (strcmp(glx_vendor, "Mesa Project and SGI")) { + for (size_t i = 0; i < ARR_SIZE(software_renderer_names); i++) { + if (strstr(gl_renderer, software_renderer_names[i]) != NULL) { + warn_software_rendering = true; + break; + } + } + } + +#ifdef GLX_MESA_query_renderer + if (glxext.has_GLX_MESA_query_renderer) { + unsigned int accelerated = 0; + glXQueryCurrentRendererIntegerMESA(GLX_RENDERER_ACCELERATED_MESA, &accelerated); + printf("* Accelerated: %d\n", accelerated); + + // Trust GLX_MESA_query_renderer when it's available + warn_software_rendering = (accelerated == 0); + } +#endif + + if (warn_software_rendering) { + printf("\n(You are using a software renderer. Unless you are doing this\n" + "intentionally, this means you don't have a graphics driver\n" + "properly installed. Performance will suffer. Please fix this\n" + "before reporting your issue.)\n"); + } +} + +struct backend_operations glx_ops = { + .init = glx_init, + .deinit = glx_deinit, + .bind_pixmap = glx_bind_pixmap, + .release_image = gl_release_image, + .compose = gl_compose, + .image_op = gl_image_op, + .set_image_property = default_set_image_property, + .read_pixel = gl_read_pixel, + .clone_image = default_clone_image, + .blur = gl_blur, + .is_image_transparent = default_is_image_transparent, + .present = glx_present, + .buffer_age = glx_buffer_age, + .render_shadow = default_backend_render_shadow, + .fill = gl_fill, + .create_blur_context = gl_create_blur_context, + .destroy_blur_context = gl_destroy_blur_context, + .get_blur_size = gl_get_blur_size, + .diagnostics = glx_diagnostics, + .max_buffer_age = 5, // Why? +}; + +/** + * Check if a GLX extension exists. + */ +static inline bool glx_has_extension(Display *dpy, int screen, const char *ext) { + const char *glx_exts = glXQueryExtensionsString(dpy, screen); + if (!glx_exts) { + log_error("Failed get GLX extension list."); + return false; + } + + auto inlen = strlen(ext); + const char *curr = glx_exts; + bool match = false; + while (curr && !match) { + const char *end = strchr(curr, ' '); + if (!end) { + // Last extension string + match = strcmp(ext, curr) == 0; + } else if (curr + inlen == end) { + // Length match, do match string + match = strncmp(ext, curr, (unsigned long)(end - curr)) == 0; + } + curr = end ? end + 1 : NULL; + } + + if (!match) { + log_info("Missing GLX extension %s.", ext); + } else { + log_info("Found GLX extension %s.", ext); + } + + return match; +} + +struct glxext_info glxext = {0}; +PFNGLXGETVIDEOSYNCSGIPROC glXGetVideoSyncSGI; +PFNGLXWAITVIDEOSYNCSGIPROC glXWaitVideoSyncSGI; +PFNGLXGETSYNCVALUESOMLPROC glXGetSyncValuesOML; +PFNGLXWAITFORMSCOMLPROC glXWaitForMscOML; +PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT; +PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI; +PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA; +PFNGLXBINDTEXIMAGEEXTPROC glXBindTexImageEXT; +PFNGLXRELEASETEXIMAGEEXTPROC glXReleaseTexImageEXT; +PFNGLXCREATECONTEXTATTRIBSARBPROC glXCreateContextAttribsARB; + +#ifdef GLX_MESA_query_renderer +PFNGLXQUERYCURRENTRENDERERINTEGERMESAPROC glXQueryCurrentRendererIntegerMESA; +#endif + +void glxext_init(Display *dpy, int screen) { + if (glxext.initialized) { + return; + } + glxext.initialized = true; +#define check_ext(name) glxext.has_##name = glx_has_extension(dpy, screen, #name) + check_ext(GLX_SGI_video_sync); + check_ext(GLX_SGI_swap_control); + check_ext(GLX_OML_sync_control); + check_ext(GLX_MESA_swap_control); + check_ext(GLX_EXT_swap_control); + check_ext(GLX_EXT_texture_from_pixmap); + check_ext(GLX_ARB_create_context); + check_ext(GLX_EXT_buffer_age); +#ifdef GLX_MESA_query_renderer + check_ext(GLX_MESA_query_renderer); +#endif +#undef check_ext + +#define lookup(name) (name = (__typeof__(name))glXGetProcAddress((GLubyte *)#name)) + // Checking if the returned function pointer is NULL is not really necessary, + // or maybe not even useful, since glXGetProcAddress might always return + // something. We are doing it just for completeness' sake. + if (!lookup(glXGetVideoSyncSGI) || !lookup(glXWaitVideoSyncSGI)) { + glxext.has_GLX_SGI_video_sync = false; + } + if (!lookup(glXSwapIntervalEXT)) { + glxext.has_GLX_EXT_swap_control = false; + } + if (!lookup(glXSwapIntervalMESA)) { + glxext.has_GLX_MESA_swap_control = false; + } + if (!lookup(glXSwapIntervalSGI)) { + glxext.has_GLX_SGI_swap_control = false; + } + if (!lookup(glXWaitForMscOML) || !lookup(glXGetSyncValuesOML)) { + glxext.has_GLX_OML_sync_control = false; + } + if (!lookup(glXBindTexImageEXT) || !lookup(glXReleaseTexImageEXT)) { + glxext.has_GLX_EXT_texture_from_pixmap = false; + } + if (!lookup(glXCreateContextAttribsARB)) { + glxext.has_GLX_ARB_create_context = false; + } +#ifdef GLX_MESA_query_renderer + if (!lookup(glXQueryCurrentRendererIntegerMESA)) { + glxext.has_GLX_MESA_query_renderer = false; + } +#endif +#undef lookup +} diff --git a/src/backend/gl/glx.h b/src/backend/gl/glx.h new file mode 100644 index 0000000..1061f0b --- /dev/null +++ b/src/backend/gl/glx.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#pragma once +#include +// Older version of glx.h defines function prototypes for these extensions... +// Rename them to avoid conflicts +#define glXSwapIntervalMESA glXSwapIntervalMESA_ +#define glXBindTexImageEXT glXBindTexImageEXT_ +#define glXReleaseTexImageEXT glXReleaseTexImageEXT +#include +#undef glXSwapIntervalMESA +#undef glXBindTexImageEXT +#undef glXReleaseTexImageEXT +#include +#include +#include + +#include "log.h" +#include "compiler.h" +#include "utils.h" +#include "x.h" + +struct glx_fbconfig_info { + GLXFBConfig cfg; + int texture_tgts; + int texture_fmt; + int y_inverted; +}; + +/// The search criteria for glx_find_fbconfig +struct glx_fbconfig_criteria { + /// Bit width of the red component + int red_size; + /// Bit width of the green component + int green_size; + /// Bit width of the blue component + int blue_size; + /// Bit width of the alpha component + int alpha_size; + /// The depth of X visual + int visual_depth; +}; + +struct glx_fbconfig_info *glx_find_fbconfig(Display *, int screen, struct xvisual_info); + + +struct glxext_info { + bool initialized; + bool has_GLX_SGI_video_sync; + bool has_GLX_SGI_swap_control; + bool has_GLX_OML_sync_control; + bool has_GLX_MESA_swap_control; + bool has_GLX_EXT_swap_control; + bool has_GLX_EXT_texture_from_pixmap; + bool has_GLX_ARB_create_context; + bool has_GLX_EXT_buffer_age; + bool has_GLX_MESA_query_renderer; +}; + +extern struct glxext_info glxext; + +extern PFNGLXGETVIDEOSYNCSGIPROC glXGetVideoSyncSGI; +extern PFNGLXWAITVIDEOSYNCSGIPROC glXWaitVideoSyncSGI; +extern PFNGLXGETSYNCVALUESOMLPROC glXGetSyncValuesOML; +extern PFNGLXWAITFORMSCOMLPROC glXWaitForMscOML; +extern PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT; +extern PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI; +extern PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA; +extern PFNGLXBINDTEXIMAGEEXTPROC glXBindTexImageEXT; +extern PFNGLXRELEASETEXIMAGEEXTPROC glXReleaseTexImageEXT; +extern PFNGLXCREATECONTEXTATTRIBSARBPROC glXCreateContextAttribsARB; + +#ifdef GLX_MESA_query_renderer +extern PFNGLXQUERYCURRENTRENDERERINTEGERMESAPROC glXQueryCurrentRendererIntegerMESA; +#endif + +void glxext_init(Display *, int screen); diff --git a/src/backend/meson.build b/src/backend/meson.build new file mode 100644 index 0000000..b8f0ad9 --- /dev/null +++ b/src/backend/meson.build @@ -0,0 +1,7 @@ +# enable xrender +srcs += [ files('backend_common.c', 'xrender/xrender.c', 'dummy/dummy.c', 'backend.c', 'driver.c') ] + +# enable opengl +if get_option('opengl') + srcs += [ files('gl/gl_common.c', 'gl/glx.c') ] +endif diff --git a/src/backend/xrender/xrender.c b/src/backend/xrender/xrender.c new file mode 100644 index 0000000..ccf358b --- /dev/null +++ b/src/backend/xrender/xrender.c @@ -0,0 +1,779 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "backend/backend.h" +#include "backend/backend_common.h" +#include "common.h" +#include "config.h" +#include "kernel.h" +#include "log.h" +#include "picom.h" +#include "region.h" +#include "types.h" +#include "utils.h" +#include "win.h" +#include "x.h" + +typedef struct _xrender_data { + backend_t base; + /// If vsync is enabled and supported by the current system + bool vsync; + xcb_visualid_t default_visual; + /// Target window + xcb_window_t target_win; + /// Painting target, it is either the root or the overlay + xcb_render_picture_t target; + /// Back buffers. Double buffer, with 1 for temporary render use + xcb_render_picture_t back[3]; + /// The back buffer that is for temporary use + /// Age of each back buffer. + int buffer_age[3]; + /// The back buffer we should be painting into + int curr_back; + /// The corresponding pixmap to the back buffer + xcb_pixmap_t back_pixmap[3]; + /// Pictures of pixel of different alpha value, used as a mask to + /// paint transparent images + xcb_render_picture_t alpha_pict[256]; + + // XXX don't know if these are really needed + + /// 1x1 white picture + xcb_render_picture_t white_pixel; + /// 1x1 black picture + xcb_render_picture_t black_pixel; + + /// Width and height of the target pixmap + int target_width, target_height; + + xcb_special_event_t *present_event; +} xrender_data; + +struct _xrender_blur_context { + enum blur_method method; + /// Blur kernels converted to X format + struct x_convolution_kernel **x_blur_kernel; + + int resize_width, resize_height; + + /// Number of blur kernels + int x_blur_kernel_count; +}; + +struct _xrender_image_data_inner { + // struct backend_image_inner_base + int refcount; + bool has_alpha; + + // Pixmap that the client window draws to, + // it will contain the content of client window. + xcb_pixmap_t pixmap; + // A Picture links to the Pixmap + xcb_render_picture_t pict; + int width, height; + xcb_visualid_t visual; + uint8_t depth; + // Whether we own this image, e.g. we allocated it; + // or not, e.g. this is a named pixmap of a X window. + bool owned; +}; + +static void compose_impl(struct _xrender_data *xd, const struct backend_image *img, + int dst_x1, int dst_y1, int dst_x2, int dst_y2, + const region_t *reg_paint, const region_t *reg_visible, + xcb_render_picture_t result) { + auto alpha_pict = xd->alpha_pict[(int)(img->opacity * MAX_ALPHA)]; + auto inner = (struct _xrender_image_data_inner *)img->inner; + region_t reg; + + bool has_alpha = inner->has_alpha || img->opacity != 1; + const auto tmpw = to_u16_checked(dst_x2 - dst_x1); + const auto tmph = to_u16_checked(dst_y2 - dst_y1); + const auto tmpew = to_u16_checked(dst_x2 - dst_x1); + const auto tmpeh = to_u16_checked(dst_y2 - dst_y1); + const xcb_render_color_t dim_color = { + .red = 0, .green = 0, .blue = 0, .alpha = (uint16_t)(0xffff * img->dim)}; + + // Clip region of rendered_pict might be set during rendering, clear it to + // make sure we get everything into the buffer + x_clear_picture_clip_region(xd->base.c, inner->pict); + + pixman_region32_init(®); + pixman_region32_intersect(®, (region_t *)reg_paint, (region_t *)reg_visible); + x_set_picture_clip_region(xd->base.c, result, 0, 0, ®); + +#define DOUBLE_TO_XFIXED(value) ((xcb_render_fixed_t)(((double)(value)) * 65536)) + { + const xcb_render_transform_t transform = { + DOUBLE_TO_XFIXED((double)img->ewidth / (double)tmpew), DOUBLE_TO_XFIXED(0.0), DOUBLE_TO_XFIXED(0.0), + DOUBLE_TO_XFIXED(0.0), DOUBLE_TO_XFIXED((double)img->eheight / (double)tmpeh), DOUBLE_TO_XFIXED(0.0), + DOUBLE_TO_XFIXED(0.0), DOUBLE_TO_XFIXED(0.0), DOUBLE_TO_XFIXED(1.0), + }; + xcb_render_set_picture_transform(xd->base.c, inner->pict, transform); + xcb_render_set_picture_filter(xd->base.c, inner->pict, 7, "nearest", 0, NULL); + } +#undef DOUBLE_TO_XFIXED + + if ((img->color_inverted || img->dim != 0) && has_alpha) { + // Apply image properties using a temporary image, because the source + // image is transparent. Otherwise the properties can be applied directly + // on the target image. + auto tmp_pict = + x_create_picture_with_visual(xd->base.c, xd->base.root, inner->width, + inner->height, inner->visual, 0, NULL); + + // Set clip region translated to source coordinate + x_set_picture_clip_region(xd->base.c, tmp_pict, to_i16_checked(-dst_x1), + to_i16_checked(-dst_y1), ®); + // Copy source -> tmp + xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_OVER, inner->pict, + XCB_NONE, tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); + if (img->color_inverted) { + if (inner->has_alpha) { + auto tmp_pict2 = x_create_picture_with_visual( + xd->base.c, xd->base.root, tmpw, tmph, inner->visual, + 0, NULL); + xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_OVER, + tmp_pict, XCB_NONE, tmp_pict2, 0, 0, + 0, 0, 0, 0, tmpw, tmph); + + xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_DIFFERENCE, + xd->white_pixel, XCB_NONE, tmp_pict, + 0, 0, 0, 0, 0, 0, tmpw, tmph); + xcb_render_composite( + xd->base.c, XCB_RENDER_PICT_OP_IN_REVERSE, tmp_pict2, + XCB_NONE, tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); + xcb_render_free_picture(xd->base.c, tmp_pict2); + } else { + xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_DIFFERENCE, + xd->white_pixel, XCB_NONE, tmp_pict, + 0, 0, 0, 0, 0, 0, tmpw, tmph); + } + } + if (img->dim != 0) { + // Dim the actually content of window + xcb_rectangle_t rect = { + .x = 0, + .y = 0, + .width = tmpw, + .height = tmph, + }; + + xcb_render_fill_rectangles(xd->base.c, XCB_RENDER_PICT_OP_OVER, + tmp_pict, dim_color, 1, &rect); + } + + xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_OVER, tmp_pict, + alpha_pict, result, 0, 0, 0, 0, to_i16_checked(dst_x1), + to_i16_checked(dst_y1), tmpew, tmpeh); + xcb_render_free_picture(xd->base.c, tmp_pict); + } else { + uint8_t op = (has_alpha ? XCB_RENDER_PICT_OP_OVER : XCB_RENDER_PICT_OP_SRC); + + xcb_render_composite(xd->base.c, op, inner->pict, alpha_pict, result, 0, + 0, 0, 0, to_i16_checked(dst_x1), + to_i16_checked(dst_y1), tmpew, tmpeh); + if (img->dim != 0 || img->color_inverted) { + // Apply properties, if we reach here, then has_alpha == false + assert(!has_alpha); + if (img->color_inverted) { + xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_DIFFERENCE, + xd->white_pixel, XCB_NONE, result, 0, + 0, 0, 0, to_i16_checked(dst_x1), + to_i16_checked(dst_y1), tmpew, tmpeh); + } + + if (img->dim != 0) { + // Dim the actually content of window + xcb_rectangle_t rect = { + .x = to_i16_checked(dst_x1), + .y = to_i16_checked(dst_y1), + .width = tmpew, + .height = tmpeh, + }; + + xcb_render_fill_rectangles(xd->base.c, XCB_RENDER_PICT_OP_OVER, + result, dim_color, 1, &rect); + } + } + } + pixman_region32_fini(®); +} + +static void compose(backend_t *base, void *img_data, + int dst_x1, int dst_y1, int dst_x2, int dst_y2, + const region_t *reg_paint, const region_t *reg_visible) { + // TODO(dccsillag): use dst_{x,y}2 + struct _xrender_data *xd = (void *)base; + return compose_impl(xd, img_data, dst_x1, dst_y1, dst_x2, dst_y2, reg_paint, reg_visible, xd->back[2]); +} + +static void fill(backend_t *base, struct color c, const region_t *clip) { + struct _xrender_data *xd = (void *)base; + const rect_t *extent = pixman_region32_extents((region_t *)clip); + x_set_picture_clip_region(base->c, xd->back[2], 0, 0, clip); + // color is in X fixed point representation + xcb_render_fill_rectangles( + base->c, XCB_RENDER_PICT_OP_OVER, xd->back[2], + (xcb_render_color_t){.red = (uint16_t)(c.red * 0xffff), + .green = (uint16_t)(c.green * 0xffff), + .blue = (uint16_t)(c.blue * 0xffff), + .alpha = (uint16_t)(c.alpha * 0xffff)}, + 1, + (xcb_rectangle_t[]){{.x = to_i16_checked(extent->x1), + .y = to_i16_checked(extent->y1), + .width = to_u16_checked(extent->x2 - extent->x1), + .height = to_u16_checked(extent->y2 - extent->y1)}}); +} + +static bool blur(backend_t *backend_data, double opacity, void *ctx_, + const region_t *reg_blur, const region_t *reg_visible) { + struct _xrender_blur_context *bctx = ctx_; + if (bctx->method == BLUR_METHOD_NONE) { + return true; + } + + struct _xrender_data *xd = (void *)backend_data; + xcb_connection_t *c = xd->base.c; + region_t reg_op; + pixman_region32_init(®_op); + pixman_region32_intersect(®_op, (region_t *)reg_blur, (region_t *)reg_visible); + if (!pixman_region32_not_empty(®_op)) { + pixman_region32_fini(®_op); + return true; + } + + region_t reg_op_resized = + resize_region(®_op, bctx->resize_width, bctx->resize_height); + + const pixman_box32_t *extent_resized = pixman_region32_extents(®_op_resized); + const auto height_resized = to_u16_checked(extent_resized->y2 - extent_resized->y1); + const auto width_resized = to_u16_checked(extent_resized->x2 - extent_resized->x1); + static const char *filter0 = "Nearest"; // The "null" filter + static const char *filter = "convolution"; + + // Create a buffer for storing blurred picture, make it just big enough + // for the blur region + const uint32_t pic_attrs_mask = XCB_RENDER_CP_REPEAT; + const xcb_render_create_picture_value_list_t pic_attrs = {.repeat = XCB_RENDER_REPEAT_PAD}; + xcb_render_picture_t tmp_picture[2] = { + x_create_picture_with_visual(xd->base.c, xd->base.root, width_resized, height_resized, + xd->default_visual, pic_attrs_mask, &pic_attrs), + x_create_picture_with_visual(xd->base.c, xd->base.root, width_resized, height_resized, + xd->default_visual, pic_attrs_mask, &pic_attrs)}; + + if (!tmp_picture[0] || !tmp_picture[1]) { + log_error("Failed to build intermediate Picture."); + pixman_region32_fini(®_op); + pixman_region32_fini(®_op_resized); + return false; + } + + region_t clip; + pixman_region32_init(&clip); + pixman_region32_copy(&clip, ®_op_resized); + pixman_region32_translate(&clip, -extent_resized->x1, -extent_resized->y1); + x_set_picture_clip_region(c, tmp_picture[0], 0, 0, &clip); + x_set_picture_clip_region(c, tmp_picture[1], 0, 0, &clip); + pixman_region32_fini(&clip); + + xcb_render_picture_t src_pict = xd->back[2], dst_pict = tmp_picture[0]; + auto alpha_pict = xd->alpha_pict[(int)(opacity * MAX_ALPHA)]; + int current = 0; + x_set_picture_clip_region(c, src_pict, 0, 0, ®_op_resized); + + // For more than 1 pass, we do: + // back -(pass 1)-> tmp0 -(pass 2)-> tmp1 ... + // -(pass n-1)-> tmp0 or tmp1 -(pass n)-> back + // For 1 pass, we do + // back -(pass 1)-> tmp0 -(copy)-> target_buffer + int i; + for (i = 0; i < bctx->x_blur_kernel_count; i++) { + // Copy from source picture to destination. The filter must + // be applied on source picture, to get the nearby pixels outside the + // window. + xcb_render_set_picture_filter(c, src_pict, to_u16_checked(strlen(filter)), + filter, + to_u32_checked(bctx->x_blur_kernel[i]->size), + bctx->x_blur_kernel[i]->kernel); + + if (i == 0) { + // First pass, back buffer -> tmp picture + // (we do this even if this is also the last pass, because we + // cannot do back buffer -> back buffer) + xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, src_pict, XCB_NONE, + dst_pict, to_i16_checked(extent_resized->x1), + to_i16_checked(extent_resized->y1), 0, 0, 0, + 0, width_resized, height_resized); + } else if (i < bctx->x_blur_kernel_count - 1) { + // This is not the last pass or the first pass, + // tmp picture 1 -> tmp picture 2 + xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, src_pict, + XCB_NONE, dst_pict, 0, 0, 0, 0, 0, 0, + width_resized, height_resized); + } else { + x_set_picture_clip_region(c, xd->back[2], 0, 0, ®_op); + // This is the last pass, and we are doing more than 1 pass + xcb_render_composite(c, XCB_RENDER_PICT_OP_OVER, src_pict, + alpha_pict, xd->back[2], 0, 0, 0, 0, + to_i16_checked(extent_resized->x1), + to_i16_checked(extent_resized->y1), + width_resized, height_resized); + } + + // reset filter + xcb_render_set_picture_filter( + c, src_pict, to_u16_checked(strlen(filter0)), filter0, 0, NULL); + + src_pict = tmp_picture[current]; + dst_pict = tmp_picture[!current]; + current = !current; + } + + // There is only 1 pass + if (i == 1) { + x_set_picture_clip_region(c, xd->back[2], 0, 0, ®_op); + xcb_render_composite( + c, XCB_RENDER_PICT_OP_OVER, src_pict, alpha_pict, xd->back[2], 0, 0, + 0, 0, to_i16_checked(extent_resized->x1), + to_i16_checked(extent_resized->y1), width_resized, height_resized); + } + + xcb_render_free_picture(c, tmp_picture[0]); + xcb_render_free_picture(c, tmp_picture[1]); + pixman_region32_fini(®_op); + pixman_region32_fini(®_op_resized); + return true; +} + +static void * +bind_pixmap(backend_t *base, xcb_pixmap_t pixmap, struct xvisual_info fmt, bool owned) { + xcb_generic_error_t *e; + auto r = xcb_get_geometry_reply(base->c, xcb_get_geometry(base->c, pixmap), &e); + if (!r) { + log_error("Invalid pixmap: %#010x", pixmap); + x_print_error(e->full_sequence, e->major_code, e->minor_code, e->error_code); + return NULL; + } + + auto img = ccalloc(1, struct backend_image); + auto inner = ccalloc(1, struct _xrender_image_data_inner); + inner->depth = (uint8_t)fmt.visual_depth; + inner->width = img->ewidth = r->width; + inner->height = img->eheight = r->height; + inner->pixmap = pixmap; + inner->has_alpha = fmt.alpha_size != 0; + inner->pict = + x_create_picture_with_visual_and_pixmap(base->c, fmt.visual, pixmap, 0, NULL); + inner->owned = owned; + inner->visual = fmt.visual; + inner->refcount = 1; + + img->inner = (struct backend_image_inner_base *)inner; + img->opacity = 1; + free(r); + + if (inner->pict == XCB_NONE) { + free(inner); + free(img); + return NULL; + } + return img; +} +static void release_image_inner(backend_t *base, struct _xrender_image_data_inner *inner) { + xcb_render_free_picture(base->c, inner->pict); + if (inner->owned) { + xcb_free_pixmap(base->c, inner->pixmap); + } + free(inner); +} +static void release_image(backend_t *base, void *image) { + struct backend_image *img = image; + img->inner->refcount--; + if (img->inner->refcount == 0) { + release_image_inner(base, (void *)img->inner); + } + free(img); +} + +static void deinit(backend_t *backend_data) { + struct _xrender_data *xd = (void *)backend_data; + for (int i = 0; i < 256; i++) { + xcb_render_free_picture(xd->base.c, xd->alpha_pict[i]); + } + xcb_render_free_picture(xd->base.c, xd->target); + for (int i = 0; i < 2; i++) { + xcb_render_free_picture(xd->base.c, xd->back[i]); + xcb_free_pixmap(xd->base.c, xd->back_pixmap[i]); + } + if (xd->present_event) { + xcb_unregister_for_special_event(xd->base.c, xd->present_event); + } + xcb_render_free_picture(xd->base.c, xd->white_pixel); + xcb_render_free_picture(xd->base.c, xd->black_pixel); + free(xd); +} + +static void present(backend_t *base, const region_t *region) { + struct _xrender_data *xd = (void *)base; + const rect_t *extent = pixman_region32_extents((region_t *)region); + int16_t orig_x = to_i16_checked(extent->x1), orig_y = to_i16_checked(extent->y1); + uint16_t region_width = to_u16_checked(extent->x2 - extent->x1), + region_height = to_u16_checked(extent->y2 - extent->y1); + + // compose() sets clip region on the back buffer, so clear it first + x_clear_picture_clip_region(base->c, xd->back[xd->curr_back]); + + // limit the region of update + x_set_picture_clip_region(base->c, xd->back[2], 0, 0, region); + + if (xd->vsync) { + // Update the back buffer first, then present + xcb_render_composite(base->c, XCB_RENDER_PICT_OP_SRC, xd->back[2], + XCB_NONE, xd->back[xd->curr_back], orig_x, orig_y, 0, + 0, orig_x, orig_y, region_width, region_height); + + // Make sure we got reply from PresentPixmap before waiting for events, + // to avoid deadlock + auto e = xcb_request_check( + base->c, xcb_present_pixmap_checked( + xd->base.c, xd->target_win, + xd->back_pixmap[xd->curr_back], 0, XCB_NONE, XCB_NONE, 0, + 0, XCB_NONE, XCB_NONE, XCB_NONE, 0, 0, 0, 0, 0, NULL)); + if (e) { + log_error("Failed to present pixmap"); + free(e); + return; + } + // TODO(yshui) don't block wait for present completion + xcb_present_generic_event_t *pev = + (void *)xcb_wait_for_special_event(base->c, xd->present_event); + if (!pev) { + // We don't know what happened, maybe X died + // But reset buffer age, so in case we do recover, we will + // render correctly. + xd->buffer_age[0] = xd->buffer_age[1] = -1; + return; + } + assert(pev->evtype == XCB_PRESENT_COMPLETE_NOTIFY); + xcb_present_complete_notify_event_t *pcev = (void *)pev; + // log_trace("Present complete: %d %ld", pcev->mode, pcev->msc); + xd->buffer_age[xd->curr_back] = 1; + + // buffer_age < 0 means that back buffer is empty + if (xd->buffer_age[1 - xd->curr_back] > 0) { + xd->buffer_age[1 - xd->curr_back]++; + } + if (pcev->mode == XCB_PRESENT_COMPLETE_MODE_FLIP) { + // We cannot use the pixmap we used anymore + xd->curr_back = 1 - xd->curr_back; + } + free(pev); + } else { + // No vsync needed, draw into the target picture directly + xcb_render_composite(base->c, XCB_RENDER_PICT_OP_SRC, xd->back[2], + XCB_NONE, xd->target, orig_x, orig_y, 0, 0, orig_x, + orig_y, region_width, region_height); + } +} + +static int buffer_age(backend_t *backend_data) { + struct _xrender_data *xd = (void *)backend_data; + if (!xd->vsync) { + // Only the target picture really holds the screen content, and its + // content is always up to date. So buffer age is always 1. + return 1; + } + return xd->buffer_age[xd->curr_back]; +} + +static struct _xrender_image_data_inner * +new_inner(backend_t *base, int w, int h, xcb_visualid_t visual, uint8_t depth) { + auto new_inner = ccalloc(1, struct _xrender_image_data_inner); + new_inner->pixmap = x_create_pixmap(base->c, depth, base->root, w, h); + if (new_inner->pixmap == XCB_NONE) { + log_error("Failed to create pixmap for copy"); + free(new_inner); + return NULL; + } + new_inner->pict = x_create_picture_with_visual_and_pixmap( + base->c, visual, new_inner->pixmap, 0, NULL); + if (new_inner->pict == XCB_NONE) { + log_error("Failed to create picture for copy"); + xcb_free_pixmap(base->c, new_inner->pixmap); + free(new_inner); + return NULL; + } + new_inner->width = w; + new_inner->height = h; + new_inner->visual = visual; + new_inner->depth = depth; + new_inner->refcount = 1; + new_inner->owned = true; + return new_inner; +} + +static bool decouple_image(backend_t *base, struct backend_image *img, const region_t *reg) { + if (img->inner->refcount == 1) { + return true; + } + auto inner = (struct _xrender_image_data_inner *)img->inner; + // Force new pixmap to a 32-bit ARGB visual to allow for transparent frames around + // non-transparent windows + auto visual = (inner->depth == 32) + ? inner->visual + : x_get_visual_for_standard(base->c, XCB_PICT_STANDARD_ARGB_32); + auto inner2 = new_inner(base, inner->width, inner->height, visual, 32); + if (!inner2) { + return false; + } + + x_set_picture_clip_region(base->c, inner->pict, 0, 0, reg); + xcb_render_composite(base->c, XCB_RENDER_PICT_OP_SRC, inner->pict, XCB_NONE, + inner2->pict, 0, 0, 0, 0, 0, 0, to_u16_checked(inner->width), + to_u16_checked(inner->height)); + + img->inner = (struct backend_image_inner_base *)inner2; + inner->refcount--; + return true; +} + +static bool image_op(backend_t *base, enum image_operations op, void *image, + const region_t *reg_op, const region_t *reg_visible, void *arg) { + struct _xrender_data *xd = (void *)base; + struct backend_image *img = image; + region_t reg; + double *dargs = arg; + + pixman_region32_init(®); + pixman_region32_intersect(®, (region_t *)reg_op, (region_t *)reg_visible); + + switch (op) { + case IMAGE_OP_APPLY_ALPHA: + assert(reg_op); + + if (!pixman_region32_not_empty(®)) { + break; + } + + if (dargs[0] == 1) { + break; + } + + if (!decouple_image(base, img, reg_visible)) { + pixman_region32_fini(®); + return false; + } + + auto inner = (struct _xrender_image_data_inner *)img->inner; + auto alpha_pict = xd->alpha_pict[(int)((1 - dargs[0]) * MAX_ALPHA)]; + x_set_picture_clip_region(base->c, inner->pict, 0, 0, ®); + xcb_render_composite(base->c, XCB_RENDER_PICT_OP_OUT_REVERSE, alpha_pict, + XCB_NONE, inner->pict, 0, 0, 0, 0, 0, 0, + to_u16_checked(inner->width), + to_u16_checked(inner->height)); + inner->has_alpha = true; + break; + } + pixman_region32_fini(®); + return true; +} + +static void * +create_blur_context(backend_t *base attr_unused, enum blur_method method, void *args) { + auto ret = ccalloc(1, struct _xrender_blur_context); + if (!method || method >= BLUR_METHOD_INVALID) { + ret->method = BLUR_METHOD_NONE; + return ret; + } + if (method == BLUR_METHOD_DUAL_KAWASE) { + log_warn("Blur method 'dual_kawase' is not compatible with the 'xrender' " + "backend."); + ret->method = BLUR_METHOD_NONE; + return ret; + } + + ret->method = BLUR_METHOD_KERNEL; + struct conv **kernels; + int kernel_count; + if (method == BLUR_METHOD_KERNEL) { + kernels = ((struct kernel_blur_args *)args)->kernels; + kernel_count = ((struct kernel_blur_args *)args)->kernel_count; + } else { + kernels = generate_blur_kernel(method, args, &kernel_count); + } + + ret->x_blur_kernel = ccalloc(kernel_count, struct x_convolution_kernel *); + for (int i = 0; i < kernel_count; i++) { + int center = kernels[i]->h * kernels[i]->w / 2; + x_create_convolution_kernel(kernels[i], kernels[i]->data[center], + &ret->x_blur_kernel[i]); + ret->resize_width += kernels[i]->w / 2; + ret->resize_height += kernels[i]->h / 2; + } + ret->x_blur_kernel_count = kernel_count; + + if (method != BLUR_METHOD_KERNEL) { + // Kernels generated by generate_blur_kernel, so we need to free them. + for (int i = 0; i < kernel_count; i++) { + free(kernels[i]); + } + free(kernels); + } + return ret; +} + +static void destroy_blur_context(backend_t *base attr_unused, void *ctx_) { + struct _xrender_blur_context *ctx = ctx_; + for (int i = 0; i < ctx->x_blur_kernel_count; i++) { + free(ctx->x_blur_kernel[i]); + } + free(ctx->x_blur_kernel); + free(ctx); +} + +static void get_blur_size(void *blur_context, int *width, int *height) { + struct _xrender_blur_context *ctx = blur_context; + *width = ctx->resize_width; + *height = ctx->resize_height; +} + +static bool +read_pixel(backend_t *backend_data, void *image_data, int x, int y, struct color *output) { + auto xd = (struct _xrender_data *)backend_data; + auto img = (struct backend_image *)image_data; + auto inner = (struct _xrender_image_data_inner *)img->inner; + + auto r = XCB_AWAIT(xcb_get_image, xd->base.c, XCB_IMAGE_FORMAT_XY_PIXMAP, inner->pixmap, + to_i16_checked(x), to_i16_checked(y), 1, 1, (uint32_t)-1L); + + if (!r) { + return false; + } + + // Color format seems to be BGRA8888, see glamor_format_for_pixmap from the + // Xserver codebase. + uint8_t *pixels = xcb_get_image_data(r); + output->blue = pixels[0] / 255.0; + output->green = pixels[1] / 255.0; + output->red = pixels[2] / 255.0; + output->alpha = pixels[3] / 255.0; + + return true; +} + +static backend_t *backend_xrender_init(session_t *ps) { + auto xd = ccalloc(1, struct _xrender_data); + init_backend_base(&xd->base, ps); + + for (int i = 0; i <= MAX_ALPHA; ++i) { + double o = (double)i / (double)MAX_ALPHA; + xd->alpha_pict[i] = solid_picture(ps->c, ps->root, false, o, 0, 0, 0); + assert(xd->alpha_pict[i] != XCB_NONE); + } + + xd->target_width = ps->root_width; + xd->target_height = ps->root_height; + xd->default_visual = ps->vis; + xd->black_pixel = solid_picture(ps->c, ps->root, true, 1, 0, 0, 0); + xd->white_pixel = solid_picture(ps->c, ps->root, true, 1, 1, 1, 1); + + xd->target_win = session_get_target_window(ps); + xcb_render_create_picture_value_list_t pa = { + .subwindowmode = XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS, + }; + xd->target = x_create_picture_with_visual_and_pixmap( + ps->c, ps->vis, xd->target_win, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); + + auto pictfmt = x_get_pictform_for_visual(ps->c, ps->vis); + if (!pictfmt) { + log_fatal("Default visual is invalid"); + abort(); + } + + xd->vsync = ps->o.vsync; + if (ps->present_exists) { + auto eid = x_new_id(ps->c); + auto e = + xcb_request_check(ps->c, xcb_present_select_input_checked( + ps->c, eid, xd->target_win, + XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY)); + if (e) { + log_error("Cannot select present input, vsync will be disabled"); + xd->vsync = false; + free(e); + } + + xd->present_event = + xcb_register_for_special_xge(ps->c, &xcb_present_id, eid, NULL); + if (!xd->present_event) { + log_error("Cannot register for special XGE, vsync will be " + "disabled"); + xd->vsync = false; + } + } else { + xd->vsync = false; + } + + // We might need to do double buffering for vsync, and buffer 0 and 1 are for + // double buffering. + int first_buffer_index = xd->vsync ? 0 : 2; + for (int i = first_buffer_index; i < 3; i++) { + xd->back_pixmap[i] = x_create_pixmap(ps->c, pictfmt->depth, ps->root, + to_u16_checked(ps->root_width), + to_u16_checked(ps->root_height)); + const uint32_t pic_attrs_mask = XCB_RENDER_CP_REPEAT; + const xcb_render_create_picture_value_list_t pic_attrs = { + .repeat = XCB_RENDER_REPEAT_PAD}; + xd->back[i] = x_create_picture_with_pictfmt_and_pixmap( + ps->c, pictfmt, xd->back_pixmap[i], pic_attrs_mask, &pic_attrs); + xd->buffer_age[i] = -1; + if (xd->back_pixmap[i] == XCB_NONE || xd->back[i] == XCB_NONE) { + log_error("Cannot create pixmap for rendering"); + goto err; + } + } + xd->curr_back = 0; + + return &xd->base; +err: + deinit(&xd->base); + return NULL; +} + +struct backend_operations xrender_ops = { + .init = backend_xrender_init, + .deinit = deinit, + .blur = blur, + .present = present, + .compose = compose, + .fill = fill, + .bind_pixmap = bind_pixmap, + .release_image = release_image, + .render_shadow = default_backend_render_shadow, + //.prepare_win = prepare_win, + //.release_win = release_win, + .is_image_transparent = default_is_image_transparent, + .buffer_age = buffer_age, + .max_buffer_age = 2, + + .image_op = image_op, + .read_pixel = read_pixel, + .clone_image = default_clone_image, + .set_image_property = default_set_image_property, + .create_blur_context = create_blur_context, + .destroy_blur_context = destroy_blur_context, + .get_blur_size = get_blur_size, +}; + +// vim: set noet sw=8 ts=8: -- cgit v1.2.3