diff options
Diffstat (limited to 'src')
65 files changed, 26522 insertions, 0 deletions
diff --git a/src/atom.c b/src/atom.c new file mode 100644 index 0000000..0272dc8 --- /dev/null +++ b/src/atom.c @@ -0,0 +1,37 @@ +#include <string.h> +#include <xcb/xcb.h> + +#include "atom.h" +#include "common.h" +#include "utils.h" +#include "log.h" + +static inline void *atom_getter(void *ud, const char *atom_name, int *err) { + xcb_connection_t *c = ud; + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply( + c, xcb_intern_atom(c, 0, to_u16_checked(strlen(atom_name)), atom_name), NULL); + + xcb_atom_t atom = XCB_NONE; + if (reply) { + log_debug("Atom %s is %d", atom_name, reply->atom); + atom = reply->atom; + free(reply); + } else { + log_error("Failed to intern atoms"); + *err = 1; + } + return (void *)(intptr_t)atom; +} + +/** + * Create a new atom structure and fetch all predefined atoms + */ +struct atom *init_atoms(xcb_connection_t *c) { + auto atoms = ccalloc(1, struct atom); + atoms->c = new_cache((void *)c, atom_getter, NULL); +#define ATOM_GET(x) atoms->a##x = (xcb_atom_t)(intptr_t)cache_get(atoms->c, #x, NULL) + LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST1); + LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST2); +#undef ATOM_GET + return atoms; +} diff --git a/src/atom.h b/src/atom.h new file mode 100644 index 0000000..baf3360 --- /dev/null +++ b/src/atom.h @@ -0,0 +1,68 @@ +#pragma once +#include <stdlib.h> + +#include <xcb/xcb.h> + +#include "meta.h" +#include "cache.h" + +// clang-format off +// Splitted into 2 lists because of the limitation of our macros +#define ATOM_LIST1 \ + _NET_WM_WINDOW_OPACITY, \ + _NET_FRAME_EXTENTS, \ + WM_STATE, \ + _NET_WM_NAME, \ + _NET_WM_PID, \ + WM_NAME, \ + WM_CLASS, \ + WM_ICON_NAME, \ + WM_TRANSIENT_FOR, \ + WM_WINDOW_ROLE, \ + WM_CLIENT_LEADER, \ + WM_CLIENT_MACHINE, \ + _NET_ACTIVE_WINDOW, \ + _COMPTON_SHADOW, \ + _NET_WM_WINDOW_TYPE, \ + _NET_CURRENT_DESKTOP + +#define ATOM_LIST2 \ + _NET_WM_WINDOW_TYPE_DESKTOP, \ + _NET_WM_WINDOW_TYPE_DOCK, \ + _NET_WM_WINDOW_TYPE_TOOLBAR, \ + _NET_WM_WINDOW_TYPE_MENU, \ + _NET_WM_WINDOW_TYPE_UTILITY, \ + _NET_WM_WINDOW_TYPE_SPLASH, \ + _NET_WM_WINDOW_TYPE_DIALOG, \ + _NET_WM_WINDOW_TYPE_NORMAL, \ + _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, \ + _NET_WM_WINDOW_TYPE_POPUP_MENU, \ + _NET_WM_WINDOW_TYPE_TOOLTIP, \ + _NET_WM_WINDOW_TYPE_NOTIFICATION, \ + _NET_WM_WINDOW_TYPE_COMBO, \ + _NET_WM_WINDOW_TYPE_DND, \ + _NET_WM_STATE, \ + _NET_WM_STATE_FULLSCREEN, \ + _NET_WM_BYPASS_COMPOSITOR, \ + UTF8_STRING, \ + C_STRING +// clang-format on + +#define ATOM_DEF(x) xcb_atom_t a##x + +struct atom { + struct cache *c; + LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST1); + LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST2); +}; + +struct atom *init_atoms(xcb_connection_t *); + +static inline xcb_atom_t get_atom(struct atom *a, const char *key) { + return (xcb_atom_t)(intptr_t)cache_get(a->c, key, NULL); +} + +static inline void destroy_atoms(struct atom *a) { + cache_free(a->c); + free(a); +} 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 <[email protected]> +#include <xcb/sync.h> +#include <xcb/xcb.h> + +#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 <damager-note> 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 + + // <damage-note> + // 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) { + // <transparent-clipping-note> + // 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: <transparent-clipping-note> + 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: <transparent-clipping-note> + 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 <[email protected]> + +#pragma once + +#include <stdbool.h> + +#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 <[email protected]> +#include <math.h> +#include <string.h> +#include <xcb/render.h> +#include <xcb/xcb_image.h> +#include <xcb/xcb_renderutil.h> + +#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 <code>Picture</code> 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 <code>Picture</code> 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 <[email protected]> +#pragma once + +#include <xcb/render.h> +#include <xcb/xcb_image.h> + +#include <stdbool.h> + +#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 <[email protected]> +#include <stdlib.h> +#include <string.h> + +#include <xcb/randr.h> +#include <xcb/xcb.h> + +#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 <[email protected]> + +#pragma once + +#include <stddef.h> +#include <stdio.h> +#include <xcb/xcb.h> + +#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 <uthash.h> +#include <xcb/xcb.h> + +#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 <[email protected]> +#include <GL/gl.h> +#include <GL/glext.h> +#include <locale.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> +#include <xcb/render.h> // 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 <[email protected]> +#pragma once +#include <GL/gl.h> +#include <GL/glext.h> +#include <stdbool.h> +#include <string.h> + +#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 <[email protected]> + * See LICENSE-mit for more information. + * + */ + +#include <X11/Xlib-xcb.h> +#include <assert.h> +#include <limits.h> +#include <pixman.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <xcb/composite.h> +#include <xcb/xcb.h> + +#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 <[email protected]> +#pragma once +#include <stdbool.h> +// 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 <GL/glx.h> +#undef glXSwapIntervalMESA +#undef glXBindTexImageEXT +#undef glXReleaseTexImageEXT +#include <X11/Xlib.h> +#include <xcb/xcb.h> +#include <xcb/render.h> + +#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 <[email protected]> +#include <assert.h> +#include <math.h> +#include <stdlib.h> +#include <string.h> + +#include <xcb/composite.h> +#include <xcb/present.h> +#include <xcb/render.h> +#include <xcb/sync.h> +#include <xcb/xcb.h> + +#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: diff --git a/src/c2.c b/src/c2.c new file mode 100644 index 0000000..3500f7b --- /dev/null +++ b/src/c2.c @@ -0,0 +1,1674 @@ +// SPDX-License-Identifier: MIT + +/* + * Compton - a compositor for X11 + * + * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard + * + * Copyright (c) 2011-2013, Christopher Jeffrey + * See LICENSE-mit for more information. + * + */ + +#include <ctype.h> +#include <fnmatch.h> +#include <stdio.h> +#include <string.h> + +// libpcre +#ifdef CONFIG_REGEX_PCRE +#include <pcre.h> + +// For compatibility with <libpcre-8.20 +#ifndef PCRE_STUDY_JIT_COMPILE +#define PCRE_STUDY_JIT_COMPILE 0 +#define LPCRE_FREE_STUDY(extra) pcre_free(extra) +#else +#define LPCRE_FREE_STUDY(extra) pcre_free_study(extra) +#endif + +#endif + +#include <X11/Xlib.h> +#include <xcb/xcb.h> + +#include "atom.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "log.h" +#include "string_utils.h" +#include "utils.h" +#include "win.h" +#include "x.h" + +#include "c2.h" + +#pragma GCC diagnostic error "-Wunused-parameter" + +#define C2_MAX_LEVELS 10 + +typedef struct _c2_b c2_b_t; +typedef struct _c2_l c2_l_t; + +/// Pointer to a condition tree. +typedef struct { + bool isbranch : 1; + union { + c2_b_t *b; + c2_l_t *l; + }; +} c2_ptr_t; + +/// Initializer for c2_ptr_t. +#define C2_PTR_INIT \ + { .isbranch = false, .l = NULL, } + +static const c2_ptr_t C2_PTR_NULL = C2_PTR_INIT; + +/// Operator of a branch element. +typedef enum { + C2_B_OUNDEFINED, + C2_B_OAND, + C2_B_OOR, + C2_B_OXOR, +} c2_b_op_t; + +/// Structure for branch element in a window condition +struct _c2_b { + bool neg : 1; + c2_b_op_t op; + c2_ptr_t opr1; + c2_ptr_t opr2; +}; + +/// Initializer for c2_b_t. +#define C2_B_INIT \ + { .neg = false, .op = C2_B_OUNDEFINED, .opr1 = C2_PTR_INIT, .opr2 = C2_PTR_INIT, } + +/// Structure for leaf element in a window condition +struct _c2_l { + bool neg : 1; + enum { C2_L_OEXISTS, + C2_L_OEQ, + C2_L_OGT, + C2_L_OGTEQ, + C2_L_OLT, + C2_L_OLTEQ, + } op : 3; + enum { C2_L_MEXACT, + C2_L_MSTART, + C2_L_MCONTAINS, + C2_L_MWILDCARD, + C2_L_MPCRE, + } match : 3; + bool match_ignorecase : 1; + char *tgt; + xcb_atom_t tgtatom; + bool tgt_onframe; + int index; + enum { C2_L_PUNDEFINED = -1, + C2_L_PID = 0, + C2_L_PX, + C2_L_PY, + C2_L_PX2, + C2_L_PY2, + C2_L_PWIDTH, + C2_L_PHEIGHT, + C2_L_PWIDTHB, + C2_L_PHEIGHTB, + C2_L_PBDW, + C2_L_PFULLSCREEN, + C2_L_POVREDIR, + C2_L_PARGB, + C2_L_PFOCUSED, + C2_L_PWMWIN, + C2_L_PBSHAPED, + C2_L_PROUNDED, + C2_L_PCLIENT, + C2_L_PWINDOWTYPE, + C2_L_PLEADER, + C2_L_PNAME, + C2_L_PCLASSG, + C2_L_PCLASSI, + C2_L_PROLE, + } predef; + enum c2_l_type { + C2_L_TUNDEFINED, + C2_L_TSTRING, + C2_L_TCARDINAL, + C2_L_TWINDOW, + C2_L_TATOM, + C2_L_TDRAWABLE, + } type; + int format; + enum { C2_L_PTUNDEFINED, + C2_L_PTSTRING, + C2_L_PTINT, + } ptntype; + char *ptnstr; + long ptnint; +#ifdef CONFIG_REGEX_PCRE + pcre *regex_pcre; + pcre_extra *regex_pcre_extra; +#endif +}; + +/// Initializer for c2_l_t. +#define C2_L_INIT \ + { \ + .neg = false, .op = C2_L_OEXISTS, .match = C2_L_MEXACT, \ + .match_ignorecase = false, .tgt = NULL, .tgtatom = 0, .tgt_onframe = false, \ + .predef = C2_L_PUNDEFINED, .index = 0, .type = C2_L_TUNDEFINED, \ + .format = 0, .ptntype = C2_L_PTUNDEFINED, .ptnstr = NULL, .ptnint = 0, \ + } + +static const c2_l_t leaf_def = C2_L_INIT; + +/// Linked list type of conditions. +struct _c2_lptr { + c2_ptr_t ptr; + void *data; + struct _c2_lptr *next; +}; + +/// Initializer for c2_lptr_t. +#define C2_LPTR_INIT \ + { .ptr = C2_PTR_INIT, .data = NULL, .next = NULL, } + +/// Structure representing a predefined target. +typedef struct { + const char *name; + enum c2_l_type type; + int format; +} c2_predef_t; + +// Predefined targets. +static const c2_predef_t C2_PREDEFS[] = { + [C2_L_PID] = {"id", C2_L_TCARDINAL, 0}, + [C2_L_PX] = {"x", C2_L_TCARDINAL, 0}, + [C2_L_PY] = {"y", C2_L_TCARDINAL, 0}, + [C2_L_PX2] = {"x2", C2_L_TCARDINAL, 0}, + [C2_L_PY2] = {"y2", C2_L_TCARDINAL, 0}, + [C2_L_PWIDTH] = {"width", C2_L_TCARDINAL, 0}, + [C2_L_PHEIGHT] = {"height", C2_L_TCARDINAL, 0}, + [C2_L_PWIDTHB] = {"widthb", C2_L_TCARDINAL, 0}, + [C2_L_PHEIGHTB] = {"heightb", C2_L_TCARDINAL, 0}, + [C2_L_PBDW] = {"border_width", C2_L_TCARDINAL, 0}, + [C2_L_PFULLSCREEN] = {"fullscreen", C2_L_TCARDINAL, 0}, + [C2_L_POVREDIR] = {"override_redirect", C2_L_TCARDINAL, 0}, + [C2_L_PARGB] = {"argb", C2_L_TCARDINAL, 0}, + [C2_L_PFOCUSED] = {"focused", C2_L_TCARDINAL, 0}, + [C2_L_PWMWIN] = {"wmwin", C2_L_TCARDINAL, 0}, + [C2_L_PBSHAPED] = {"bounding_shaped", C2_L_TCARDINAL, 0}, + [C2_L_PROUNDED] = {"rounded_corners", C2_L_TCARDINAL, 0}, + [C2_L_PCLIENT] = {"client", C2_L_TWINDOW, 0}, + [C2_L_PWINDOWTYPE] = {"window_type", C2_L_TSTRING, 0}, + [C2_L_PLEADER] = {"leader", C2_L_TWINDOW, 0}, + [C2_L_PNAME] = {"name", C2_L_TSTRING, 0}, + [C2_L_PCLASSG] = {"class_g", C2_L_TSTRING, 0}, + [C2_L_PCLASSI] = {"class_i", C2_L_TSTRING, 0}, + [C2_L_PROLE] = {"role", C2_L_TSTRING, 0}, +}; + +/** + * Get the numeric property value from a win_prop_t. + */ +static inline long winprop_get_int(winprop_t prop, size_t index) { + long tgt = 0; + + if (!prop.nitems || index >= prop.nitems) { + return 0; + } + + switch (prop.format) { + case 8: tgt = *(prop.p8 + index); break; + case 16: tgt = *(prop.p16 + index); break; + case 32: tgt = *(prop.p32 + index); break; + default: assert(0); break; + } + + return tgt; +} + +/** + * Compare next word in a string with another string. + */ +static inline int strcmp_wd(const char *needle, const char *src) { + int ret = mstrncmp(needle, src); + if (ret) + return ret; + + char c = src[strlen(needle)]; + if (isalnum((unsigned char)c) || '_' == c) + return 1; + else + return 0; +} + +/** + * Return whether a c2_ptr_t is empty. + */ +static inline attr_unused bool c2_ptr_isempty(const c2_ptr_t p) { + return !(p.isbranch ? (bool)p.b : (bool)p.l); +} + +/** + * Reset a c2_ptr_t. + */ +static inline void c2_ptr_reset(c2_ptr_t *pp) { + if (pp) + memcpy(pp, &C2_PTR_NULL, sizeof(c2_ptr_t)); +} + +/** + * Combine two condition trees. + */ +static inline c2_ptr_t c2h_comb_tree(c2_b_op_t op, c2_ptr_t p1, c2_ptr_t p2) { + c2_ptr_t p = {.isbranch = true, .b = NULL}; + p.b = cmalloc(c2_b_t); + + p.b->neg = false; + p.b->op = op; + p.b->opr1 = p1; + p.b->opr2 = p2; + + return p; +} + +/** + * Get the precedence value of a condition branch operator. + */ +static inline int c2h_b_opp(c2_b_op_t op) { + switch (op) { + case C2_B_OAND: return 2; + case C2_B_OOR: return 1; + case C2_B_OXOR: return 1; + default: break; + } + + assert(0); + return 0; +} + +/** + * Compare precedence of two condition branch operators. + * + * Associativity is left-to-right, forever. + * + * @return positive number if op1 > op2, 0 if op1 == op2 in precedence, + * negative number otherwise + */ +static inline int c2h_b_opcmp(c2_b_op_t op1, c2_b_op_t op2) { + return c2h_b_opp(op1) - c2h_b_opp(op2); +} + +static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int level); + +static int c2_parse_target(const char *pattern, int offset, c2_ptr_t *presult); + +static int c2_parse_op(const char *pattern, int offset, c2_ptr_t *presult); + +static int c2_parse_pattern(const char *pattern, int offset, c2_ptr_t *presult); + +static int c2_parse_legacy(const char *pattern, int offset, c2_ptr_t *presult); + +static void c2_free(c2_ptr_t p); + +/** + * Wrapper of c2_free(). + */ +static inline void c2_freep(c2_ptr_t *pp) { + if (pp) { + c2_free(*pp); + c2_ptr_reset(pp); + } +} + +static const char *c2h_dump_str_tgt(const c2_l_t *pleaf); + +static const char *c2h_dump_str_type(const c2_l_t *pleaf); + +static void attr_unused c2_dump(c2_ptr_t p); + +static xcb_atom_t c2_get_atom_type(const c2_l_t *pleaf); + +static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_ptr_t cond); + +/** + * Parse a condition string. + */ +c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data) { + if (!pattern) + return NULL; + + // Parse the pattern + c2_ptr_t result = C2_PTR_INIT; + int offset = -1; + + if (strlen(pattern) >= 2 && ':' == pattern[1]) + offset = c2_parse_legacy(pattern, 0, &result); + else + offset = c2_parse_grp(pattern, 0, &result, 0); + + if (offset < 0) { + c2_freep(&result); + return NULL; + } + + // Insert to pcondlst + { + static const c2_lptr_t lptr_def = C2_LPTR_INIT; + auto plptr = cmalloc(c2_lptr_t); + memcpy(plptr, &lptr_def, sizeof(c2_lptr_t)); + plptr->ptr = result; + plptr->data = data; + if (pcondlst) { + plptr->next = *pcondlst; + *pcondlst = plptr; + } + +#ifdef DEBUG_C2 + log_trace("(\"%s\"): ", pattern); + c2_dump(plptr->ptr); + putchar('\n'); +#endif + + return plptr; + } +} + +#define c2_error(format, ...) \ + do { \ + log_error("Pattern \"%s\" pos %d: " format, pattern, offset, ##__VA_ARGS__); \ + goto fail; \ + } while (0) + +// TODO(yshui) Not a very good macro, should probably be a function +#define C2H_SKIP_SPACES() \ + { \ + while (isspace((unsigned char)pattern[offset])) \ + ++offset; \ + } + +/** + * Parse a group in condition string. + * + * @return offset of next character in string + */ +static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int level) { + // Check for recursion levels + if (level > C2_MAX_LEVELS) + c2_error("Exceeded maximum recursion levels."); + + if (!pattern) + return -1; + + // Expected end character + const char endchar = (offset ? ')' : '\0'); + + // We use a system that a maximum of 2 elements are kept. When we find + // the third element, we combine the elements according to operator + // precedence. This design limits operators to have at most two-levels + // of precedence and fixed left-to-right associativity. + + // For storing branch operators. ops[0] is actually unused + c2_b_op_t ops[3] = {}; + // For storing elements + c2_ptr_t eles[2] = {C2_PTR_INIT, C2_PTR_INIT}; + // Index of next free element slot in eles + int elei = 0; + // Pointer to the position of next element + c2_ptr_t *pele = eles; + // Negation flag of next operator + bool neg = false; + // Whether we are expecting an element immediately, is true at first, or + // after encountering a logical operator + bool next_expected = true; + + // Parse the pattern character-by-character + for (; pattern[offset]; ++offset) { + assert(elei <= 2); + + // Jump over spaces + if (isspace((unsigned char)pattern[offset])) + continue; + + // Handle end of group + if (')' == pattern[offset]) + break; + + // Handle "!" + if ('!' == pattern[offset]) { + if (!next_expected) + c2_error("Unexpected \"!\"."); + + neg = !neg; + continue; + } + + // Handle AND and OR + if ('&' == pattern[offset] || '|' == pattern[offset]) { + if (next_expected) + c2_error("Unexpected logical operator."); + + next_expected = true; + if (!mstrncmp("&&", pattern + offset)) { + ops[elei] = C2_B_OAND; + ++offset; + } else if (!mstrncmp("||", pattern + offset)) { + ops[elei] = C2_B_OOR; + ++offset; + } else + c2_error("Illegal logical operator."); + + continue; + } + + // Parsing an element + if (!next_expected) + c2_error("Unexpected expression."); + + assert(!elei || ops[elei]); + + // If we are out of space + if (2 == elei) { + --elei; + // If the first operator has higher or equal precedence, combine + // the first two elements + if (c2h_b_opcmp(ops[1], ops[2]) >= 0) { + eles[0] = c2h_comb_tree(ops[1], eles[0], eles[1]); + c2_ptr_reset(&eles[1]); + pele = &eles[elei]; + ops[1] = ops[2]; + } + // Otherwise, combine the second and the incoming one + else { + eles[1] = c2h_comb_tree(ops[2], eles[1], C2_PTR_NULL); + assert(eles[1].isbranch); + pele = &eles[1].b->opr2; + } + // The last operator always needs to be reset + ops[2] = C2_B_OUNDEFINED; + } + + // It's a subgroup if it starts with '(' + if ('(' == pattern[offset]) { + if ((offset = c2_parse_grp(pattern, offset + 1, pele, level + 1)) < 0) + goto fail; + } + // Otherwise it's a leaf + else { + if ((offset = c2_parse_target(pattern, offset, pele)) < 0) + goto fail; + + assert(!pele->isbranch && !c2_ptr_isempty(*pele)); + + if ((offset = c2_parse_op(pattern, offset, pele)) < 0) + goto fail; + + if ((offset = c2_parse_pattern(pattern, offset, pele)) < 0) + goto fail; + } + // Decrement offset -- we will increment it in loop update + --offset; + + // Apply negation + if (neg) { + neg = false; + if (pele->isbranch) + pele->b->neg = !pele->b->neg; + else + pele->l->neg = !pele->l->neg; + } + + next_expected = false; + ++elei; + pele = &eles[elei]; + } + + // Wrong end character? + if (pattern[offset] && !endchar) + c2_error("Expected end of string but found '%c'.", pattern[offset]); + if (!pattern[offset] && endchar) + c2_error("Expected '%c' but found end of string.", endchar); + + // Handle end of group + if (!elei) { + c2_error("Empty group."); + } else if (next_expected) { + c2_error("Missing rule before end of group."); + } else if (elei > 1) { + assert(2 == elei); + assert(ops[1]); + eles[0] = c2h_comb_tree(ops[1], eles[0], eles[1]); + c2_ptr_reset(&eles[1]); + } + + *presult = eles[0]; + + if (')' == pattern[offset]) + ++offset; + + return offset; + +fail: + c2_freep(&eles[0]); + c2_freep(&eles[1]); + + return -1; +} + +/** + * Parse the target part of a rule. + */ +static int c2_parse_target(const char *pattern, int offset, c2_ptr_t *presult) { + // Initialize leaf + presult->isbranch = false; + presult->l = cmalloc(c2_l_t); + + c2_l_t *const pleaf = presult->l; + memcpy(pleaf, &leaf_def, sizeof(c2_l_t)); + + // Parse negation marks + while ('!' == pattern[offset]) { + pleaf->neg = !pleaf->neg; + ++offset; + C2H_SKIP_SPACES(); + } + + // Copy target name out + int tgtlen = 0; + for (; pattern[offset] && + (isalnum((unsigned char)pattern[offset]) || '_' == pattern[offset]); + ++offset) { + ++tgtlen; + } + if (!tgtlen) { + c2_error("Empty target."); + } + pleaf->tgt = strndup(&pattern[offset - tgtlen], (size_t)tgtlen); + + // Check for predefined targets + static const int npredefs = (int)(sizeof(C2_PREDEFS) / sizeof(C2_PREDEFS[0])); + for (int i = 0; i < npredefs; ++i) { + if (!strcmp(C2_PREDEFS[i].name, pleaf->tgt)) { + pleaf->predef = i; + pleaf->type = C2_PREDEFS[i].type; + pleaf->format = C2_PREDEFS[i].format; + break; + } + } + + C2H_SKIP_SPACES(); + + // Parse target-on-frame flag + if ('@' == pattern[offset]) { + pleaf->tgt_onframe = true; + ++offset; + C2H_SKIP_SPACES(); + } + + // Parse index + if ('[' == pattern[offset]) { + if (pleaf->predef != C2_L_PUNDEFINED) { + c2_error("Predefined targets can't have index."); + } + + offset++; + + C2H_SKIP_SPACES(); + + long index = -1; + const char *endptr = NULL; + + if ('*' == pattern[offset]) { + index = -1; + endptr = pattern + offset + 1; + } else { + index = strtol(pattern + offset, (char **)&endptr, 0); + if (index < 0) { + c2_error("Index number invalid."); + } + } + + if (!endptr || pattern + offset == endptr) { + c2_error("No index number found after bracket."); + } + + pleaf->index = to_int_checked(index); + offset = to_int_checked(endptr - pattern); + + C2H_SKIP_SPACES(); + + if (pattern[offset] != ']') { + c2_error("Index end marker not found."); + } + + ++offset; + + C2H_SKIP_SPACES(); + } + + // Parse target type and format + if (':' == pattern[offset]) { + ++offset; + C2H_SKIP_SPACES(); + + // Look for format + bool hasformat = false; + long format = 0; + { + char *endptr = NULL; + format = strtol(pattern + offset, &endptr, 0); + assert(endptr); + if ((hasformat = (endptr && endptr != pattern + offset))) { + offset = to_int_checked(endptr - pattern); + } + C2H_SKIP_SPACES(); + } + + // Look for type + enum c2_l_type type = C2_L_TUNDEFINED; + switch (pattern[offset]) { + case 'w': type = C2_L_TWINDOW; break; + case 'd': type = C2_L_TDRAWABLE; break; + case 'c': type = C2_L_TCARDINAL; break; + case 's': type = C2_L_TSTRING; break; + case 'a': type = C2_L_TATOM; break; + default: c2_error("Invalid type character."); + } + + if (type) { + if (pleaf->predef != C2_L_PUNDEFINED) { + log_warn("Type specified for a default target " + "will be ignored."); + } else { + if (pleaf->type && type != pleaf->type) { + log_warn("Default type overridden on " + "target."); + } + pleaf->type = type; + } + } + + offset++; + C2H_SKIP_SPACES(); + + // Default format + if (!pleaf->format) { + switch (pleaf->type) { + case C2_L_TWINDOW: + case C2_L_TDRAWABLE: + case C2_L_TATOM: pleaf->format = 32; break; + case C2_L_TSTRING: pleaf->format = 8; break; + default: break; + } + } + + // Write format + if (hasformat) { + if (pleaf->predef != C2_L_PUNDEFINED) { + log_warn("Format \"%ld\" specified on a default target " + "will be ignored.", + format); + } else if (pleaf->type == C2_L_TSTRING) { + log_warn("Format \"%ld\" specified on a string target " + "will be ignored.", + format); + } else { + if (pleaf->format && pleaf->format != format) { + log_warn("Default format %d overridden on " + "target.", + pleaf->format); + } + pleaf->format = to_int_checked(format); + } + } + } + + if (!pleaf->type) { + c2_error("Target type cannot be determined."); + } + + // if (!pleaf->predef && !pleaf->format && C2_L_TSTRING != pleaf->type) + // c2_error("Target format cannot be determined."); + + if (pleaf->format && 8 != pleaf->format && 16 != pleaf->format && 32 != pleaf->format) { + c2_error("Invalid format."); + } + + return offset; + +fail: + return -1; +} + +/** + * Parse the operator part of a leaf. + */ +static int c2_parse_op(const char *pattern, int offset, c2_ptr_t *presult) { + c2_l_t *const pleaf = presult->l; + + // Parse negation marks + C2H_SKIP_SPACES(); + while ('!' == pattern[offset]) { + pleaf->neg = !pleaf->neg; + ++offset; + C2H_SKIP_SPACES(); + } + + // Parse qualifiers + if ('*' == pattern[offset] || '^' == pattern[offset] || '%' == pattern[offset] || + '~' == pattern[offset]) { + switch (pattern[offset]) { + case '*': pleaf->match = C2_L_MCONTAINS; break; + case '^': pleaf->match = C2_L_MSTART; break; + case '%': pleaf->match = C2_L_MWILDCARD; break; + case '~': pleaf->match = C2_L_MPCRE; break; + default: assert(0); + } + ++offset; + C2H_SKIP_SPACES(); + } + + // Parse flags + while ('?' == pattern[offset]) { + pleaf->match_ignorecase = true; + ++offset; + C2H_SKIP_SPACES(); + } + + // Parse operator + while ('=' == pattern[offset] || '>' == pattern[offset] || '<' == pattern[offset]) { + if ('=' == pattern[offset] && C2_L_OGT == pleaf->op) + pleaf->op = C2_L_OGTEQ; + else if ('=' == pattern[offset] && C2_L_OLT == pleaf->op) + pleaf->op = C2_L_OLTEQ; + else if (pleaf->op) { + c2_error("Duplicate operator."); + } else { + switch (pattern[offset]) { + case '=': pleaf->op = C2_L_OEQ; break; + case '>': pleaf->op = C2_L_OGT; break; + case '<': pleaf->op = C2_L_OLT; break; + default: assert(0); + } + } + ++offset; + C2H_SKIP_SPACES(); + } + + // Check for problems + if (C2_L_OEQ != pleaf->op && (pleaf->match || pleaf->match_ignorecase)) + c2_error("Exists/greater-than/less-than operators cannot have a " + "qualifier."); + + return offset; + +fail: + return -1; +} + +/** + * Parse the pattern part of a leaf. + */ +static int c2_parse_pattern(const char *pattern, int offset, c2_ptr_t *presult) { + c2_l_t *const pleaf = presult->l; + + // Exists operator cannot have pattern + if (!pleaf->op) { + return offset; + } + + C2H_SKIP_SPACES(); + + char *endptr = NULL; + if (!strcmp_wd("true", &pattern[offset])) { + pleaf->ptntype = C2_L_PTINT; + pleaf->ptnint = true; + offset += 4; // length of "true"; + } else if (!strcmp_wd("false", &pattern[offset])) { + pleaf->ptntype = C2_L_PTINT; + pleaf->ptnint = false; + offset += 5; // length of "false"; + } else if (pleaf->ptnint = strtol(pattern + offset, &endptr, 0), + pattern + offset != endptr) { + pleaf->ptntype = C2_L_PTINT; + offset = to_int_checked(endptr - pattern); + // Make sure we are stopping at the end of a word + if (isalnum((unsigned char)pattern[offset])) { + c2_error("Trailing characters after a numeric pattern."); + } + } else { + // Parse string patterns + bool raw = false; + char delim = '\0'; + + // String flags + if (tolower((unsigned char)pattern[offset]) == 'r') { + raw = true; + ++offset; + C2H_SKIP_SPACES(); + } + + // Check for delimiters + if (pattern[offset] == '\"' || pattern[offset] == '\'') { + pleaf->ptntype = C2_L_PTSTRING; + delim = pattern[offset]; + ++offset; + } + + if (pleaf->ptntype != C2_L_PTSTRING) { + c2_error("Invalid pattern type."); + } + + // Parse the string now + // We can't determine the length of the pattern, so we use the length + // to the end of the pattern string -- currently escape sequences + // cannot be converted to a string longer than itself. + auto tptnstr = ccalloc((strlen(pattern + offset) + 1), char); + char *ptptnstr = tptnstr; + pleaf->ptnstr = tptnstr; + for (; pattern[offset] && delim != pattern[offset]; ++offset) { + // Handle escape sequences if it's not a raw string + if ('\\' == pattern[offset] && !raw) { + switch (pattern[++offset]) { + case '\\': *(ptptnstr++) = '\\'; break; + case '\'': *(ptptnstr++) = '\''; break; + case '\"': *(ptptnstr++) = '\"'; break; + case 'a': *(ptptnstr++) = '\a'; break; + case 'b': *(ptptnstr++) = '\b'; break; + case 'f': *(ptptnstr++) = '\f'; break; + case 'n': *(ptptnstr++) = '\n'; break; + case 'r': *(ptptnstr++) = '\r'; break; + case 't': *(ptptnstr++) = '\t'; break; + case 'v': *(ptptnstr++) = '\v'; break; + case 'o': + case 'x': { + char *tstr = strndup(pattern + offset + 1, 2); + char *pstr = NULL; + long val = strtol( + tstr, &pstr, ('o' == pattern[offset] ? 8 : 16)); + free(tstr); + if (pstr != &tstr[2] || val <= 0) + c2_error("Invalid octal/hex escape " + "sequence."); + *(ptptnstr++) = to_char_checked(val); + offset += 2; + break; + } + default: c2_error("Invalid escape sequence."); + } + } else { + *(ptptnstr++) = pattern[offset]; + } + } + if (!pattern[offset]) + c2_error("Premature end of pattern string."); + ++offset; + *ptptnstr = '\0'; + pleaf->ptnstr = strdup(tptnstr); + free(tptnstr); + } + + C2H_SKIP_SPACES(); + + if (!pleaf->ptntype) + c2_error("Invalid pattern type."); + + // Check if the type is correct + if (!(((C2_L_TSTRING == pleaf->type || C2_L_TATOM == pleaf->type) && + C2_L_PTSTRING == pleaf->ptntype) || + ((C2_L_TCARDINAL == pleaf->type || C2_L_TWINDOW == pleaf->type || + C2_L_TDRAWABLE == pleaf->type) && + C2_L_PTINT == pleaf->ptntype))) + c2_error("Pattern type incompatible with target type."); + + if (C2_L_PTINT == pleaf->ptntype && pleaf->match) + c2_error("Integer/boolean pattern cannot have operator qualifiers."); + + if (C2_L_PTINT == pleaf->ptntype && pleaf->match_ignorecase) + c2_error("Integer/boolean pattern cannot have flags."); + + if (C2_L_PTSTRING == pleaf->ptntype && + (C2_L_OGT == pleaf->op || C2_L_OGTEQ == pleaf->op || C2_L_OLT == pleaf->op || + C2_L_OLTEQ == pleaf->op)) + c2_error("String pattern cannot have an arithmetic operator."); + + return offset; + +fail: + return -1; +} + +/** + * Parse a condition with legacy syntax. + */ +static int c2_parse_legacy(const char *pattern, int offset, c2_ptr_t *presult) { + if (strlen(pattern + offset) < 4 || pattern[offset + 1] != ':' || + !strchr(pattern + offset + 2, ':')) { + c2_error("Legacy parser: Invalid format."); + } + + // Allocate memory for new leaf + auto pleaf = cmalloc(c2_l_t); + presult->isbranch = false; + presult->l = pleaf; + memcpy(pleaf, &leaf_def, sizeof(c2_l_t)); + pleaf->type = C2_L_TSTRING; + pleaf->op = C2_L_OEQ; + pleaf->ptntype = C2_L_PTSTRING; + + // Determine the pattern target +#define TGTFILL(pdefid) \ + (pleaf->predef = pdefid, pleaf->type = C2_PREDEFS[pdefid].type, \ + pleaf->format = C2_PREDEFS[pdefid].format) + switch (pattern[offset]) { + case 'n': TGTFILL(C2_L_PNAME); break; + case 'i': TGTFILL(C2_L_PCLASSI); break; + case 'g': TGTFILL(C2_L_PCLASSG); break; + case 'r': TGTFILL(C2_L_PROLE); break; + default: c2_error("Target \"%c\" invalid.\n", pattern[offset]); + } +#undef TGTFILL + + offset += 2; + + // Determine the match type + switch (pattern[offset]) { + case 'e': pleaf->match = C2_L_MEXACT; break; + case 'a': pleaf->match = C2_L_MCONTAINS; break; + case 's': pleaf->match = C2_L_MSTART; break; + case 'w': pleaf->match = C2_L_MWILDCARD; break; + case 'p': pleaf->match = C2_L_MPCRE; break; + default: c2_error("Type \"%c\" invalid.\n", pattern[offset]); + } + ++offset; + + // Determine the pattern flags + while (':' != pattern[offset]) { + switch (pattern[offset]) { + case 'i': pleaf->match_ignorecase = true; break; + default: c2_error("Flag \"%c\" invalid.", pattern[offset]); + } + ++offset; + } + ++offset; + + // Copy the pattern + pleaf->ptnstr = strdup(pattern + offset); + + return offset; + +fail: + return -1; +} + +#undef c2_error + +/** + * Do postprocessing on a condition leaf. + */ +static bool c2_l_postprocess(session_t *ps, c2_l_t *pleaf) { + // Give a pattern type to a leaf with exists operator, if needed + if (C2_L_OEXISTS == pleaf->op && !pleaf->ptntype) { + pleaf->ptntype = (C2_L_TSTRING == pleaf->type ? C2_L_PTSTRING : C2_L_PTINT); + } + + // Get target atom if it's not a predefined one + if (pleaf->predef == C2_L_PUNDEFINED) { + pleaf->tgtatom = get_atom(ps->atoms, pleaf->tgt); + if (!pleaf->tgtatom) { + log_error("Failed to get atom for target \"%s\".", pleaf->tgt); + return false; + } + } + + // Insert target Atom into atom track list + if (pleaf->tgtatom) { + bool found = false; + for (latom_t *platom = ps->track_atom_lst; platom; platom = platom->next) { + if (pleaf->tgtatom == platom->atom) { + found = true; + break; + } + } + if (!found) { + auto pnew = cmalloc(latom_t); + pnew->next = ps->track_atom_lst; + pnew->atom = pleaf->tgtatom; + ps->track_atom_lst = pnew; + } + } + + // Warn about lower case characters in target name + if (pleaf->predef == C2_L_PUNDEFINED) { + for (const char *pc = pleaf->tgt; *pc; ++pc) { + if (islower((unsigned char)*pc)) { + log_warn("Lowercase character in target name \"%s\".", + pleaf->tgt); + break; + } + } + } + + // PCRE patterns + if (C2_L_PTSTRING == pleaf->ptntype && C2_L_MPCRE == pleaf->match) { +#ifdef CONFIG_REGEX_PCRE + const char *error = NULL; + int erroffset = 0; + int options = 0; + + // Ignore case flag + if (pleaf->match_ignorecase) + options |= PCRE_CASELESS; + + // Compile PCRE expression + pleaf->regex_pcre = + pcre_compile(pleaf->ptnstr, options, &error, &erroffset, NULL); + if (!pleaf->regex_pcre) { + log_error("Pattern \"%s\": PCRE regular expression parsing " + "failed on " + "offset %d: %s", + pleaf->ptnstr, erroffset, error); + return false; + } +#ifdef CONFIG_REGEX_PCRE_JIT + pleaf->regex_pcre_extra = + pcre_study(pleaf->regex_pcre, PCRE_STUDY_JIT_COMPILE, &error); + if (!pleaf->regex_pcre_extra) { + printf("Pattern \"%s\": PCRE regular expression study failed: %s", + pleaf->ptnstr, error); + } +#endif + + // Free the target string + // free(pleaf->tgt); + // pleaf->tgt = NULL; +#else + log_error("PCRE regular expression support not compiled in."); + return false; +#endif + } + + return true; +} + +static bool c2_tree_postprocess(session_t *ps, c2_ptr_t node) { + if (!node.isbranch) { + return c2_l_postprocess(ps, node.l); + } + if (!c2_tree_postprocess(ps, node.b->opr1)) + return false; + return c2_tree_postprocess(ps, node.b->opr2); +} + +bool c2_list_postprocess(session_t *ps, c2_lptr_t *list) { + c2_lptr_t *head = list; + while (head) { + if (!c2_tree_postprocess(ps, head->ptr)) + return false; + head = head->next; + } + return true; +} +/** + * Free a condition tree. + */ +static void c2_free(c2_ptr_t p) { + // For a branch element + if (p.isbranch) { + c2_b_t *const pbranch = p.b; + + if (!pbranch) + return; + + c2_free(pbranch->opr1); + c2_free(pbranch->opr2); + free(pbranch); + } + // For a leaf element + else { + c2_l_t *const pleaf = p.l; + + if (!pleaf) + return; + + free(pleaf->tgt); + free(pleaf->ptnstr); +#ifdef CONFIG_REGEX_PCRE + pcre_free(pleaf->regex_pcre); + LPCRE_FREE_STUDY(pleaf->regex_pcre_extra); +#endif + free(pleaf); + } +} + +/** + * Free a condition tree in c2_lptr_t. + */ +c2_lptr_t *c2_free_lptr(c2_lptr_t *lp) { + if (!lp) + return NULL; + + c2_lptr_t *pnext = lp->next; + c2_free(lp->ptr); + free(lp); + + return pnext; +} + +/** + * Get a string representation of a rule target. + */ +static const char *c2h_dump_str_tgt(const c2_l_t *pleaf) { + if (pleaf->predef != C2_L_PUNDEFINED) { + return C2_PREDEFS[pleaf->predef].name; + } else { + return pleaf->tgt; + } +} + +/** + * Get a string representation of a target. + */ +static const char *c2h_dump_str_type(const c2_l_t *pleaf) { + switch (pleaf->type) { + case C2_L_TWINDOW: return "w"; + case C2_L_TDRAWABLE: return "d"; + case C2_L_TCARDINAL: return "c"; + case C2_L_TSTRING: return "s"; + case C2_L_TATOM: return "a"; + case C2_L_TUNDEFINED: break; + } + + return NULL; +} + +/** + * Dump a condition tree. + */ +static void c2_dump(c2_ptr_t p) { + // For a branch + if (p.isbranch) { + const c2_b_t *const pbranch = p.b; + + if (!pbranch) { + return; + } + + if (pbranch->neg) { + putchar('!'); + } + + printf("("); + c2_dump(pbranch->opr1); + + switch (pbranch->op) { + case C2_B_OAND: printf(" && "); break; + case C2_B_OOR: printf(" || "); break; + case C2_B_OXOR: printf(" XOR "); break; + default: assert(0); break; + } + + c2_dump(pbranch->opr2); + printf(") "); + } + // For a leaf + else { + const c2_l_t *const pleaf = p.l; + + if (!pleaf) { + return; + } + + if (C2_L_OEXISTS == pleaf->op && pleaf->neg) { + putchar('!'); + } + + // Print target name, type, and format + { + printf("%s", c2h_dump_str_tgt(pleaf)); + if (pleaf->tgt_onframe) { + putchar('@'); + } + if (pleaf->predef == C2_L_PUNDEFINED) { + if (pleaf->index < 0) { + printf("[*]"); + } else { + printf("[%d]", pleaf->index); + } + } + printf(":%d%s", pleaf->format, c2h_dump_str_type(pleaf)); + } + + // Print operator + putchar(' '); + + if (C2_L_OEXISTS != pleaf->op && pleaf->neg) { + putchar('!'); + } + + switch (pleaf->match) { + case C2_L_MEXACT: break; + case C2_L_MCONTAINS: putchar('*'); break; + case C2_L_MSTART: putchar('^'); break; + case C2_L_MPCRE: putchar('~'); break; + case C2_L_MWILDCARD: putchar('%'); break; + } + + if (pleaf->match_ignorecase) { + putchar('?'); + } + + switch (pleaf->op) { + case C2_L_OEXISTS: break; + case C2_L_OEQ: fputs("=", stdout); break; + case C2_L_OGT: fputs(">", stdout); break; + case C2_L_OGTEQ: fputs(">=", stdout); break; + case C2_L_OLT: fputs("<", stdout); break; + case C2_L_OLTEQ: fputs("<=", stdout); break; + } + + if (C2_L_OEXISTS == pleaf->op) { + return; + } + + // Print pattern + putchar(' '); + switch (pleaf->ptntype) { + case C2_L_PTINT: printf("%ld", pleaf->ptnint); break; + case C2_L_PTSTRING: + // TODO(yshui) Escape string before printing out? + printf("\"%s\"", pleaf->ptnstr); + break; + default: assert(0); break; + } + } +} + +/** + * Get the type atom of a condition. + */ +static xcb_atom_t c2_get_atom_type(const c2_l_t *pleaf) { + switch (pleaf->type) { + case C2_L_TCARDINAL: return XCB_ATOM_CARDINAL; + case C2_L_TWINDOW: return XCB_ATOM_WINDOW; + case C2_L_TSTRING: return XCB_ATOM_STRING; + case C2_L_TATOM: return XCB_ATOM_ATOM; + case C2_L_TDRAWABLE: return XCB_ATOM_DRAWABLE; + default: assert(0); break; + } + unreachable; +} + +/** + * Match a window against a single leaf window condition. + * + * For internal use. + */ +static inline void c2_match_once_leaf(session_t *ps, const struct managed_win *w, + const c2_l_t *pleaf, bool *pres, bool *perr) { + assert(pleaf); + + const xcb_window_t wid = (pleaf->tgt_onframe ? w->client_win : w->base.id); + + // Return if wid is missing + if (pleaf->predef == C2_L_PUNDEFINED && !wid) { + return; + } + + const int idx = (pleaf->index < 0 ? 0 : pleaf->index); + + switch (pleaf->ptntype) { + // Deal with integer patterns + case C2_L_PTINT: { + long *targets = NULL; + long *targets_free = NULL; + size_t ntargets = 0; + + // Get the value + // A predefined target + long predef_target = 0; + if (pleaf->predef != C2_L_PUNDEFINED) { + *perr = false; + switch (pleaf->predef) { + case C2_L_PID: predef_target = wid; break; + case C2_L_PX: predef_target = w->g.x; break; + case C2_L_PY: predef_target = w->g.y; break; + case C2_L_PX2: predef_target = w->g.x + w->widthb; break; + case C2_L_PY2: predef_target = w->g.y + w->heightb; break; + case C2_L_PWIDTH: predef_target = w->g.width; break; + case C2_L_PHEIGHT: predef_target = w->g.height; break; + case C2_L_PWIDTHB: predef_target = w->widthb; break; + case C2_L_PHEIGHTB: predef_target = w->heightb; break; + case C2_L_PBDW: predef_target = w->g.border_width; break; + case C2_L_PFULLSCREEN: + predef_target = win_is_fullscreen(ps, w); + break; + case C2_L_POVREDIR: predef_target = w->a.override_redirect; break; + case C2_L_PARGB: predef_target = win_has_alpha(w); break; + case C2_L_PFOCUSED: + predef_target = win_is_focused_raw(ps, w); + break; + case C2_L_PWMWIN: predef_target = w->wmwin; break; + case C2_L_PBSHAPED: predef_target = w->bounding_shaped; break; + case C2_L_PROUNDED: predef_target = w->rounded_corners; break; + case C2_L_PCLIENT: predef_target = w->client_win; break; + case C2_L_PLEADER: predef_target = w->leader; break; + default: + *perr = true; + assert(0); + break; + } + ntargets = 1; + targets = &predef_target; + } + // A raw window property + else { + int word_count = 1; + if (pleaf->index < 0) { + // Get length of property in 32-bit multiples + auto prop_info = x_get_prop_info(ps->c, wid, pleaf->tgtatom); + word_count = to_int_checked((prop_info.length + 4 - 1) / 4); + } + winprop_t prop = x_get_prop_with_offset( + ps->c, wid, pleaf->tgtatom, idx, word_count, + c2_get_atom_type(pleaf), pleaf->format); + + ntargets = (pleaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); + if (ntargets > 0) { + targets = targets_free = ccalloc(ntargets, long); + *perr = false; + for (size_t i = 0; i < ntargets; ++i) { + targets[i] = winprop_get_int(prop, i); + } + } + free_winprop(&prop); + } + + if (*perr) { + goto fail_int; + } + + // Do comparison + bool res = false; + for (size_t i = 0; i < ntargets; ++i) { + long tgt = targets[i]; + switch (pleaf->op) { + case C2_L_OEXISTS: + res = (pleaf->predef != C2_L_PUNDEFINED ? tgt : true); + break; + case C2_L_OEQ: res = (tgt == pleaf->ptnint); break; + case C2_L_OGT: res = (tgt > pleaf->ptnint); break; + case C2_L_OGTEQ: res = (tgt >= pleaf->ptnint); break; + case C2_L_OLT: res = (tgt < pleaf->ptnint); break; + case C2_L_OLTEQ: res = (tgt <= pleaf->ptnint); break; + default: *perr = true; assert(0); + } + if (res) { + break; + } + } + *pres = res; + + fail_int: + // Free property values after usage, if necessary + if (targets_free) { + free(targets_free); + } + } break; + // String patterns + case C2_L_PTSTRING: { + const char **targets = NULL; + const char **targets_free = NULL; + const char **targets_free_inner = NULL; + size_t ntargets = 0; + + // A predefined target + const char *predef_target = NULL; + if (pleaf->predef != C2_L_PUNDEFINED) { + switch (pleaf->predef) { + case C2_L_PWINDOWTYPE: + predef_target = WINTYPES[w->window_type]; + break; + case C2_L_PNAME: predef_target = w->name; break; + case C2_L_PCLASSG: predef_target = w->class_general; break; + case C2_L_PCLASSI: predef_target = w->class_instance; break; + case C2_L_PROLE: predef_target = w->role; break; + default: assert(0); break; + } + ntargets = 1; + targets = &predef_target; + } + // An atom type property, convert it to string + else if (pleaf->type == C2_L_TATOM) { + int word_count = 1; + if (pleaf->index < 0) { + // Get length of property in 32-bit multiples + auto prop_info = x_get_prop_info(ps->c, wid, pleaf->tgtatom); + word_count = to_int_checked((prop_info.length + 4 - 1) / 4); + } + winprop_t prop = x_get_prop_with_offset( + ps->c, wid, pleaf->tgtatom, idx, word_count, + c2_get_atom_type(pleaf), pleaf->format); + + ntargets = (pleaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); + targets = targets_free = (const char **)ccalloc(2 * ntargets, char *); + targets_free_inner = targets + ntargets; + + for (size_t i = 0; i < ntargets; ++i) { + xcb_atom_t atom = (xcb_atom_t)winprop_get_int(prop, i); + if (atom) { + xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply( + ps->c, xcb_get_atom_name(ps->c, atom), NULL); + if (reply) { + targets[i] = targets_free_inner[i] = strndup( + xcb_get_atom_name_name(reply), + (size_t)xcb_get_atom_name_name_length(reply)); + free(reply); + } + } + } + free_winprop(&prop); + } + // Not an atom type, just fetch the string list + else { + char **strlst = NULL; + int nstr = 0; + if (wid_get_text_prop(ps, wid, pleaf->tgtatom, &strlst, &nstr)) { + if (pleaf->index < 0 && nstr > 0 && strlen(strlst[0]) > 0) { + ntargets = to_u32_checked(nstr); + targets = (const char **)strlst; + } else if (nstr > idx) { + ntargets = 1; + targets = (const char **)strlst + idx; + } + } + if (strlst) { + targets_free = (const char **)strlst; + } + } + + if (ntargets == 0) { + goto fail_str; + } + for (size_t i = 0; i < ntargets; ++i) { + if (!targets[i]) { + goto fail_str; + } + } + *perr = false; + + // Actual matching + bool res = false; + for (size_t i = 0; i < ntargets; ++i) { + const char *tgt = targets[i]; + switch (pleaf->op) { + case C2_L_OEXISTS: res = true; break; + case C2_L_OEQ: + switch (pleaf->match) { + case C2_L_MEXACT: + if (pleaf->match_ignorecase) { + res = !strcasecmp(tgt, pleaf->ptnstr); + } else { + res = !strcmp(tgt, pleaf->ptnstr); + } + break; + case C2_L_MCONTAINS: + if (pleaf->match_ignorecase) { + res = strcasestr(tgt, pleaf->ptnstr); + } else { + res = strstr(tgt, pleaf->ptnstr); + } + break; + case C2_L_MSTART: + if (pleaf->match_ignorecase) { + res = !strncasecmp(tgt, pleaf->ptnstr, + strlen(pleaf->ptnstr)); + } else { + res = !strncmp(tgt, pleaf->ptnstr, + strlen(pleaf->ptnstr)); + } + break; + case C2_L_MWILDCARD: { + int flags = 0; + if (pleaf->match_ignorecase) { + flags |= FNM_CASEFOLD; + } + res = !fnmatch(pleaf->ptnstr, tgt, flags); + } break; + case C2_L_MPCRE: +#ifdef CONFIG_REGEX_PCRE + assert(strlen(tgt) <= INT_MAX); + res = (pcre_exec(pleaf->regex_pcre, + pleaf->regex_pcre_extra, tgt, + (int)strlen(tgt), 0, 0, NULL, 0) >= 0); +#else + assert(0); +#endif + break; + } + break; + default: *perr = true; assert(0); + } + if (res) { + break; + } + } + *pres = res; + + fail_str: + // Free the string after usage, if necessary + if (targets_free_inner) { + for (size_t i = 0; i < ntargets; ++i) { + if (targets_free_inner[i]) { + free((void *)targets_free_inner[i]); + } + } + } + // Free property values after usage, if necessary + if (targets_free) { + free(targets_free); + } + } break; + default: assert(0); break; + } +} + +/** + * Match a window against a single window condition. + * + * @return true if matched, false otherwise. + */ +static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_ptr_t cond) { + bool result = false; + bool error = true; + + // Handle a branch + if (cond.isbranch) { + const c2_b_t *pb = cond.b; + + if (!pb) + return false; + + error = false; + + switch (pb->op) { + case C2_B_OAND: + result = (c2_match_once(ps, w, pb->opr1) && + c2_match_once(ps, w, pb->opr2)); + break; + case C2_B_OOR: + result = (c2_match_once(ps, w, pb->opr1) || + c2_match_once(ps, w, pb->opr2)); + break; + case C2_B_OXOR: + result = (c2_match_once(ps, w, pb->opr1) != + c2_match_once(ps, w, pb->opr2)); + break; + default: error = true; assert(0); + } + +#ifdef DEBUG_WINMATCH + log_trace("(%#010x): branch: result = %d, pattern = ", w->base.id, result); + c2_dump(cond); + putchar('\n'); +#endif + } + // Handle a leaf + else { + const c2_l_t *pleaf = cond.l; + + if (!pleaf) + return false; + + c2_match_once_leaf(ps, w, pleaf, &result, &error); + + // For EXISTS operator, no errors are fatal + if (C2_L_OEXISTS == pleaf->op && error) { + result = false; + error = false; + } + +#ifdef DEBUG_WINMATCH + log_trace("(%#010x): leaf: result = %d, error = %d, " + "client = %#010x, pattern = ", + w->base.id, result, error, w->client_win); + c2_dump(cond); + putchar('\n'); +#endif + } + + // Postprocess the result + if (error) + result = false; + + if (cond.isbranch ? cond.b->neg : cond.l->neg) + result = !result; + + return result; +} + +/** + * Match a window against a condition linked list. + * + * @param cache a place to cache the last matched condition + * @param pdata a place to return the data + * @return true if matched, false otherwise. + */ +bool c2_match(session_t *ps, const struct managed_win *w, const c2_lptr_t *condlst, + void **pdata) { + assert(ps->server_grabbed); + // Then go through the whole linked list + for (; condlst; condlst = condlst->next) { + if (c2_match_once(ps, w, condlst->ptr)) { + if (pdata) + *pdata = condlst->data; + return true; + } + } + + return false; +} diff --git a/src/c2.h b/src/c2.h new file mode 100644 index 0000000..d6b1d37 --- /dev/null +++ b/src/c2.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +/* + * Compton - a compositor for X11 + * + * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard + * + * Copyright (c) 2011-2013, Christopher Jeffrey + * See LICENSE-mit for more information. + * + */ + +#pragma once + +#include <stdbool.h> + +typedef struct _c2_lptr c2_lptr_t; +typedef struct session session_t; +struct managed_win; + +c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data); + +c2_lptr_t *c2_free_lptr(c2_lptr_t *lp); + +bool c2_match(session_t *ps, const struct managed_win *w, const c2_lptr_t *condlst, void **pdata); + +bool c2_list_postprocess(session_t *ps, c2_lptr_t *list); diff --git a/src/cache.c b/src/cache.c new file mode 100644 index 0000000..1ffb31c --- /dev/null +++ b/src/cache.c @@ -0,0 +1,95 @@ +#include <uthash.h> + +#include "compiler.h" +#include "utils.h" +#include "cache.h" + +struct cache_entry { + char *key; + void *value; + UT_hash_handle hh; +}; + +struct cache { + cache_getter_t getter; + cache_free_t free; + void *user_data; + struct cache_entry *entries; +}; + +void cache_set(struct cache *c, const char *key, void *data) { + struct cache_entry *e = NULL; + HASH_FIND_STR(c->entries, key, e); + CHECK(!e); + + e = ccalloc(1, struct cache_entry); + e->key = strdup(key); + e->value = data; + HASH_ADD_STR(c->entries, key, e); +} + +void *cache_get(struct cache *c, const char *key, int *err) { + struct cache_entry *e; + HASH_FIND_STR(c->entries, key, e); + if (e) { + return e->value; + } + + int tmperr; + if (!err) { + err = &tmperr; + } + + *err = 0; + e = ccalloc(1, struct cache_entry); + e->key = strdup(key); + e->value = c->getter(c->user_data, key, err); + if (*err) { + free(e->key); + free(e); + return NULL; + } + + HASH_ADD_STR(c->entries, key, e); + return e->value; +} + +static inline void _cache_invalidate(struct cache *c, struct cache_entry *e) { + if (c->free) { + c->free(c->user_data, e->value); + } + free(e->key); + HASH_DEL(c->entries, e); + free(e); +} + +void cache_invalidate(struct cache *c, const char *key) { + struct cache_entry *e; + HASH_FIND_STR(c->entries, key, e); + + if (e) { + _cache_invalidate(c, e); + } +} + +void cache_invalidate_all(struct cache *c) { + struct cache_entry *e, *tmpe; + HASH_ITER(hh, c->entries, e, tmpe) { + _cache_invalidate(c, e); + } +} + +void *cache_free(struct cache *c) { + void *ret = c->user_data; + cache_invalidate_all(c); + free(c); + return ret; +} + +struct cache *new_cache(void *ud, cache_getter_t getter, cache_free_t f) { + auto c = ccalloc(1, struct cache); + c->user_data = ud; + c->getter = getter; + c->free = f; + return c; +} diff --git a/src/cache.h b/src/cache.h new file mode 100644 index 0000000..3ca054f --- /dev/null +++ b/src/cache.h @@ -0,0 +1,32 @@ +#pragma once + +struct cache; + +typedef void *(*cache_getter_t)(void *user_data, const char *key, int *err); +typedef void (*cache_free_t)(void *user_data, void *data); + +/// Create a cache with `getter`, and a free function `f` which is used to free the cache +/// value when they are invalidated. +/// +/// `user_data` will be passed to `getter` and `f` when they are called. +struct cache *new_cache(void *user_data, cache_getter_t getter, cache_free_t f); + +/// Fetch a value from the cache. If the value doesn't present in the cache yet, the +/// getter will be called, and the returned value will be stored into the cache. +void *cache_get(struct cache *, const char *key, int *err); + +/// Invalidate a value in the cache. +void cache_invalidate(struct cache *, const char *key); + +/// Invalidate all values in the cache. +void cache_invalidate_all(struct cache *); + +/// Invalidate all values in the cache and free it. Returns the user data passed to +/// `new_cache` +void *cache_free(struct cache *); + +/// Insert a key-value pair into the cache. Only used for internal testing. Takes +/// ownership of `data` +/// +/// If `key` already exists in the cache, this function will abort the program. +void cache_set(struct cache *c, const char *key, void *data); diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..b7f2fe0 --- /dev/null +++ b/src/common.h @@ -0,0 +1,541 @@ +// 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) 2018, Yuxuan Shui <[email protected]> + * + * See LICENSE-mit for more information. + * + */ + +#pragma once + +// === Options === + +// Debug options, enable them using -D in CFLAGS +// #define DEBUG_REPAINT 1 +// #define DEBUG_EVENTS 1 +// #define DEBUG_RESTACK 1 +// #define DEBUG_WINMATCH 1 +// #define DEBUG_C2 1 +// #define DEBUG_GLX_DEBUG_CONTEXT 1 + +#define MAX_ALPHA (255) + +// === Includes === + +// For some special functions +#include <assert.h> +#include <stdbool.h> +#include <sys/time.h> +#include <time.h> + +#include <X11/Xlib.h> +#include <ev.h> +#include <pixman.h> +#include <xcb/xproto.h> +#include <xcb/render.h> +#include <xcb/sync.h> + +#include "uthash_extra.h" +#ifdef CONFIG_OPENGL +#include "backend/gl/glx.h" +#endif + +// X resource checker +#ifdef DEBUG_XRC +#include "xrescheck.h" +#endif + +// FIXME This list of includes should get shorter +#include "backend/backend.h" +#include "backend/driver.h" +#include "compiler.h" +#include "config.h" +#include "region.h" +#include "types.h" +#include "utils.h" +#include "list.h" +#include "render.h" +#include "win_defs.h" +#include "x.h" + +// === Constants ===0 + +#define NS_PER_SEC 1000000000L +#define US_PER_SEC 1000000L +#define MS_PER_SEC 1000 + +/// @brief Maximum OpenGL FBConfig depth. +#define OPENGL_MAX_DEPTH 32 + +/// @brief Maximum OpenGL buffer age. +#define CGLX_MAX_BUFFER_AGE 5 + +// Window flags + +// === Types === +typedef struct glx_fbconfig glx_fbconfig_t; +struct glx_session; +struct atom; +struct conv; + +typedef struct _ignore { + struct _ignore *next; + unsigned long sequence; +} ignore_t; + +#ifdef CONFIG_OPENGL +#ifdef DEBUG_GLX_DEBUG_CONTEXT +typedef GLXContext (*f_glXCreateContextAttribsARB)(Display *dpy, GLXFBConfig config, + GLXContext share_context, Bool direct, + const int *attrib_list); +typedef void (*GLDEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, + GLsizei length, const GLchar *message, GLvoid *userParam); +typedef void (*f_DebugMessageCallback)(GLDEBUGPROC, void *userParam); +#endif + +typedef struct glx_prog_main { + /// GLSL program. + GLuint prog; + /// Location of uniform "opacity" in window GLSL program. + GLint unifm_opacity; + /// Location of uniform "invert_color" in blur GLSL program. + GLint unifm_invert_color; + /// Location of uniform "tex" in window GLSL program. + GLint unifm_tex; + /// Location of uniform "time" in window GLSL program. + GLint unifm_time; +} glx_prog_main_t; + +#define GLX_PROG_MAIN_INIT \ + { \ + .prog = 0, .unifm_opacity = -1, .unifm_invert_color = -1, \ + .unifm_tex = -1, .unifm_time = -1 \ + } + +#else +struct glx_prog_main {}; +#endif + +#define PAINT_INIT \ + { .pixmap = XCB_NONE, .pict = XCB_NONE } + +/// Linked list type of atoms. +typedef struct _latom { + xcb_atom_t atom; + struct _latom *next; +} latom_t; + +/// Structure containing all necessary data for a session. +typedef struct session { + // === Event handlers === + /// ev_io for X connection + ev_io xiow; + /// Timeout for delayed unredirection. + ev_timer unredir_timer; + /// Timer for fading + ev_timer fade_timer; + /// Timer for animations + ev_timer animation_timer; + /// Timer for delayed drawing, right now only used by + /// swopti + ev_timer delayed_draw_timer; + /// Use an ev_idle callback for drawing + /// So we only start drawing when events are processed + ev_idle draw_idle; + /// Called everytime we have timeouts or new data on socket, + /// so we can be sure if xcb read from X socket at anytime during event + /// handling, we will not left any event unhandled in the queue + ev_prepare event_check; + /// Signal handler for SIGUSR1 + ev_signal usr1_signal; + /// Signal handler for SIGINT + ev_signal int_signal; + /// backend data + backend_t *backend_data; + /// backend blur context + void *backend_blur_context; + /// graphic drivers used + enum driver drivers; + /// file watch handle + void *file_watch_handle; + /// libev mainloop + struct ev_loop *loop; + + // === Display related === + /// Whether the X server is grabbed by us + bool server_grabbed; + /// Display in use. + Display *dpy; + /// Previous handler of X errors + XErrorHandler previous_xerror_handler; + /// Default screen. + int scr; + /// XCB connection. + xcb_connection_t *c; + /// Default visual. + xcb_visualid_t vis; + /// Default depth. + int depth; + /// Root window. + xcb_window_t root; + /// Height of root window. + int root_height; + /// Width of root window. + int root_width; + /// Current desktop number of root window + int root_desktop_num; + /// Desktop switch direction + int root_desktop_switch_direction; + // Damage of root window. + // Damage root_damage; + /// X Composite overlay window. Used if <code>--paint-on-overlay</code>. + xcb_window_t overlay; + /// The target window for debug mode + xcb_window_t debug_window; + /// Whether the root tile is filled by us. + bool root_tile_fill; + /// Picture of the root window background. + paint_t root_tile_paint; + /// The backend data the root pixmap bound to + void *root_image; + /// A region of the size of the screen. + region_t screen_reg; + /// Picture of root window. Destination of painting in no-DBE painting + /// mode. + xcb_render_picture_t root_picture; + /// A Picture acting as the painting target. + xcb_render_picture_t tgt_picture; + /// Temporary buffer to paint to before sending to display. + paint_t tgt_buffer; + /// Window ID of the window we register as a symbol. + xcb_window_t reg_win; +#ifdef CONFIG_OPENGL + /// Pointer to GLX data. + struct glx_session *psglx; + /// Custom GLX program used for painting window. + // XXX should be in struct glx_session + glx_prog_main_t glx_prog_win; + struct glx_fbconfig_info *argb_fbconfig; +#endif + /// Sync fence to sync draw operations + xcb_sync_fence_t sync_fence; + /// Whether we are rendering the first frame after screen is redirected + bool first_frame; + + // === Operation related === + /// Flags related to the root window + uint64_t root_flags; + /// Program options. + options_t o; + /// Whether we have hit unredirection timeout. + bool tmout_unredir_hit; + /// Whether we need to redraw the screen + bool redraw_needed; + + /// Cache a xfixes region so we don't need to allocate it everytime. + /// A workaround for yshui/picom#301 + xcb_xfixes_region_t damaged_region; + /// The region needs to painted on next paint. + region_t *damage; + /// The region damaged on the last paint. + region_t *damage_ring; + /// Number of damage regions we track + int ndamage; + /// Whether all windows are currently redirected. + bool redirected; + /// Pre-generated alpha pictures. + xcb_render_picture_t *alpha_picts; + /// Time of last fading. In milliseconds. + long fade_time; + /// Time of last window animation step. In milliseconds. + long animation_time; + /// Head pointer of the error ignore linked list. + ignore_t *ignore_head; + /// Pointer to the <code>next</code> member of tail element of the error + /// ignore linked list. + ignore_t **ignore_tail; + // Cached blur convolution kernels. + struct x_convolution_kernel **blur_kerns_cache; + /// If we should quit + bool quit:1; + // TODO(yshui) use separate flags for dfferent kinds of updates so we don't + // waste our time. + /// Whether there are pending updates, like window creation, etc. + bool pending_updates:1; + + // === Expose event related === + /// Pointer to an array of <code>XRectangle</code>-s of exposed region. + /// XXX why do we need this array? + rect_t *expose_rects; + /// Number of <code>XRectangle</code>-s in <code>expose_rects</code>. + int size_expose; + /// Index of the next free slot in <code>expose_rects</code>. + int n_expose; + + // === Window related === + /// A hash table of all windows. + struct win *windows; + /// Windows in their stacking order + struct list_node window_stack; + /// Pointer to <code>win</code> of current active window. Used by + /// EWMH <code>_NET_ACTIVE_WINDOW</code> focus detection. In theory, + /// it's more reliable to store the window ID directly here, just in + /// case the WM does something extraordinary, but caching the pointer + /// means another layer of complexity. + struct managed_win *active_win; + /// Window ID of leader window of currently active window. Used for + /// subsidiary window detection. + xcb_window_t active_leader; + + // === Shadow/dimming related === + /// 1x1 black Picture. + xcb_render_picture_t black_picture; + /// 1x1 Picture of the shadow color. + xcb_render_picture_t cshadow_picture; + /// 1x1 white Picture. + xcb_render_picture_t white_picture; + /// Gaussian map of shadow. + struct conv *gaussian_map; + // for shadow precomputation + /// A region in which shadow is not painted on. + region_t shadow_exclude_reg; + + // === Software-optimization-related === + /// Currently used refresh rate. + int refresh_rate; + /// Interval between refresh in nanoseconds. + long refresh_intv; + /// Nanosecond offset of the first painting. + long paint_tm_offset; + +#ifdef CONFIG_VSYNC_DRM + // === DRM VSync related === + /// File descriptor of DRI device file. Used for DRM VSync. + int drm_fd; +#endif + + // === X extension related === + /// Event base number for X Fixes extension. + int xfixes_event; + /// Error base number for X Fixes extension. + int xfixes_error; + /// Event base number for X Damage extension. + int damage_event; + /// Error base number for X Damage extension. + int damage_error; + /// Event base number for X Render extension. + int render_event; + /// Error base number for X Render extension. + int render_error; + /// Event base number for X Composite extension. + int composite_event; + /// Error base number for X Composite extension. + int composite_error; + /// Major opcode for X Composite extension. + int composite_opcode; + /// Whether X Shape extension exists. + bool shape_exists; + /// Event base number for X Shape extension. + int shape_event; + /// Error base number for X Shape extension. + int shape_error; + /// Whether X RandR extension exists. + bool randr_exists; + /// Event base number for X RandR extension. + int randr_event; + /// Error base number for X RandR extension. + int randr_error; + /// Whether X Present extension exists. + bool present_exists; + /// Whether X GLX extension exists. + bool glx_exists; + /// Event base number for X GLX extension. + int glx_event; + /// Error base number for X GLX extension. + int glx_error; + /// Whether X Xinerama extension exists. + bool xinerama_exists; + /// Xinerama screen regions. + region_t *xinerama_scr_regs; + /// Number of Xinerama screens. + int xinerama_nscrs; + /// Whether X Sync extension exists. + bool xsync_exists; + /// Event base number for X Sync extension. + int xsync_event; + /// Error base number for X Sync extension. + int xsync_error; + /// Whether X Render convolution filter exists. + bool xrfilter_convolution_exists; + + // === Atoms === + struct atom *atoms; + /// Array of atoms of all possible window types. + xcb_atom_t atoms_wintypes[NUM_WINTYPES]; + /// Linked list of additional atoms to track. + latom_t *track_atom_lst; + +#ifdef CONFIG_DBUS + // === DBus related === + void *dbus_data; +#endif + + int (*vsync_wait)(session_t *); +} session_t; + +/// Enumeration for window event hints. +typedef enum { WIN_EVMODE_UNKNOWN, WIN_EVMODE_FRAME, WIN_EVMODE_CLIENT } win_evmode_t; + +extern const char *const WINTYPES[NUM_WINTYPES]; +extern session_t *ps_g; + +void ev_xcb_error(session_t *ps, xcb_generic_error_t *err); + +// === Functions === + +/** + * Subtracting two struct timespec values. + * + * Taken from glibc manual. + * + * Subtract the `struct timespec' values X and Y, + * storing the result in RESULT. + * Return 1 if the difference is negative, otherwise 0. + */ +static inline int +timespec_subtract(struct timespec *result, struct timespec *x, struct timespec *y) { + /* Perform the carry for the later subtraction by updating y. */ + if (x->tv_nsec < y->tv_nsec) { + long nsec = (y->tv_nsec - x->tv_nsec) / NS_PER_SEC + 1; + y->tv_nsec -= NS_PER_SEC * nsec; + y->tv_sec += nsec; + } + + if (x->tv_nsec - y->tv_nsec > NS_PER_SEC) { + long nsec = (x->tv_nsec - y->tv_nsec) / NS_PER_SEC; + y->tv_nsec += NS_PER_SEC * nsec; + y->tv_sec -= nsec; + } + + /* Compute the time remaining to wait. + tv_nsec is certainly positive. */ + result->tv_sec = x->tv_sec - y->tv_sec; + result->tv_nsec = x->tv_nsec - y->tv_nsec; + + /* Return 1 if result is negative. */ + return x->tv_sec < y->tv_sec; +} + +/** + * Get current time in struct timeval. + */ +static inline struct timeval get_time_timeval(void) { + struct timeval tv = {0, 0}; + + gettimeofday(&tv, NULL); + + // Return a time of all 0 if the call fails + return tv; +} + +/** + * Get current time in struct timespec. + * + * Note its starting time is unspecified. + */ +static inline struct timespec get_time_timespec(void) { + struct timespec tm = {0, 0}; + + clock_gettime(CLOCK_MONOTONIC, &tm); + + // Return a time of all 0 if the call fails + return tm; +} + +/** + * Return the painting target window. + */ +static inline xcb_window_t get_tgt_window(session_t *ps) { + return ps->overlay != XCB_NONE ? ps->overlay : ps->root; +} + +/** + * Check if current backend uses GLX. + */ +static inline bool bkend_use_glx(session_t *ps) { + return BKEND_GLX == ps->o.backend || BKEND_XR_GLX_HYBRID == ps->o.backend; +} + +static void set_ignore(session_t *ps, unsigned long sequence) { + if (ps->o.show_all_xerrors) + return; + + auto i = cmalloc(ignore_t); + if (!i) + return; + + i->sequence = sequence; + i->next = 0; + *ps->ignore_tail = i; + ps->ignore_tail = &i->next; +} + +/** + * Ignore X errors caused by given X request. + */ +static inline void set_ignore_cookie(session_t *ps, xcb_void_cookie_t cookie) { + set_ignore(ps, cookie.sequence); +} + +/** + * Determine if a window has a specific property. + * + * @param ps current session + * @param w window to check + * @param atom atom of property to check + * @return true if it has the attribute, false otherwise + */ +static inline bool wid_has_prop(const session_t *ps, xcb_window_t w, xcb_atom_t atom) { + auto r = xcb_get_property_reply( + ps->c, xcb_get_property(ps->c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 0), NULL); + if (!r) { + return false; + } + + auto rtype = r->type; + free(r); + + if (rtype != XCB_NONE) { + return true; + } + return false; +} + +void force_repaint(session_t *ps); + +/** @name DBus handling + */ +///@{ +#ifdef CONFIG_DBUS +/** @name DBus hooks + */ +///@{ +void opts_set_no_fading_openclose(session_t *ps, bool newval); +//!@} +#endif + +/** + * Set a <code>bool</code> array of all wintypes to true. + */ +static inline void wintype_arr_enable(bool arr[]) { + wintype_t i; + + for (i = 0; i < NUM_WINTYPES; ++i) { + arr[i] = true; + } +} diff --git a/src/compiler.h b/src/compiler.h new file mode 100644 index 0000000..f146bd2 --- /dev/null +++ b/src/compiler.h @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> +#pragma once + +#ifdef HAS_STDC_PREDEF_H +#include <stdc-predef.h> +#endif + +// clang-format off +#define auto __auto_type +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) +#define likely_if(x) if (likely(x)) +#define unlikely_if(x) if (unlikely(x)) + +#ifndef __has_attribute +# if __GNUC__ >= 4 +# define __has_attribute(x) 1 +# else +# define __has_attribute(x) 0 +# endif +#endif + +#if __has_attribute(const) +# define attr_const __attribute__((const)) +#else +# define attr_const +#endif + +#if __has_attribute(format) +# define attr_printf(a, b) __attribute__((format(printf, a, b))) +#else +# define attr_printf(a, b) +#endif + +#if __has_attribute(pure) +# define attr_pure __attribute__((pure)) +#else +# define attr_pure +#endif + +#if __has_attribute(unused) +# define attr_unused __attribute__((unused)) +#else +# define attr_unused +#endif + +#if __has_attribute(warn_unused_result) +# define attr_warn_unused_result __attribute__((warn_unused_result)) +#else +# define attr_warn_unused_result +#endif +// An alias for conveninence +#define must_use attr_warn_unused_result + +#if __has_attribute(nonnull) +# define attr_nonnull(...) __attribute__((nonnull(__VA_ARGS__))) +# define attr_nonnull_all __attribute__((nonnull)) +#else +# define attr_nonnull(...) +# define attr_nonnull_all +#endif + +#if __has_attribute(returns_nonnull) +# define attr_ret_nonnull __attribute__((returns_nonnull)) +#else +# define attr_ret_nonnull +#endif + +#if __has_attribute(deprecated) +# define attr_deprecated __attribute__((deprecated)) +#else +# define attr_deprecated +#endif + +#if __has_attribute(malloc) +# define attr_malloc __attribute__((malloc)) +#else +# define attr_malloc +#endif + +#if __has_attribute(fallthrough) +# define fallthrough() __attribute__((fallthrough)) +#else +# define fallthrough() +#endif + +#if __STDC_VERSION__ >= 201112L +# define attr_noret _Noreturn +#else +# if __has_attribute(noreturn) +# define attr_noret __attribute__((noreturn)) +# else +# define attr_noret +# endif +#endif + +#if defined(__GNUC__) || defined(__clang__) +# define unreachable __builtin_unreachable() +#else +# define unreachable do {} while(0) +#endif + +#ifndef __has_include +# define __has_include(x) 0 +#endif + +#if !defined(__STDC_NO_THREADS__) && __has_include(<threads.h>) +# include <threads.h> +#elif __STDC_VERSION__ >= 201112L +# define thread_local _Thread_local +#elif defined(__GNUC__) || defined(__clang__) +# define thread_local __thread +#else +# define thread_local _Pragma("GCC error \"No thread local storage support\"") __error__ +#endif +// clang-format on + +typedef unsigned long ulong; +typedef unsigned int uint; + +static inline int attr_const popcntul(unsigned long a) { + return __builtin_popcountl(a); +} diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..d24bf64 --- /dev/null +++ b/src/config.c @@ -0,0 +1,650 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2011-2013, Christopher Jeffrey +// Copyright (c) 2013 Richard Grenville <[email protected]> + +#include <ctype.h> +#include <limits.h> +#include <math.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <xcb/render.h> // for xcb_render_fixed_t, XXX + +#include "c2.h" +#include "common.h" +#include "compiler.h" +#include "kernel.h" +#include "log.h" +#include "region.h" +#include "string_utils.h" +#include "types.h" +#include "utils.h" +#include "win.h" + +#include "config.h" + +/** + * Parse a long number. + */ +bool parse_long(const char *s, long *dest) { + const char *endptr = NULL; + long val = strtol(s, (char **)&endptr, 0); + if (!endptr || endptr == s) { + log_error("Invalid number: %s", s); + return false; + } + while (isspace((unsigned char)*endptr)) + ++endptr; + if (*endptr) { + log_error("Trailing characters: %s", s); + return false; + } + *dest = val; + return true; +} + +/** + * Parse an int number. + */ +bool parse_int(const char *s, int *dest) { + long val; + if (!parse_long(s, &val)) { + return false; + } + if (val > INT_MAX || val < INT_MIN) { + log_error("Number exceeded int limits: %ld", val); + return false; + } + *dest = (int)val; + return true; +} + +/** + * Parse a floating-point number in from a string, + * also strips the trailing space and comma after the number. + * + * @param[in] src string to parse + * @param[out] dest return the number parsed from the string + * @return pointer to the last character parsed + */ +const char *parse_readnum(const char *src, double *dest) { + const char *pc = NULL; + double val = strtod_simple(src, &pc); + if (!pc || pc == src) { + log_error("No number found: %s", src); + return src; + } + while (*pc && (isspace((unsigned char)*pc) || *pc == ',')) { + ++pc; + } + *dest = val; + return pc; +} + +enum blur_method parse_blur_method(const char *src) { + if (strcmp(src, "kernel") == 0) { + return BLUR_METHOD_KERNEL; + } else if (strcmp(src, "box") == 0) { + return BLUR_METHOD_BOX; + } else if (strcmp(src, "gaussian") == 0) { + return BLUR_METHOD_GAUSSIAN; + } else if (strcmp(src, "dual_kawase") == 0) { + return BLUR_METHOD_DUAL_KAWASE; + } else if (strcmp(src, "kawase") == 0) { + log_warn("Blur method 'kawase' has been renamed to 'dual_kawase'. " + "Interpreted as 'dual_kawase', but this will stop working " + "soon."); + return BLUR_METHOD_DUAL_KAWASE; + } else if (strcmp(src, "none") == 0) { + return BLUR_METHOD_NONE; + } + return BLUR_METHOD_INVALID; +} + +/** + * Parse a matrix. + * + * @param[in] src the blur kernel string + * @param[out] endptr return where the end of kernel is in the string + * @param[out] hasneg whether the kernel has negative values + */ +conv *parse_blur_kern(const char *src, const char **endptr, bool *hasneg) { + int width = 0, height = 0; + *hasneg = false; + + const char *pc = NULL; + + // Get matrix width and height + double val = 0.0; + if (src == (pc = parse_readnum(src, &val))) + goto err1; + src = pc; + width = (int)val; + if (src == (pc = parse_readnum(src, &val))) + goto err1; + src = pc; + height = (int)val; + + // Validate matrix width and height + if (width <= 0 || height <= 0) { + log_error("Blue kernel width/height can't be negative."); + goto err1; + } + if (!(width % 2 && height % 2)) { + log_error("Blur kernel width/height must be odd."); + goto err1; + } + if (width > 16 || height > 16) + log_warn("Blur kernel width/height too large, may slow down" + "rendering, and/or consume lots of memory"); + + // Allocate memory + conv *matrix = cvalloc(sizeof(conv) + (size_t)(width * height) * sizeof(double)); + + // Read elements + int skip = height / 2 * width + width / 2; + for (int i = 0; i < width * height; ++i) { + // Ignore the center element + if (i == skip) { + matrix->data[i] = 1; + continue; + } + if (src == (pc = parse_readnum(src, &val))) { + goto err2; + } + src = pc; + if (val < 0) { + *hasneg = true; + } + matrix->data[i] = val; + } + + // Detect trailing characters + for (; *pc && *pc != ';'; pc++) { + if (!isspace((unsigned char)*pc) && *pc != ',') { + // TODO(yshui) isspace is locale aware, be careful + log_error("Trailing characters in blur kernel string."); + goto err2; + } + } + + // Jump over spaces after ';' + if (*pc == ';') { + pc++; + while (*pc && isspace((unsigned char)*pc)) { + ++pc; + } + } + + // Require an end of string if endptr is not provided, otherwise + // copy end pointer to endptr + if (endptr) { + *endptr = pc; + } else if (*pc) { + log_error("Only one blur kernel expected."); + goto err2; + } + + // Fill in width and height + matrix->w = width; + matrix->h = height; + return matrix; + +err2: + free(matrix); +err1: + return NULL; +} + +/** + * Parse a list of convolution kernels. + * + * @param[in] src string to parse + * @param[out] hasneg whether any of the kernels have negative values + * @return the kernels + */ +struct conv **parse_blur_kern_lst(const char *src, bool *hasneg, int *count) { + // TODO(yshui) just return a predefined kernels, not parse predefined strings... + static const struct { + const char *name; + const char *kern_str; + } CONV_KERN_PREDEF[] = { + {"3x3box", "3,3,1,1,1,1,1,1,1,1,"}, + {"5x5box", "5,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,"}, + {"7x7box", "7,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1," + "1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,"}, + {"3x3gaussian", "3,3,0.243117,0.493069,0.243117,0.493069,0.493069,0.243117,0." + "493069,0.243117,"}, + {"5x5gaussian", "5,5,0.003493,0.029143,0.059106,0.029143,0.003493,0.029143,0." + "243117,0.493069,0.243117,0.029143,0.059106,0.493069,0." + "493069,0.059106,0.029143,0.243117,0.493069,0.243117,0." + "029143,0.003493,0.029143,0.059106,0.029143,0.003493,"}, + {"7x7gaussian", "7,7,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0." + "000003,0.000102,0.003493,0.029143,0.059106,0.029143,0." + "003493,0.000102,0.000849,0.029143,0.243117,0.493069,0." + "243117,0.029143,0.000849,0.001723,0.059106,0.493069,0." + "493069,0.059106,0.001723,0.000849,0.029143,0.243117,0." + "493069,0.243117,0.029143,0.000849,0.000102,0.003493,0." + "029143,0.059106,0.029143,0.003493,0.000102,0.000003,0." + "000102,0.000849,0.001723,0.000849,0.000102,0.000003,"}, + {"9x9gaussian", + "9,9,0.000000,0.000000,0.000001,0.000006,0.000012,0.000006,0.000001,0." + "000000,0.000000,0.000000,0.000003,0.000102,0.000849,0.001723,0.000849,0." + "000102,0.000003,0.000000,0.000001,0.000102,0.003493,0.029143,0.059106,0." + "029143,0.003493,0.000102,0.000001,0.000006,0.000849,0.029143,0.243117,0." + "493069,0.243117,0.029143,0.000849,0.000006,0.000012,0.001723,0.059106,0." + "493069,0.493069,0.059106,0.001723,0.000012,0.000006,0.000849,0.029143,0." + "243117,0.493069,0.243117,0.029143,0.000849,0.000006,0.000001,0.000102,0." + "003493,0.029143,0.059106,0.029143,0.003493,0.000102,0.000001,0.000000,0." + "000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0.000000,0." + "000000,0.000000,0.000001,0.000006,0.000012,0.000006,0.000001,0.000000,0." + "000000,"}, + {"11x11gaussian", + "11,11,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0." + "000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000001,0." + "000006,0.000012,0.000006,0.000001,0.000000,0.000000,0.000000,0.000000,0." + "000000,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0." + "000000,0.000000,0.000000,0.000001,0.000102,0.003493,0.029143,0.059106,0." + "029143,0.003493,0.000102,0.000001,0.000000,0.000000,0.000006,0.000849,0." + "029143,0.243117,0.493069,0.243117,0.029143,0.000849,0.000006,0.000000,0." + "000000,0.000012,0.001723,0.059106,0.493069,0.493069,0.059106,0.001723,0." + "000012,0.000000,0.000000,0.000006,0.000849,0.029143,0.243117,0.493069,0." + "243117,0.029143,0.000849,0.000006,0.000000,0.000000,0.000001,0.000102,0." + "003493,0.029143,0.059106,0.029143,0.003493,0.000102,0.000001,0.000000,0." + "000000,0.000000,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0." + "000003,0.000000,0.000000,0.000000,0.000000,0.000000,0.000001,0.000006,0." + "000012,0.000006,0.000001,0.000000,0.000000,0.000000,0.000000,0.000000,0." + "000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0." + "000000,"}, + }; + + *count = 0; + *hasneg = false; + for (unsigned int i = 0; + i < sizeof(CONV_KERN_PREDEF) / sizeof(CONV_KERN_PREDEF[0]); ++i) { + if (!strcmp(CONV_KERN_PREDEF[i].name, src)) + return parse_blur_kern_lst(CONV_KERN_PREDEF[i].kern_str, hasneg, count); + } + + int nkernels = 1; + for (int i = 0; src[i]; i++) { + if (src[i] == ';') { + nkernels++; + } + } + + struct conv **ret = ccalloc(nkernels, struct conv *); + + int i = 0; + const char *pc = src; + + // Continue parsing until the end of source string + i = 0; + while (pc && *pc) { + bool tmp_hasneg; + assert(i < nkernels); + ret[i] = parse_blur_kern(pc, &pc, &tmp_hasneg); + if (!ret[i]) { + for (int j = 0; j < i; j++) { + free(ret[j]); + } + free(ret); + return NULL; + } + i++; + *hasneg |= tmp_hasneg; + } + + if (i > 1) { + log_warn("You are seeing this message because you are using " + "multipass blur. Please report an issue to us so we know " + "multipass blur is actually been used. Otherwise it might be " + "removed in future releases"); + } + + *count = i; + + return ret; +} + +/** + * Parse a X geometry. + * + * ps->root_width and ps->root_height must be valid + */ +bool parse_geometry(session_t *ps, const char *src, region_t *dest) { + pixman_region32_clear(dest); + if (!src) { + return true; + } + if (!ps->root_width || !ps->root_height) { + return true; + } + + long x = 0, y = 0; + long width = ps->root_width, height = ps->root_height; + long val = 0L; + char *endptr = NULL; + + src = skip_space(src); + if (!*src) { + goto parse_geometry_end; + } + + // Parse width + // Must be base 10, because "0x0..." may appear + if (*src != '+' && *src != '-') { + val = strtol(src, &endptr, 10); + assert(endptr); + if (src != endptr) { + if (val < 0) { + log_error("Invalid width: %s", src); + return false; + } + width = val; + src = endptr; + } + src = skip_space(src); + } + + // Parse height + if (*src == 'x') { + ++src; + val = strtol(src, &endptr, 10); + assert(endptr); + if (src != endptr) { + if (val < 0) { + log_error("Invalid height: %s", src); + return false; + } + height = val; + src = endptr; + } + src = skip_space(src); + } + + // Parse x + if (*src == '+' || *src == '-') { + val = strtol(src, &endptr, 10); + if (endptr && src != endptr) { + x = val; + if (*src == '-') { + x += ps->root_width - width; + } + src = endptr; + } + src = skip_space(src); + } + + // Parse y + if (*src == '+' || *src == '-') { + val = strtol(src, &endptr, 10); + if (endptr && src != endptr) { + y = val; + if (*src == '-') { + y += ps->root_height - height; + } + src = endptr; + } + src = skip_space(src); + } + + if (*src) { + log_error("Trailing characters: %s", src); + return false; + } + +parse_geometry_end: + if (x < INT_MIN || x > INT_MAX || y < INT_MIN || y > INT_MAX) { + log_error("Geometry coordinates exceeded limits: %s", src); + return false; + } + if (width > UINT_MAX || height > UINT_MAX) { + // less than 0 is checked for earlier + log_error("Geometry size exceeded limits: %s", src); + return false; + } + pixman_region32_union_rect(dest, dest, (int)x, (int)y, (uint)width, (uint)height); + return true; +} + +/** + * Parse a list of opacity rules. + */ +bool parse_rule_opacity(c2_lptr_t **res, const char *src) { + // Find opacity value + char *endptr = NULL; + long val = strtol(src, &endptr, 0); + if (!endptr || endptr == src) { + log_error("No opacity specified: %s", src); + return false; + } + if (val > 100 || val < 0) { + log_error("Opacity %ld invalid: %s", val, src); + return false; + } + + // Skip over spaces + while (*endptr && isspace((unsigned char)*endptr)) + ++endptr; + if (':' != *endptr) { + log_error("Opacity terminator not found: %s", src); + return false; + } + ++endptr; + + // Parse pattern + // I hope 1-100 is acceptable for (void *) + return c2_parse(res, endptr, (void *)val); +} + +/** + * Add a pattern to a condition linked list. + */ +bool condlst_add(c2_lptr_t **pcondlst, const char *pattern) { + if (!pattern) + return false; + + if (!c2_parse(pcondlst, pattern, NULL)) + exit(1); + + return true; +} + +void set_default_winopts(options_t *opt, win_option_mask_t *mask, bool shadow_enable, + bool fading_enable, bool blur_enable) { + // Apply default wintype options. + if (!mask[WINTYPE_DESKTOP].shadow) { + // Desktop windows are always drawn without shadow by default. + mask[WINTYPE_DESKTOP].shadow = true; + opt->wintype_option[WINTYPE_DESKTOP].shadow = false; + } + + // Focused/unfocused state only apply to a few window types, all other windows + // are always considered focused. + const wintype_t nofocus_type[] = {WINTYPE_UNKNOWN, WINTYPE_NORMAL, WINTYPE_UTILITY}; + for (unsigned long i = 0; i < ARR_SIZE(nofocus_type); i++) { + if (!mask[nofocus_type[i]].focus) { + mask[nofocus_type[i]].focus = true; + opt->wintype_option[nofocus_type[i]].focus = false; + } + } + for (unsigned long i = 0; i < NUM_WINTYPES; i++) { + if (!mask[i].shadow) { + mask[i].shadow = true; + opt->wintype_option[i].shadow = shadow_enable; + } + if (!mask[i].fade) { + mask[i].fade = true; + opt->wintype_option[i].fade = fading_enable; + } + if (!mask[i].focus) { + mask[i].focus = true; + opt->wintype_option[i].focus = true; + } + if (!mask[i].blur_background) { + mask[i].blur_background = true; + opt->wintype_option[i].blur_background = blur_enable; + } + if (!mask[i].full_shadow) { + mask[i].full_shadow = true; + opt->wintype_option[i].full_shadow = false; + } + if (!mask[i].redir_ignore) { + mask[i].redir_ignore = true; + opt->wintype_option[i].redir_ignore = false; + } + if (!mask[i].opacity) { + mask[i].opacity = true; + // Opacity is not set to a concrete number here because the + // opacity logic is complicated, and needs an "unset" state + opt->wintype_option[i].opacity = NAN; + } + if (!mask[i].animation) { + mask[i].animation = OPEN_WINDOW_ANIMATION_INVALID; + opt->wintype_option[i].animation = OPEN_WINDOW_ANIMATION_INVALID; + } + if (!mask[i].animation_unmap) { + mask[i].animation_unmap = OPEN_WINDOW_ANIMATION_INVALID; + opt->wintype_option[i].animation_unmap = OPEN_WINDOW_ANIMATION_INVALID; + } + if (!mask[i].animation_workspace_in) { + mask[i].animation_workspace_in = OPEN_WINDOW_ANIMATION_INVALID; + opt->wintype_option[i].animation_workspace_in = OPEN_WINDOW_ANIMATION_INVALID; + } + if (!mask[i].animation_workspace_out) { + mask[i].animation_workspace_out = OPEN_WINDOW_ANIMATION_INVALID; + opt->wintype_option[i].animation_workspace_out = OPEN_WINDOW_ANIMATION_INVALID; + } + if (!mask[i].clip_shadow_above) { + mask[i].clip_shadow_above = true; + opt->wintype_option[i].clip_shadow_above = false; + } + } +} + +enum open_window_animation parse_open_window_animation(const char *src) { + if (strcmp(src, "none") == 0) { + return OPEN_WINDOW_ANIMATION_NONE; + }else if (strcmp(src, "auto") == 0) { + return OPEN_WINDOW_ANIMATION_AUTO; + } else if (strcmp(src, "fly-in") == 0) { + return OPEN_WINDOW_ANIMATION_FLYIN; + } else if (strcmp(src, "zoom") == 0) { + return OPEN_WINDOW_ANIMATION_ZOOM; + } else if (strcmp(src, "slide-up") == 0) { + return OPEN_WINDOW_ANIMATION_SLIDE_UP; + } else if (strcmp(src, "slide-down") == 0) { + return OPEN_WINDOW_ANIMATION_SLIDE_DOWN; + } else if (strcmp(src, "slide-left") == 0) { + return OPEN_WINDOW_ANIMATION_SLIDE_LEFT; + } else if (strcmp(src, "slide-right") == 0) { + return OPEN_WINDOW_ANIMATION_SLIDE_RIGHT; + } + return OPEN_WINDOW_ANIMATION_INVALID; +} + +char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable, + bool *fading_enable, bool *hasneg, win_option_mask_t *winopt_mask) { + // clang-format off + *opt = (struct options){ + .backend = BKEND_XRENDER, + .glx_no_stencil = false, + .mark_wmwin_focused = false, + .mark_ovredir_focused = false, + .detect_rounded_corners = false, + .resize_damage = 0, + .unredir_if_possible = false, + .unredir_if_possible_blacklist = NULL, + .unredir_if_possible_delay = 0, + .redirected_force = UNSET, + .stoppaint_force = UNSET, + .dbus = false, + .benchmark = 0, + .benchmark_wid = XCB_NONE, + .logpath = NULL, + + .refresh_rate = 0, + .sw_opti = false, + .use_damage = true, + + .shadow_red = 0.0, + .shadow_green = 0.0, + .shadow_blue = 0.0, + .shadow_radius = 18, + .shadow_offset_x = -15, + .shadow_offset_y = -15, + .shadow_opacity = .75, + .shadow_blacklist = NULL, + .shadow_ignore_shaped = false, + .xinerama_shadow_crop = false, + .shadow_clip_list = NULL, + + .corner_radius = 0, + + .fade_in_step = 0.028, + .fade_out_step = 0.03, + .fade_delta = 10, + .no_fading_openclose = false, + .no_fading_destroyed_argb = false, + .fade_blacklist = NULL, + + .animations = false, + .animation_for_open_window = OPEN_WINDOW_ANIMATION_NONE, + .animation_for_transient_window = OPEN_WINDOW_ANIMATION_NONE, + .animation_for_unmap_window = OPEN_WINDOW_ANIMATION_AUTO, + .animation_for_workspace_switch_in = OPEN_WINDOW_ANIMATION_AUTO, + .animation_for_workspace_switch_out = OPEN_WINDOW_ANIMATION_AUTO, + .animation_stiffness = 200.0, + .animation_window_mass = 1.0, + .animation_dampening = 25, + .animation_delta = 10, + .animation_force_steps = false, + .animation_clamping = true, + + .inactive_opacity = 1.0, + .inactive_opacity_override = false, + .active_opacity = 1.0, + .frame_opacity = 1.0, + .detect_client_opacity = false, + + .blur_method = BLUR_METHOD_NONE, + .blur_radius = 3, + .blur_deviation = 0.84089642, + .blur_strength = 5, + .blur_background_frame = false, + .blur_background_fixed = false, + .blur_background_blacklist = NULL, + .blur_kerns = NULL, + .blur_kernel_count = 0, + .inactive_dim = 0.0, + .inactive_dim_fixed = false, + .invert_color_list = NULL, + .opacity_rules = NULL, + .max_brightness = 1.0, + + .use_ewmh_active_win = false, + .focus_blacklist = NULL, + .detect_transient = false, + .detect_client_leader = false, + .no_ewmh_fullscreen = false, + + .track_leader = false, + + .rounded_corners_blacklist = NULL + }; + // clang-format on + + char *ret = NULL; +#ifdef CONFIG_LIBCONFIG + ret = parse_config_libconfig(opt, config_file, shadow_enable, fading_enable, + hasneg, winopt_mask); +#else + (void)config_file; + (void)shadow_enable; + (void)fading_enable; + (void)hasneg; + (void)winopt_mask; +#endif + return ret; +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..a22d222 --- /dev/null +++ b/src/config.h @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2011-2013, Christopher Jeffrey +// Copyright (c) 2013 Richard Grenville <[email protected]> +// Copyright (c) 2018 Yuxuan Shui <[email protected]> +#pragma once + +/// Common functions and definitions for configuration parsing +/// Used for command line arguments and config files + +#include <ctype.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <xcb/render.h> // for xcb_render_fixed_t, XXX +#include <xcb/xcb.h> +#include <xcb/xfixes.h> + +#ifdef CONFIG_LIBCONFIG +#include <libconfig.h> +#endif + +#include "compiler.h" +#include "kernel.h" +#include "log.h" +#include "region.h" +#include "types.h" +#include "win_defs.h" + +typedef struct session session_t; + +/// @brief Possible backends +enum backend { + BKEND_XRENDER, + BKEND_GLX, + BKEND_XR_GLX_HYBRID, + BKEND_DUMMY, + NUM_BKEND, +}; + +enum open_window_animation { + OPEN_WINDOW_ANIMATION_NONE = 0, + OPEN_WINDOW_ANIMATION_AUTO, + OPEN_WINDOW_ANIMATION_FLYIN, + OPEN_WINDOW_ANIMATION_ZOOM, + OPEN_WINDOW_ANIMATION_SLIDE_UP, + OPEN_WINDOW_ANIMATION_SLIDE_DOWN, + OPEN_WINDOW_ANIMATION_SLIDE_LEFT, + OPEN_WINDOW_ANIMATION_SLIDE_RIGHT, + OPEN_WINDOW_ANIMATION_SLIDE_IN, + OPEN_WINDOW_ANIMATION_SLIDE_OUT, + OPEN_WINDOW_ANIMATION_INVALID, +}; + +typedef struct win_option_mask { + bool shadow : 1; + bool fade : 1; + bool focus : 1; + bool blur_background : 1; + bool full_shadow : 1; + bool redir_ignore : 1; + bool opacity : 1; + bool clip_shadow_above : 1; + enum open_window_animation animation; + enum open_window_animation animation_unmap; + enum open_window_animation animation_workspace_in; + enum open_window_animation animation_workspace_out; +} win_option_mask_t; + +typedef struct win_option { + bool shadow; + bool fade; + bool focus; + bool blur_background; + bool full_shadow; + bool redir_ignore; + double opacity; + bool clip_shadow_above; + enum open_window_animation animation; + enum open_window_animation animation_unmap; + enum open_window_animation animation_workspace_in; + enum open_window_animation animation_workspace_out; +} win_option_t; + +enum blur_method { + BLUR_METHOD_NONE = 0, + BLUR_METHOD_KERNEL, + BLUR_METHOD_BOX, + BLUR_METHOD_GAUSSIAN, + BLUR_METHOD_DUAL_KAWASE, + BLUR_METHOD_INVALID, +}; + +typedef struct _c2_lptr c2_lptr_t; + +/// Structure representing all options. +typedef struct options { + // === Debugging === + bool monitor_repaint; + bool print_diagnostics; + /// Render to a separate window instead of taking over the screen + bool debug_mode; + // === General === + /// Use the experimental new backends? + bool experimental_backends; + /// Path to write PID to. + char *write_pid_path; + /// The backend in use. + enum backend backend; + /// Whether to sync X drawing with X Sync fence to avoid certain delay + /// issues with GLX backend. + bool xrender_sync_fence; + /// Whether to avoid using stencil buffer under GLX backend. Might be + /// unsafe. + bool glx_no_stencil; + /// Whether to avoid rebinding pixmap on window damage. + bool glx_no_rebind_pixmap; + /// Custom fragment shader for painting windows, as a string. + char *glx_fshader_win_str; + /// Whether to detect rounded corners. + bool detect_rounded_corners; + /// Force painting of window content with blending. + bool force_win_blend; + /// Resize damage for a specific number of pixels. + int resize_damage; + /// Whether to unredirect all windows if a full-screen opaque window + /// is detected. + bool unredir_if_possible; + /// List of conditions of windows to ignore as a full-screen window + /// when determining if a window could be unredirected. + c2_lptr_t *unredir_if_possible_blacklist; + /// Delay before unredirecting screen, in milliseconds. + long unredir_if_possible_delay; + /// Forced redirection setting through D-Bus. + switch_t redirected_force; + /// Whether to stop painting. Controlled through D-Bus. + switch_t stoppaint_force; + /// Whether to enable D-Bus support. + bool dbus; + /// Path to log file. + char *logpath; + /// Number of cycles to paint in benchmark mode. 0 for disabled. + int benchmark; + /// Window to constantly repaint in benchmark mode. 0 for full-screen. + xcb_window_t benchmark_wid; + /// A list of conditions of windows not to paint. + c2_lptr_t *paint_blacklist; + /// Whether to show all X errors. + bool show_all_xerrors; + /// Whether to avoid acquiring X Selection. + bool no_x_selection; + /// Window type option override. + win_option_t wintype_option[NUM_WINTYPES]; + + // === VSync & software optimization === + /// User-specified refresh rate. + int refresh_rate; + /// Whether to enable refresh-rate-based software optimization. + bool sw_opti; + /// VSync method to use; + bool vsync; + /// Whether to use glFinish() instead of glFlush() for (possibly) better + /// VSync yet probably higher CPU usage. + bool vsync_use_glfinish; + /// Whether use damage information to help limit the area to paint + bool use_damage; + + // === Shadow === + /// Red, green and blue tone of the shadow. + double shadow_red, shadow_green, shadow_blue; + int shadow_radius; + int shadow_offset_x, shadow_offset_y; + double shadow_opacity; + /// argument string to shadow-exclude-reg option + char *shadow_exclude_reg_str; + /// Shadow blacklist. A linked list of conditions. + c2_lptr_t *shadow_blacklist; + /// Whether bounding-shaped window should be ignored. + bool shadow_ignore_shaped; + /// Whether to crop shadow to the very Xinerama screen. + bool xinerama_shadow_crop; + /// Don't draw shadow over these windows. A linked list of conditions. + c2_lptr_t *shadow_clip_list; + + // === Fading === + /// How much to fade in in a single fading step. + double fade_in_step; + /// How much to fade out in a single fading step. + double fade_out_step; + /// Fading time delta. In milliseconds. + int fade_delta; + /// Whether to disable fading on window open/close. + bool no_fading_openclose; + /// Whether to disable fading on ARGB managed destroyed windows. + bool no_fading_destroyed_argb; + /// Fading blacklist. A linked list of conditions. + c2_lptr_t *fade_blacklist; + + // === Animations === + /// Whether to do window animations + bool animations; + /// Which animation to run when opening a window + enum open_window_animation animation_for_open_window; + /// Which animation to run when opening a transient window + enum open_window_animation animation_for_transient_window; + /// Which animation to run when unmapping (e.g. minimizing) a window + enum open_window_animation animation_for_unmap_window; + /// Which animation to run when switching workspace + /// IMPORTANT: will only work if window manager updates + /// _NET_CURRENT_DESKTOP before doing the hide/show of windows + enum open_window_animation animation_for_workspace_switch_in; + enum open_window_animation animation_for_workspace_switch_out; + /// Spring stiffness for animation + double animation_stiffness; + /// Window mass for animation + double animation_window_mass; + /// Animation dampening + double animation_dampening; + /// Animation delta. In milliseconds. + double animation_delta; + /// Whether to force animations to not miss a beat + bool animation_force_steps; + /// Whether to clamp animations + bool animation_clamping; + /// TODO: window animation blacklist + /// TODO: open/close animations + + // === Opacity === + /// Default opacity for inactive windows. + /// 32-bit integer with the format of _NET_WM_OPACITY. + double inactive_opacity; + /// Default opacity for inactive windows. + double active_opacity; + /// Whether inactive_opacity overrides the opacity set by window + /// attributes. + bool inactive_opacity_override; + /// Frame opacity. Relative to window opacity, also affects shadow + /// opacity. + double frame_opacity; + /// Whether to detect _NET_WM_OPACITY on client windows. Used on window + /// managers that don't pass _NET_WM_OPACITY to frame windows. + bool detect_client_opacity; + + // === Other window processing === + /// Blur method for background of semi-transparent windows + enum blur_method blur_method; + // Size of the blur kernel + int blur_radius; + // Standard deviation for the gaussian blur + double blur_deviation; + // Strength of the dual_kawase blur + int blur_strength; + /// Whether to blur background when the window frame is not opaque. + /// Implies blur_background. + bool blur_background_frame; + /// Whether to use fixed blur strength instead of adjusting according + /// to window opacity. + bool blur_background_fixed; + /// Background blur blacklist. A linked list of conditions. + c2_lptr_t *blur_background_blacklist; + /// Blur convolution kernel. + struct conv **blur_kerns; + /// Number of convolution kernels + int blur_kernel_count; + /// How much to dim an inactive window. 0.0 - 1.0, 0 to disable. + double inactive_dim; + /// Whether to use fixed inactive dim opacity, instead of deciding + /// based on window opacity. + bool inactive_dim_fixed; + /// Conditions of windows to have inverted colors. + c2_lptr_t *invert_color_list; + /// Rules to change window opacity. + c2_lptr_t *opacity_rules; + /// Limit window brightness + double max_brightness; + // Radius of rounded window corners + int corner_radius; + /// Rounded corners blacklist. A linked list of conditions. + c2_lptr_t *rounded_corners_blacklist; + + // === Focus related === + /// Whether to try to detect WM windows and mark them as focused. + bool mark_wmwin_focused; + /// Whether to mark override-redirect windows as focused. + bool mark_ovredir_focused; + /// Whether to use EWMH _NET_ACTIVE_WINDOW to find active window. + bool use_ewmh_active_win; + /// A list of windows always to be considered focused. + c2_lptr_t *focus_blacklist; + /// Whether to do window grouping with <code>WM_TRANSIENT_FOR</code>. + bool detect_transient; + /// Whether to do window grouping with <code>WM_CLIENT_LEADER</code>. + bool detect_client_leader; + + // === Calculated === + /// Whether we need to track window leaders. + bool track_leader; + + // Don't use EWMH to detect fullscreen applications + bool no_ewmh_fullscreen; + + // Make transparent windows clip other windows, instead of blending on top of + // them + bool transparent_clipping; +} options_t; + +extern const char *const BACKEND_STRS[NUM_BKEND + 1]; + +bool must_use parse_long(const char *, long *); +bool must_use parse_int(const char *, int *); +struct conv **must_use parse_blur_kern_lst(const char *, bool *hasneg, int *count); +bool must_use parse_geometry(session_t *, const char *, region_t *); +bool must_use parse_rule_opacity(c2_lptr_t **, const char *); +enum blur_method must_use parse_blur_method(const char *src); +enum open_window_animation must_use parse_open_window_animation(const char *src); + +/** + * Add a pattern to a condition linked list. + */ +bool condlst_add(c2_lptr_t **, const char *); + +#ifdef CONFIG_LIBCONFIG +/// Parse a configuration file +/// Returns the actually config_file name used, allocated on heap +/// Outputs: +/// shadow_enable = whether shaodw is enabled globally +/// fading_enable = whether fading is enabled globally +/// win_option_mask = whether option overrides for specific window type is set for given +/// options +/// hasneg = whether the convolution kernel has negative values +char * +parse_config_libconfig(options_t *, const char *config_file, bool *shadow_enable, + bool *fading_enable, bool *hasneg, win_option_mask_t *winopt_mask); +#endif + +void set_default_winopts(options_t *, win_option_mask_t *, bool shadow_enable, + bool fading_enable, bool blur_enable); +/// Parse a configuration file is that is enabled, also initialize the winopt_mask with +/// default values +/// Outputs and returns: +/// same as parse_config_libconfig +char *parse_config(options_t *, const char *config_file, bool *shadow_enable, + bool *fading_enable, bool *hasneg, win_option_mask_t *winopt_mask); + +/** + * Parse a backend option argument. + */ +static inline attr_pure enum backend parse_backend(const char *str) { + for (enum backend i = 0; BACKEND_STRS[i]; ++i) { + if (!strcasecmp(str, BACKEND_STRS[i])) { + return i; + } + } + // Keep compatibility with an old revision containing a spelling mistake... + if (!strcasecmp(str, "xr_glx_hybird")) { + log_warn("backend xr_glx_hybird should be xr_glx_hybrid, the misspelt " + "version will be removed soon."); + return BKEND_XR_GLX_HYBRID; + } + // cju wants to use dashes + if (!strcasecmp(str, "xr-glx-hybrid")) { + log_warn("backend xr-glx-hybrid should be xr_glx_hybrid, the alternative " + "version will be removed soon."); + return BKEND_XR_GLX_HYBRID; + } + log_error("Invalid backend argument: %s", str); + return NUM_BKEND; +} + +/** + * Parse a VSync option argument. + */ +static inline bool parse_vsync(const char *str) { + if (strcmp(str, "no") == 0 || strcmp(str, "none") == 0 || + strcmp(str, "false") == 0 || strcmp(str, "nah") == 0) { + return false; + } + return true; +} + +// vim: set noet sw=8 ts=8 : diff --git a/src/config_libconfig.c b/src/config_libconfig.c new file mode 100644 index 0000000..7d266cd --- /dev/null +++ b/src/config_libconfig.c @@ -0,0 +1,784 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2012-2014 Richard Grenville <[email protected]> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <libconfig.h> +#include <libgen.h> + +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "err.h" +#include "log.h" +#include "options.h" +#include "string_utils.h" +#include "utils.h" +#include "win.h" + +#pragma GCC diagnostic error "-Wunused-parameter" + +/** + * Wrapper of libconfig's <code>config_lookup_int</code>. + * + * So it takes a pointer to bool. + */ +static inline int lcfg_lookup_bool(const config_t *config, const char *path, bool *value) { + int ival; + + int ret = config_lookup_bool(config, path, &ival); + if (ret) + *value = ival; + + return ret; +} + +const char *xdg_config_home(void) { + char *xdgh = getenv("XDG_CONFIG_HOME"); + char *home = getenv("HOME"); + const char *default_dir = "/.config"; + + if (!xdgh) { + if (!home) { + return NULL; + } + + xdgh = cvalloc(strlen(home) + strlen(default_dir) + 1); + + strcpy(xdgh, home); + strcat(xdgh, default_dir); + } else { + xdgh = strdup(xdgh); + } + + return xdgh; +} + +char **xdg_config_dirs(void) { + char *xdgd = getenv("XDG_CONFIG_DIRS"); + size_t count = 0; + + if (!xdgd) { + xdgd = "/etc/xdg"; + } + + for (int i = 0; xdgd[i]; i++) { + if (xdgd[i] == ':') { + count++; + } + } + + // Store the string and the result pointers together so they can be + // freed together + char **dir_list = cvalloc(sizeof(char *) * (count + 2) + strlen(xdgd) + 1); + auto dirs = strcpy((char *)dir_list + sizeof(char *) * (count + 2), xdgd); + auto path = dirs; + + for (size_t i = 0; i < count; i++) { + dir_list[i] = path; + path = strchr(path, ':'); + *path = '\0'; + path++; + } + dir_list[count] = path; + + size_t fill = 0; + for (size_t i = 0; i <= count; i++) { + if (dir_list[i][0] == '/') { + dir_list[fill] = dir_list[i]; + fill++; + } + } + + dir_list[fill] = NULL; + + return dir_list; +} + +TEST_CASE(xdg_config_dirs) { + auto old_var = getenv("XDG_CONFIG_DIRS"); + if (old_var) { + old_var = strdup(old_var); + } + unsetenv("XDG_CONFIG_DIRS"); + + auto result = xdg_config_dirs(); + TEST_STREQUAL(result[0], "/etc/xdg"); + TEST_EQUAL(result[1], NULL); + free(result); + + setenv("XDG_CONFIG_DIRS", ".:.:/etc/xdg:.:/:", 1); + result = xdg_config_dirs(); + TEST_STREQUAL(result[0], "/etc/xdg"); + TEST_STREQUAL(result[1], "/"); + TEST_EQUAL(result[2], NULL); + free(result); + + setenv("XDG_CONFIG_DIRS", ":", 1); + result = xdg_config_dirs(); + TEST_EQUAL(result[0], NULL); + free(result); + + if (old_var) { + setenv("XDG_CONFIG_DIRS", old_var, 1); + free(old_var); + } +} + +/// Search for config file under a base directory +FILE *open_config_file_at(const char *base, char **out_path) { + static const char *config_paths[] = {"/picom.conf", "/picom/picom.conf", + "/compton.conf", "/compton/compton.conf"}; + for (size_t i = 0; i < ARR_SIZE(config_paths); i++) { + char *path = mstrjoin(base, config_paths[i]); + FILE *ret = fopen(path, "r"); + if (ret && out_path) { + *out_path = path; + } else { + free(path); + } + if (ret) { + if (strstr(config_paths[i], "compton")) { + log_warn("This compositor has been renamed to \"picom\", " + "the old config file paths is deprecated. " + "Please replace the \"compton\"s in the path " + "with \"picom\""); + } + return ret; + } + } + return NULL; +} + +/** + * Get a file stream of the configuration file to read. + * + * Follows the XDG specification to search for the configuration file. + */ +FILE *open_config_file(const char *cpath, char **ppath) { + static const char config_filename_legacy[] = "/.compton.conf"; + + if (cpath) { + FILE *ret = fopen(cpath, "r"); + if (ret && ppath) + *ppath = strdup(cpath); + return ret; + } + + // First search for config file in user config directory + auto config_home = xdg_config_home(); + auto ret = open_config_file_at(config_home, ppath); + free((void *)config_home); + if (ret) { + return ret; + } + + // Fall back to legacy config file in user home directory + const char *home = getenv("HOME"); + if (home && strlen(home)) { + auto path = mstrjoin(home, config_filename_legacy); + ret = fopen(path, "r"); + if (ret && ppath) { + *ppath = path; + } else { + free(path); + } + if (ret) { + return ret; + } + } + + // Fall back to config file in system config directory + auto config_dirs = xdg_config_dirs(); + for (int i = 0; config_dirs[i]; i++) { + ret = open_config_file_at(config_dirs[i], ppath); + if (ret) { + free(config_dirs); + return ret; + } + } + free(config_dirs); + + return NULL; +} + +/** + * Parse a condition list in configuration file. + */ +void parse_cfg_condlst(const config_t *pcfg, c2_lptr_t **pcondlst, const char *name) { + config_setting_t *setting = config_lookup(pcfg, name); + if (setting) { + // Parse an array of options + if (config_setting_is_array(setting)) { + int i = config_setting_length(setting); + while (i--) + condlst_add(pcondlst, + config_setting_get_string_elem(setting, i)); + } + // Treat it as a single pattern if it's a string + else if (CONFIG_TYPE_STRING == config_setting_type(setting)) { + condlst_add(pcondlst, config_setting_get_string(setting)); + } + } +} + +/** + * Parse an opacity rule list in configuration file. + */ +static inline void +parse_cfg_condlst_opct(options_t *opt, const config_t *pcfg, const char *name) { + config_setting_t *setting = config_lookup(pcfg, name); + if (setting) { + // Parse an array of options + if (config_setting_is_array(setting)) { + int i = config_setting_length(setting); + while (i--) + if (!parse_rule_opacity( + &opt->opacity_rules, + config_setting_get_string_elem(setting, i))) + exit(1); + } + // Treat it as a single pattern if it's a string + else if (config_setting_type(setting) == CONFIG_TYPE_STRING) { + if (!parse_rule_opacity(&opt->opacity_rules, + config_setting_get_string(setting))) + exit(1); + } + } +} + +static inline void parse_wintype_config(const config_t *cfg, const char *member_name, + win_option_t *o, win_option_mask_t *mask) { + char *str = mstrjoin("wintypes.", member_name); + const config_setting_t *setting = config_lookup(cfg, str); + free(str); + + int ival = 0; + const char *sval = NULL; + + if (setting) { + if (config_setting_lookup_bool(setting, "shadow", &ival)) { + o->shadow = ival; + mask->shadow = true; + } + if (config_setting_lookup_bool(setting, "fade", &ival)) { + o->fade = ival; + mask->fade = true; + } + if (config_setting_lookup_bool(setting, "focus", &ival)) { + o->focus = ival; + mask->focus = true; + } + if (config_setting_lookup_bool(setting, "blur-background", &ival)) { + o->blur_background = ival; + mask->blur_background = true; + } + if (config_setting_lookup_bool(setting, "full-shadow", &ival)) { + o->full_shadow = ival; + mask->full_shadow = true; + } + if (config_setting_lookup_bool(setting, "redir-ignore", &ival)) { + o->redir_ignore = ival; + mask->redir_ignore = true; + } + if (config_setting_lookup_bool(setting, "clip-shadow-above", &ival)) { + o->clip_shadow_above = ival; + mask->clip_shadow_above = true; + } + if (config_setting_lookup_string(setting, "animation", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) + animation = OPEN_WINDOW_ANIMATION_NONE; + + o->animation = animation; + mask->animation = OPEN_WINDOW_ANIMATION_INVALID; + } + if (config_setting_lookup_string(setting, "animation-unmap", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) + animation = OPEN_WINDOW_ANIMATION_NONE; + + o->animation_unmap = animation; + mask->animation_unmap = OPEN_WINDOW_ANIMATION_INVALID; + } + if (config_setting_lookup_string(setting, "animation-workspace-in", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) + animation = OPEN_WINDOW_ANIMATION_NONE; + + o->animation_workspace_in = animation; + mask->animation_workspace_in = OPEN_WINDOW_ANIMATION_INVALID; + } + if (config_setting_lookup_string(setting, "animation-workspace-out", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) + animation = OPEN_WINDOW_ANIMATION_NONE; + + o->animation_workspace_out = animation; + mask->animation_workspace_out = OPEN_WINDOW_ANIMATION_INVALID; + } + + double fval; + if (config_setting_lookup_float(setting, "opacity", &fval)) { + o->opacity = normalize_d(fval); + mask->opacity = true; + } + } +} + +/** + * Parse a configuration file from default location. + * + * Returns the actually config_file name + */ +char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shadow_enable, + bool *fading_enable, bool *conv_kern_hasneg, + win_option_mask_t *winopt_mask) { + char *path = NULL; + FILE *f; + config_t cfg; + int ival = 0; + bool bval; + double dval = 0.0; + // libconfig manages string memory itself, so no need to manually free + // anything + const char *sval = NULL; + + f = open_config_file(config_file, &path); + if (!f) { + free(path); + if (config_file) { + log_fatal("Failed to read configuration file \"%s\".", config_file); + return ERR_PTR(-1); + } + return NULL; + } + + config_init(&cfg); +#ifdef CONFIG_OPTION_ALLOW_OVERRIDES + config_set_options(&cfg, CONFIG_OPTION_ALLOW_OVERRIDES); +#endif + { + // dirname() could modify the original string, thus we must pass a + // copy + char *path2 = strdup(path); + char *parent = dirname(path2); + + if (parent) + config_set_include_dir(&cfg, parent); + + free(path2); + } + + { + int read_result = config_read(&cfg, f); + fclose(f); + f = NULL; + if (read_result == CONFIG_FALSE) { + log_fatal("Error when reading configuration file \"%s\", line " + "%d: %s", + path, config_error_line(&cfg), config_error_text(&cfg)); + goto err; + } + } + config_set_auto_convert(&cfg, 1); + + // Get options from the configuration file. We don't do range checking + // right now. It will be done later + + // -D (fade_delta) + if (config_lookup_int(&cfg, "fade-delta", &ival)) + opt->fade_delta = ival; + // -I (fade_in_step) + if (config_lookup_float(&cfg, "fade-in-step", &dval)) + opt->fade_in_step = normalize_d(dval); + // -O (fade_out_step) + if (config_lookup_float(&cfg, "fade-out-step", &dval)) + opt->fade_out_step = normalize_d(dval); + // -r (shadow_radius) + config_lookup_int(&cfg, "shadow-radius", &opt->shadow_radius); + // -o (shadow_opacity) + config_lookup_float(&cfg, "shadow-opacity", &opt->shadow_opacity); + // -l (shadow_offset_x) + config_lookup_int(&cfg, "shadow-offset-x", &opt->shadow_offset_x); + // -t (shadow_offset_y) + config_lookup_int(&cfg, "shadow-offset-y", &opt->shadow_offset_y); + // -i (inactive_opacity) + if (config_lookup_float(&cfg, "inactive-opacity", &dval)) + opt->inactive_opacity = normalize_d(dval); + // --active_opacity + if (config_lookup_float(&cfg, "active-opacity", &dval)) + opt->active_opacity = normalize_d(dval); + // --corner-radius + config_lookup_int(&cfg, "corner-radius", &opt->corner_radius); + // --rounded-corners-exclude + parse_cfg_condlst(&cfg, &opt->rounded_corners_blacklist, "rounded-corners-exclude"); + // -e (frame_opacity) + config_lookup_float(&cfg, "frame-opacity", &opt->frame_opacity); + // -c (shadow_enable) + if (config_lookup_bool(&cfg, "shadow", &ival)) + *shadow_enable = ival; + // -C (no_dock_shadow) + if (config_lookup_bool(&cfg, "no-dock-shadow", &ival)) { + log_error("Option `no-dock-shadow` has been removed. Please use the " + "wintype option `shadow` of `dock` instead."); + goto err; + } + // -G (no_dnd_shadow) + if (config_lookup_bool(&cfg, "no-dnd-shadow", &ival)) { + log_error("Option `no-dnd-shadow` has been removed. Please use the " + "wintype option `shadow` of `dnd` instead."); + goto err; + }; + // -m (menu_opacity) + if (config_lookup_float(&cfg, "menu-opacity", &dval)) { + log_warn("Option `menu-opacity` is deprecated, and will be " + "removed.Please use the " + "wintype option `opacity` of `popup_menu` and `dropdown_menu` " + "instead."); + opt->wintype_option[WINTYPE_DROPDOWN_MENU].opacity = dval; + opt->wintype_option[WINTYPE_POPUP_MENU].opacity = dval; + winopt_mask[WINTYPE_DROPDOWN_MENU].opacity = true; + winopt_mask[WINTYPE_POPUP_MENU].opacity = true; + } + // -f (fading_enable) + if (config_lookup_bool(&cfg, "fading", &ival)) + *fading_enable = ival; + // --no-fading-open-close + lcfg_lookup_bool(&cfg, "no-fading-openclose", &opt->no_fading_openclose); + // --no-fading-destroyed-argb + lcfg_lookup_bool(&cfg, "no-fading-destroyed-argb", &opt->no_fading_destroyed_argb); + // --shadow-red + config_lookup_float(&cfg, "shadow-red", &opt->shadow_red); + // --shadow-green + config_lookup_float(&cfg, "shadow-green", &opt->shadow_green); + // --shadow-blue + config_lookup_float(&cfg, "shadow-blue", &opt->shadow_blue); + // --shadow-color + if (config_lookup_string(&cfg, "shadow-color", &sval)) { + struct color rgb; + rgb = hex_to_rgb(sval); + opt->shadow_red = rgb.red; + opt->shadow_green = rgb.green; + opt->shadow_blue = rgb.blue; + } + // --shadow-exclude-reg + if (config_lookup_string(&cfg, "shadow-exclude-reg", &sval)) + opt->shadow_exclude_reg_str = strdup(sval); + // --inactive-opacity-override + lcfg_lookup_bool(&cfg, "inactive-opacity-override", &opt->inactive_opacity_override); + // --inactive-dim + config_lookup_float(&cfg, "inactive-dim", &opt->inactive_dim); + // --mark-wmwin-focused + lcfg_lookup_bool(&cfg, "mark-wmwin-focused", &opt->mark_wmwin_focused); + // --mark-ovredir-focused + lcfg_lookup_bool(&cfg, "mark-ovredir-focused", &opt->mark_ovredir_focused); + // --shadow-ignore-shaped + lcfg_lookup_bool(&cfg, "shadow-ignore-shaped", &opt->shadow_ignore_shaped); + // --detect-rounded-corners + lcfg_lookup_bool(&cfg, "detect-rounded-corners", &opt->detect_rounded_corners); + // --xinerama-shadow-crop + lcfg_lookup_bool(&cfg, "xinerama-shadow-crop", &opt->xinerama_shadow_crop); + // --detect-client-opacity + lcfg_lookup_bool(&cfg, "detect-client-opacity", &opt->detect_client_opacity); + // --refresh-rate + if (config_lookup_int(&cfg, "refresh-rate", &opt->refresh_rate)) { + if (opt->refresh_rate < 0) { + log_warn("Invalid refresh rate %d, fallback to 0", opt->refresh_rate); + opt->refresh_rate = 0; + } + } + // --vsync + if (config_lookup_string(&cfg, "vsync", &sval)) { + opt->vsync = parse_vsync(sval); + log_warn("vsync option will take a boolean from now on. \"%s\" is " + "interpreted as \"%s\" for compatibility, but this will stop " + "working soon", + sval, opt->vsync ? "true" : "false"); + } + lcfg_lookup_bool(&cfg, "vsync", &opt->vsync); + // --backend + if (config_lookup_string(&cfg, "backend", &sval)) { + opt->backend = parse_backend(sval); + if (opt->backend >= NUM_BKEND) { + log_fatal("Cannot parse backend"); + goto err; + } + } + // --log-level + if (config_lookup_string(&cfg, "log-level", &sval)) { + auto level = string_to_log_level(sval); + if (level == LOG_LEVEL_INVALID) { + log_warn("Invalid log level, defaults to WARN"); + } else { + log_set_level_tls(level); + } + } + // --log-file + if (config_lookup_string(&cfg, "log-file", &sval)) { + if (*sval != '/') { + log_warn("The log-file in your configuration file is not an " + "absolute path"); + } + opt->logpath = strdup(sval); + } + // --sw-opti + lcfg_lookup_bool(&cfg, "sw-opti", &opt->sw_opti); + // --use-ewmh-active-win + lcfg_lookup_bool(&cfg, "use-ewmh-active-win", &opt->use_ewmh_active_win); + // --unredir-if-possible + lcfg_lookup_bool(&cfg, "unredir-if-possible", &opt->unredir_if_possible); + // --unredir-if-possible-delay + if (config_lookup_int(&cfg, "unredir-if-possible-delay", &ival)) { + if (ival < 0) { + log_warn("Invalid unredir-if-possible-delay %d", ival); + } else { + opt->unredir_if_possible_delay = ival; + } + } + // --inactive-dim-fixed + lcfg_lookup_bool(&cfg, "inactive-dim-fixed", &opt->inactive_dim_fixed); + // --detect-transient + lcfg_lookup_bool(&cfg, "detect-transient", &opt->detect_transient); + // --detect-client-leader + lcfg_lookup_bool(&cfg, "detect-client-leader", &opt->detect_client_leader); + // --no-ewmh-fullscreen + lcfg_lookup_bool(&cfg, "no-ewmh-fullscreen", &opt->no_ewmh_fullscreen); + // --transparent-clipping + lcfg_lookup_bool(&cfg, "transparent-clipping", &opt->transparent_clipping); + // --shadow-exclude + parse_cfg_condlst(&cfg, &opt->shadow_blacklist, "shadow-exclude"); + // --clip-shadow-above + parse_cfg_condlst(&cfg, &opt->shadow_clip_list, "clip-shadow-above"); + // --fade-exclude + parse_cfg_condlst(&cfg, &opt->fade_blacklist, "fade-exclude"); + // --animations + lcfg_lookup_bool(&cfg, "animations", &opt->animations); + // --animation-for-open-window + if (config_lookup_string(&cfg, "animation-for-open-window", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) { + log_fatal("Invalid open-window animation %s", sval); + goto err; + } + opt->animation_for_open_window = animation; + } + // --animation-for-transient-window + if (config_lookup_string(&cfg, "animation-for-transient-window", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) { + log_fatal("Invalid transient-window animation %s", sval); + goto err; + } + opt->animation_for_transient_window = animation; + } + // --animation-for-unmap-window + if (config_lookup_string(&cfg, "animation-for-unmap-window", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) { + log_fatal("Invalid unmap-window animation %s", sval); + goto err; + } + opt->animation_for_unmap_window = animation; + } + // --animation-for-workspace-switch-in + if (config_lookup_string(&cfg, "animation-for-workspace-switch-in", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) { + log_fatal("Invalid workspace-switch-in animation %s", sval); + goto err; + } + opt->animation_for_workspace_switch_in = animation; + } + // --animation-for-workspace-switch-out + if (config_lookup_string(&cfg, "animation-for-workspace-switch-out", &sval)) { + enum open_window_animation animation = parse_open_window_animation(sval); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) { + log_fatal("Invalid workspace-switch-out animation %s", sval); + goto err; + } + opt->animation_for_workspace_switch_out = animation; + } + // --animation-stiffness + config_lookup_float(&cfg, "animation-stiffness", &opt->animation_stiffness); + // --animation-window-mass + config_lookup_float(&cfg, "animation-window-mass", &opt->animation_window_mass); + // --animation-dampening + config_lookup_float(&cfg, "animation-dampening", &opt->animation_dampening); + // --animation-delta + config_lookup_float(&cfg, "animation-delta", &opt->animation_delta); + // --animation-force-steps + lcfg_lookup_bool(&cfg, "animation-force-steps", &opt->animation_force_steps); + // --animation-clamping + lcfg_lookup_bool(&cfg, "animation-clamping", &opt->animation_clamping); + // --focus-exclude + parse_cfg_condlst(&cfg, &opt->focus_blacklist, "focus-exclude"); + // --invert-color-include + parse_cfg_condlst(&cfg, &opt->invert_color_list, "invert-color-include"); + // --blur-background-exclude + parse_cfg_condlst(&cfg, &opt->blur_background_blacklist, "blur-background-exclude"); + // --opacity-rule + parse_cfg_condlst_opct(opt, &cfg, "opacity-rule"); + // --unredir-if-possible-exclude + parse_cfg_condlst(&cfg, &opt->unredir_if_possible_blacklist, + "unredir-if-possible-exclude"); + // --blur-method + if (config_lookup_string(&cfg, "blur-method", &sval)) { + enum blur_method method = parse_blur_method(sval); + if (method >= BLUR_METHOD_INVALID) { + log_fatal("Invalid blur method %s", sval); + goto err; + } + opt->blur_method = method; + } + // --blur-size + config_lookup_int(&cfg, "blur-size", &opt->blur_radius); + // --blur-deviation + config_lookup_float(&cfg, "blur-deviation", &opt->blur_deviation); + // --blur-strength + config_lookup_int(&cfg, "blur-strength", &opt->blur_strength); + // --blur-background + if (config_lookup_bool(&cfg, "blur-background", &ival) && ival) { + if (opt->blur_method == BLUR_METHOD_NONE) { + opt->blur_method = BLUR_METHOD_KERNEL; + } + } + // --blur-background-frame + lcfg_lookup_bool(&cfg, "blur-background-frame", &opt->blur_background_frame); + // --blur-background-fixed + lcfg_lookup_bool(&cfg, "blur-background-fixed", &opt->blur_background_fixed); + // --blur-kern + if (config_lookup_string(&cfg, "blur-kern", &sval)) { + opt->blur_kerns = + parse_blur_kern_lst(sval, conv_kern_hasneg, &opt->blur_kernel_count); + if (!opt->blur_kerns) { + log_fatal("Cannot parse \"blur-kern\""); + goto err; + } + } + // --resize-damage + config_lookup_int(&cfg, "resize-damage", &opt->resize_damage); + // --glx-no-stencil + lcfg_lookup_bool(&cfg, "glx-no-stencil", &opt->glx_no_stencil); + // --glx-no-rebind-pixmap + lcfg_lookup_bool(&cfg, "glx-no-rebind-pixmap", &opt->glx_no_rebind_pixmap); + lcfg_lookup_bool(&cfg, "force-win-blend", &opt->force_win_blend); + // --glx-swap-method + if (config_lookup_string(&cfg, "glx-swap-method", &sval)) { + char *endptr; + long val = strtol(sval, &endptr, 10); + if (*endptr || !(*sval)) { + // sval is not a number, or an empty string + val = -1; + } + if (strcmp(sval, "undefined") != 0 && val != 0) { + // If not undefined, we will use damage and buffer-age to limit + // the rendering area. + opt->use_damage = true; + } + log_warn("glx-swap-method has been deprecated since v6, your setting " + "\"%s\" should be %s.", + sval, + opt->use_damage ? "replaced by `use-damage = true`" : "removed"); + } + // --use-damage + lcfg_lookup_bool(&cfg, "use-damage", &opt->use_damage); + + // --max-brightness + if (config_lookup_float(&cfg, "max-brightness", &opt->max_brightness) && + opt->use_damage && opt->max_brightness < 1) { + log_warn("max-brightness requires use-damage = false. Falling back to " + "1.0"); + opt->max_brightness = 1.0; + } + + // --glx-use-gpushader4 + if (config_lookup_bool(&cfg, "glx-use-gpushader4", &ival) && ival) { + log_warn("glx-use-gpushader4 is deprecated since v6, please remove it " + "from" + "your config file"); + } + // --xrender-sync + if (config_lookup_bool(&cfg, "xrender-sync", &ival) && ival) { + log_error("Please use xrender-sync-fence instead of xrender-sync."); + goto err; + } + // --xrender-sync-fence + lcfg_lookup_bool(&cfg, "xrender-sync-fence", &opt->xrender_sync_fence); + + if (lcfg_lookup_bool(&cfg, "clear-shadow", &bval)) + log_warn("\"clear-shadow\" is removed as an option, and is always" + " enabled now. Consider removing it from your config file"); + if (lcfg_lookup_bool(&cfg, "paint-on-overlay", &bval)) { + log_error("\"paint-on-overlay\" has been removed as an option, and " + "the feature is enabled whenever possible"); + goto err; + } + + if (config_lookup_float(&cfg, "alpha-step", &dval)) { + log_error("\"alpha-step\" has been removed, compton now tries to make use" + " of all alpha values"); + goto err; + } + + const char *deprecation_message attr_unused = + "has been removed. If you encounter problems " + "without this feature, please feel free to open a bug report"; + + config_setting_t *blur_cfg = config_lookup(&cfg, "blur"); + if (blur_cfg) { + if (config_setting_lookup_string(blur_cfg, "method", &sval)) { + enum blur_method method = parse_blur_method(sval); + if (method >= BLUR_METHOD_INVALID) { + log_warn("Invalid blur method %s, ignoring.", sval); + } else { + opt->blur_method = method; + } + } + + config_setting_lookup_int(blur_cfg, "size", &opt->blur_radius); + + if (config_setting_lookup_string(blur_cfg, "kernel", &sval)) { + opt->blur_kerns = parse_blur_kern_lst(sval, conv_kern_hasneg, + &opt->blur_kernel_count); + if (!opt->blur_kerns) { + log_warn("Failed to parse blur kernel: %s", sval); + } + } + + config_setting_lookup_float(blur_cfg, "deviation", &opt->blur_deviation); + config_setting_lookup_int(blur_cfg, "strength", &opt->blur_strength); + } + + // --write-pid-path + if (config_lookup_string(&cfg, "write-pid-path", &sval)) { + if (*sval != '/') { + log_warn("The write-pid-path in your configuration file is not" + " an absolute path"); + } + opt->write_pid_path = strdup(sval); + } + + // Wintype settings + + // XXX ! Refactor all the wintype_* arrays into a struct + for (wintype_t i = 0; i < NUM_WINTYPES; ++i) { + parse_wintype_config(&cfg, WINTYPES[i], &opt->wintype_option[i], + &winopt_mask[i]); + } + + // Compatibility with the old name for notification windows. + parse_wintype_config(&cfg, "notify", &opt->wintype_option[WINTYPE_NOTIFICATION], + &winopt_mask[WINTYPE_NOTIFICATION]); + + config_destroy(&cfg); + return path; + +err: + config_destroy(&cfg); + free(path); + return ERR_PTR(-1); +} diff --git a/src/dbus.c b/src/dbus.c new file mode 100644 index 0000000..8d6094e --- /dev/null +++ b/src/dbus.c @@ -0,0 +1,1340 @@ +// SPDX-License-Identifier: MIT +/* + * Compton - a compositor for X11 + * + * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard + * + * Copyright (c) 2011-2013, Christopher Jeffrey + * See LICENSE-mit for more information. + * + */ + +#include <X11/Xlib.h> +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <unistd.h> +#include <xcb/xcb.h> + +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "list.h" +#include "log.h" +#include "string_utils.h" +#include "types.h" +#include "uthash_extra.h" +#include "utils.h" +#include "win.h" + +#include "dbus.h" + +struct cdbus_data { + /// DBus connection. + DBusConnection *dbus_conn; + /// DBus service name. + char *dbus_service; +}; + +// Window type +typedef uint32_t cdbus_window_t; +#define CDBUS_TYPE_WINDOW DBUS_TYPE_UINT32 +#define CDBUS_TYPE_WINDOW_STR DBUS_TYPE_UINT32_AS_STRING + +typedef uint32_t cdbus_enum_t; +#define CDBUS_TYPE_ENUM DBUS_TYPE_UINT32 +#define CDBUS_TYPE_ENUM_STR DBUS_TYPE_UINT32_AS_STRING + +#define CDBUS_SERVICE_NAME "com.github.chjj.compton" +#define CDBUS_INTERFACE_NAME CDBUS_SERVICE_NAME +#define CDBUS_OBJECT_NAME "/com/github/chjj/compton" +#define CDBUS_ERROR_PREFIX CDBUS_INTERFACE_NAME ".error" +#define CDBUS_ERROR_UNKNOWN CDBUS_ERROR_PREFIX ".unknown" +#define CDBUS_ERROR_UNKNOWN_S "Well, I don't know what happened. Do you?" +#define CDBUS_ERROR_BADMSG CDBUS_ERROR_PREFIX ".bad_message" +#define CDBUS_ERROR_BADMSG_S \ + "Unrecognized command. Beware compton " \ + "cannot make you a sandwich." +#define CDBUS_ERROR_BADARG CDBUS_ERROR_PREFIX ".bad_argument" +#define CDBUS_ERROR_BADARG_S "Failed to parse argument %d: %s" +#define CDBUS_ERROR_BADWIN CDBUS_ERROR_PREFIX ".bad_window" +#define CDBUS_ERROR_BADWIN_S "Requested window %#010x not found." +#define CDBUS_ERROR_BADTGT CDBUS_ERROR_PREFIX ".bad_target" +#define CDBUS_ERROR_BADTGT_S "Target \"%s\" not found." +#define CDBUS_ERROR_FORBIDDEN CDBUS_ERROR_PREFIX ".forbidden" +#define CDBUS_ERROR_FORBIDDEN_S "Incorrect password, access denied." +#define CDBUS_ERROR_CUSTOM CDBUS_ERROR_PREFIX ".custom" +#define CDBUS_ERROR_CUSTOM_S "%s" + +#define cdbus_reply_err(ps, srcmsg, err_name, err_format, ...) \ + cdbus_reply_errm((ps), dbus_message_new_error_printf( \ + (srcmsg), (err_name), (err_format), ##__VA_ARGS__)) + +static DBusHandlerResult cdbus_process(DBusConnection *conn, DBusMessage *m, void *); + +static dbus_bool_t cdbus_callback_add_timeout(DBusTimeout *timeout, void *data); + +static void cdbus_callback_remove_timeout(DBusTimeout *timeout, void *data); + +static void cdbus_callback_timeout_toggled(DBusTimeout *timeout, void *data); + +static dbus_bool_t cdbus_callback_add_watch(DBusWatch *watch, void *data); + +static void cdbus_callback_remove_watch(DBusWatch *watch, void *data); + +static void cdbus_callback_watch_toggled(DBusWatch *watch, void *data); + +/** + * Initialize D-Bus connection. + */ +bool cdbus_init(session_t *ps, const char *uniq) { + auto cd = cmalloc(struct cdbus_data); + cd->dbus_service = NULL; + + // Set ps->dbus_data here because add_watch functions need it + ps->dbus_data = cd; + + DBusError err = {}; + + // Initialize + dbus_error_init(&err); + + // Connect to D-Bus + // Use dbus_bus_get_private() so we can fully recycle it ourselves + cd->dbus_conn = dbus_bus_get_private(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + log_error("D-Bus connection failed (%s).", err.message); + dbus_error_free(&err); + goto fail; + } + + if (!cd->dbus_conn) { + log_error("D-Bus connection failed for unknown reason."); + goto fail; + } + + // Avoid exiting on disconnect + dbus_connection_set_exit_on_disconnect(cd->dbus_conn, false); + + // Request service name + { + // Build service name + size_t service_len = strlen(CDBUS_SERVICE_NAME) + strlen(uniq) + 2; + char *service = ccalloc(service_len, char); + snprintf(service, service_len, "%s.%s", CDBUS_SERVICE_NAME, uniq); + + // Make a valid dbus name by converting non alphanumeric characters to + // underscore + char *tmp = service + strlen(CDBUS_SERVICE_NAME) + 1; + while (*tmp) { + if (!isalnum((unsigned char)*tmp)) { + *tmp = '_'; + } + tmp++; + } + cd->dbus_service = service; + + // Request for the name + int ret = dbus_bus_request_name(cd->dbus_conn, service, + DBUS_NAME_FLAG_DO_NOT_QUEUE, &err); + + if (dbus_error_is_set(&err)) { + log_error("Failed to obtain D-Bus name (%s).", err.message); + dbus_error_free(&err); + goto fail; + } + + if (DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER != ret && + DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER != ret) { + log_error("Failed to become the primary owner of requested D-Bus " + "name (%d).", + ret); + goto fail; + } + } + + // Add watch handlers + if (!dbus_connection_set_watch_functions(cd->dbus_conn, cdbus_callback_add_watch, + cdbus_callback_remove_watch, + cdbus_callback_watch_toggled, ps, NULL)) { + log_error("Failed to add D-Bus watch functions."); + goto fail; + } + + // Add timeout handlers + if (!dbus_connection_set_timeout_functions( + cd->dbus_conn, cdbus_callback_add_timeout, cdbus_callback_remove_timeout, + cdbus_callback_timeout_toggled, ps, NULL)) { + log_error("Failed to add D-Bus timeout functions."); + goto fail; + } + + // Add match + dbus_bus_add_match(cd->dbus_conn, + "type='method_call',interface='" CDBUS_INTERFACE_NAME "'", &err); + if (dbus_error_is_set(&err)) { + log_error("Failed to add D-Bus match."); + dbus_error_free(&err); + goto fail; + } + dbus_connection_add_filter(cd->dbus_conn, cdbus_process, ps, NULL); + return true; +fail: + ps->dbus_data = NULL; + free(cd->dbus_service); + free(cd); + return false; +} + +/** + * Destroy D-Bus connection. + */ +void cdbus_destroy(session_t *ps) { + struct cdbus_data *cd = ps->dbus_data; + if (cd->dbus_conn) { + // Release DBus name firstly + if (cd->dbus_service) { + DBusError err = {}; + dbus_error_init(&err); + + dbus_bus_release_name(cd->dbus_conn, cd->dbus_service, &err); + if (dbus_error_is_set(&err)) { + log_error("Failed to release DBus name (%s).", err.message); + dbus_error_free(&err); + } + free(cd->dbus_service); + } + + // Close and unref the connection + dbus_connection_close(cd->dbus_conn); + dbus_connection_unref(cd->dbus_conn); + } + free(cd); +} + +/** @name DBusTimeout handling + */ +///@{ + +typedef struct ev_dbus_timer { + ev_timer w; + DBusTimeout *t; +} ev_dbus_timer; + +/** + * Callback for handling a D-Bus timeout. + */ +static void +cdbus_callback_handle_timeout(EV_P attr_unused, ev_timer *w, int revents attr_unused) { + ev_dbus_timer *t = (void *)w; + dbus_timeout_handle(t->t); +} + +/** + * Callback for adding D-Bus timeout. + */ +static dbus_bool_t cdbus_callback_add_timeout(DBusTimeout *timeout, void *data) { + session_t *ps = data; + + auto t = ccalloc(1, ev_dbus_timer); + double i = dbus_timeout_get_interval(timeout) / 1000.0; + ev_timer_init(&t->w, cdbus_callback_handle_timeout, i, i); + t->t = timeout; + dbus_timeout_set_data(timeout, t, NULL); + + if (dbus_timeout_get_enabled(timeout)) + ev_timer_start(ps->loop, &t->w); + + return true; +} + +/** + * Callback for removing D-Bus timeout. + */ +static void cdbus_callback_remove_timeout(DBusTimeout *timeout, void *data) { + session_t *ps = data; + + ev_dbus_timer *t = dbus_timeout_get_data(timeout); + assert(t); + ev_timer_stop(ps->loop, &t->w); + free(t); +} + +/** + * Callback for toggling a D-Bus timeout. + */ +static void cdbus_callback_timeout_toggled(DBusTimeout *timeout, void *data) { + session_t *ps = data; + ev_dbus_timer *t = dbus_timeout_get_data(timeout); + + assert(t); + ev_timer_stop(ps->loop, &t->w); + if (dbus_timeout_get_enabled(timeout)) { + double i = dbus_timeout_get_interval(timeout) / 1000.0; + ev_timer_set(&t->w, i, i); + ev_timer_start(ps->loop, &t->w); + } +} + +///@} + +/** @name DBusWatch handling + */ +///@{ + +typedef struct ev_dbus_io { + ev_io w; + struct cdbus_data *cd; + DBusWatch *dw; +} ev_dbus_io; + +void cdbus_io_callback(EV_P attr_unused, ev_io *w, int revents) { + ev_dbus_io *dw = (void *)w; + DBusWatchFlags flags = 0; + if (revents & EV_READ) + flags |= DBUS_WATCH_READABLE; + if (revents & EV_WRITE) + flags |= DBUS_WATCH_WRITABLE; + dbus_watch_handle(dw->dw, flags); + while (dbus_connection_dispatch(dw->cd->dbus_conn) != DBUS_DISPATCH_COMPLETE) + ; +} + +/** + * Determine the poll condition of a DBusWatch. + */ +static inline int cdbus_get_watch_cond(DBusWatch *watch) { + const unsigned flags = dbus_watch_get_flags(watch); + int condition = 0; + if (flags & DBUS_WATCH_READABLE) + condition |= EV_READ; + if (flags & DBUS_WATCH_WRITABLE) + condition |= EV_WRITE; + + return condition; +} + +/** + * Callback for adding D-Bus watch. + */ +static dbus_bool_t cdbus_callback_add_watch(DBusWatch *watch, void *data) { + session_t *ps = data; + + auto w = ccalloc(1, ev_dbus_io); + w->dw = watch; + w->cd = ps->dbus_data; + ev_io_init(&w->w, cdbus_io_callback, dbus_watch_get_unix_fd(watch), + cdbus_get_watch_cond(watch)); + + // Leave disabled watches alone + if (dbus_watch_get_enabled(watch)) + ev_io_start(ps->loop, &w->w); + + dbus_watch_set_data(watch, w, NULL); + + // Always return true + return true; +} + +/** + * Callback for removing D-Bus watch. + */ +static void cdbus_callback_remove_watch(DBusWatch *watch, void *data) { + session_t *ps = data; + ev_dbus_io *w = dbus_watch_get_data(watch); + ev_io_stop(ps->loop, &w->w); + free(w); +} + +/** + * Callback for toggling D-Bus watch status. + */ +static void cdbus_callback_watch_toggled(DBusWatch *watch, void *data) { + session_t *ps = data; + ev_io *w = dbus_watch_get_data(watch); + if (dbus_watch_get_enabled(watch)) + ev_io_start(ps->loop, w); + else + ev_io_stop(ps->loop, w); +} + +///@} + +/** @name Message argument appending callbacks + */ +///@{ + +/** + * Callback to append a bool argument to a message. + */ +static bool cdbus_apdarg_bool(session_t *ps attr_unused, DBusMessage *msg, const void *data) { + assert(data); + + dbus_bool_t val = *(const bool *)data; + + if (!dbus_message_append_args(msg, DBUS_TYPE_BOOLEAN, &val, DBUS_TYPE_INVALID)) { + log_error("Failed to append argument."); + return false; + } + + return true; +} + +/** + * Callback to append an int32 argument to a message. + */ +static bool cdbus_apdarg_int32(session_t *ps attr_unused, DBusMessage *msg, const void *data) { + if (!dbus_message_append_args(msg, DBUS_TYPE_INT32, data, DBUS_TYPE_INVALID)) { + log_error("Failed to append argument."); + return false; + } + + return true; +} + +/** + * Callback to append an uint32 argument to a message. + */ +static bool +cdbus_apdarg_uint32(session_t *ps attr_unused, DBusMessage *msg, const void *data) { + if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, data, DBUS_TYPE_INVALID)) { + log_error("Failed to append argument."); + return false; + } + + return true; +} + +/** + * Callback to append a double argument to a message. + */ +static bool +cdbus_apdarg_double(session_t *ps attr_unused, DBusMessage *msg, const void *data) { + if (!dbus_message_append_args(msg, DBUS_TYPE_DOUBLE, data, DBUS_TYPE_INVALID)) { + log_error("Failed to append argument."); + return false; + } + + return true; +} + +/** + * Callback to append a Window argument to a message. + */ +static bool cdbus_apdarg_wid(session_t *ps attr_unused, DBusMessage *msg, const void *data) { + assert(data); + cdbus_window_t val = *(const xcb_window_t *)data; + + if (!dbus_message_append_args(msg, CDBUS_TYPE_WINDOW, &val, DBUS_TYPE_INVALID)) { + log_error("Failed to append argument."); + return false; + } + + return true; +} + +/** + * Callback to append an cdbus_enum_t argument to a message. + */ +static bool cdbus_apdarg_enum(session_t *ps attr_unused, DBusMessage *msg, const void *data) { + assert(data); + if (!dbus_message_append_args(msg, CDBUS_TYPE_ENUM, data, DBUS_TYPE_INVALID)) { + log_error("Failed to append argument."); + return false; + } + + return true; +} + +/** + * Callback to append a string argument to a message. + */ +static bool +cdbus_apdarg_string(session_t *ps attr_unused, DBusMessage *msg, const void *data) { + const char *str = data; + if (!str) + str = ""; + + if (!dbus_message_append_args(msg, DBUS_TYPE_STRING, &str, DBUS_TYPE_INVALID)) { + log_error("Failed to append argument."); + return false; + } + + return true; +} + +/** + * Callback to append all window IDs to a message. + */ +static bool cdbus_apdarg_wids(session_t *ps, DBusMessage *msg, const void *data attr_unused) { + // Get the number of wids we are to include + unsigned count = 0; + HASH_ITER2(ps->windows, w) { + assert(!w->destroyed); + ++count; + } + + if (!count) { + // Nothing to append + return true; + } + + // Allocate memory for an array of window IDs + auto arr = ccalloc(count, cdbus_window_t); + + // Build the array + cdbus_window_t *pcur = arr; + HASH_ITER2(ps->windows, w) { + assert(!w->destroyed); + *pcur = w->id; + ++pcur; + } + assert(pcur == arr + count); + + // Append arguments + if (!dbus_message_append_args(msg, DBUS_TYPE_ARRAY, CDBUS_TYPE_WINDOW, &arr, + count, DBUS_TYPE_INVALID)) { + log_error("Failed to append argument."); + free(arr); + return false; + } + + free(arr); + return true; +} +///@} + +/** + * Send a D-Bus signal. + * + * @param ps current session + * @param name signal name + * @param func a function that modifies the built message, to, for example, + * add an argument + * @param data data pointer to pass to the function + */ +static bool cdbus_signal(session_t *ps, const char *name, + bool (*func)(session_t *ps, DBusMessage *msg, const void *data), + const void *data) { + struct cdbus_data *cd = ps->dbus_data; + DBusMessage *msg = NULL; + + // Create a signal + msg = dbus_message_new_signal(CDBUS_OBJECT_NAME, CDBUS_INTERFACE_NAME, name); + if (!msg) { + log_error("Failed to create D-Bus signal."); + return false; + } + + // Append arguments onto message + if (func && !func(ps, msg, data)) { + dbus_message_unref(msg); + return false; + } + + // Send the message and flush the connection + if (!dbus_connection_send(cd->dbus_conn, msg, NULL)) { + log_error("Failed to send D-Bus signal."); + dbus_message_unref(msg); + return false; + } + dbus_connection_flush(cd->dbus_conn); + + // Free the message + dbus_message_unref(msg); + + return true; +} + +/** + * Send a signal with a Window ID as argument. + */ +static inline bool cdbus_signal_wid(session_t *ps, const char *name, xcb_window_t wid) { + return cdbus_signal(ps, name, cdbus_apdarg_wid, &wid); +} + +/** + * Send a D-Bus reply. + * + * @param ps current session + * @param srcmsg original message + * @param func a function that modifies the built message, to, for example, + * add an argument + * @param data data pointer to pass to the function + */ +static bool cdbus_reply(session_t *ps, DBusMessage *srcmsg, + bool (*func)(session_t *ps, DBusMessage *msg, const void *data), + const void *data) { + struct cdbus_data *cd = ps->dbus_data; + DBusMessage *msg = NULL; + + // Create a reply + msg = dbus_message_new_method_return(srcmsg); + if (!msg) { + log_error("Failed to create D-Bus reply."); + return false; + } + + // Append arguments onto message + if (func && !func(ps, msg, data)) { + dbus_message_unref(msg); + return false; + } + + // Send the message and flush the connection + if (!dbus_connection_send(cd->dbus_conn, msg, NULL)) { + log_error("Failed to send D-Bus reply."); + dbus_message_unref(msg); + return false; + } + dbus_connection_flush(cd->dbus_conn); + + // Free the message + dbus_message_unref(msg); + + return true; +} + +/** + * Send a reply with a bool argument. + */ +static inline bool cdbus_reply_bool(session_t *ps, DBusMessage *srcmsg, bool bval) { + return cdbus_reply(ps, srcmsg, cdbus_apdarg_bool, &bval); +} + +/** + * Send a reply with an int32 argument. + */ +static inline bool cdbus_reply_int32(session_t *ps, DBusMessage *srcmsg, int32_t val) { + return cdbus_reply(ps, srcmsg, cdbus_apdarg_int32, &val); +} + +/** + * Send a reply with an int32 argument, cast from a long. + */ +static inline bool cdbus_reply_int32l(session_t *ps, DBusMessage *srcmsg, long val) { + int32_t tmp = (int32_t)val; + return cdbus_reply(ps, srcmsg, cdbus_apdarg_int32, &tmp); +} + +/** + * Send a reply with an uint32 argument. + */ +static inline bool cdbus_reply_uint32(session_t *ps, DBusMessage *srcmsg, uint32_t val) { + return cdbus_reply(ps, srcmsg, cdbus_apdarg_uint32, &val); +} + +/** + * Send a reply with a double argument. + */ +static inline bool cdbus_reply_double(session_t *ps, DBusMessage *srcmsg, double val) { + return cdbus_reply(ps, srcmsg, cdbus_apdarg_double, &val); +} + +/** + * Send a reply with a wid argument. + */ +static inline bool cdbus_reply_wid(session_t *ps, DBusMessage *srcmsg, xcb_window_t wid) { + return cdbus_reply(ps, srcmsg, cdbus_apdarg_wid, &wid); +} + +/** + * Send a reply with a string argument. + */ +static inline bool cdbus_reply_string(session_t *ps, DBusMessage *srcmsg, const char *str) { + return cdbus_reply(ps, srcmsg, cdbus_apdarg_string, str); +} + +/** + * Send a reply with a enum argument. + */ +static inline bool cdbus_reply_enum(session_t *ps, DBusMessage *srcmsg, cdbus_enum_t eval) { + return cdbus_reply(ps, srcmsg, cdbus_apdarg_enum, &eval); +} + +/** + * Send a D-Bus error reply. + * + * @param ps current session + * @param msg the new error DBusMessage + */ +static bool cdbus_reply_errm(session_t *ps, DBusMessage *msg) { + struct cdbus_data *cd = ps->dbus_data; + if (!msg) { + log_error("Failed to create D-Bus reply."); + return false; + } + + // Send the message and flush the connection + if (!dbus_connection_send(cd->dbus_conn, msg, NULL)) { + log_error("Failed to send D-Bus reply."); + dbus_message_unref(msg); + return false; + } + dbus_connection_flush(cd->dbus_conn); + + // Free the message + dbus_message_unref(msg); + + return true; +} + +/** + * Get n-th argument of a D-Bus message. + * + * @param count the position of the argument to get, starting from 0 + * @param type libdbus type number of the type + * @param pdest pointer to the target + * @return true if successful, false otherwise. + */ +static bool cdbus_msg_get_arg(DBusMessage *msg, int count, const int type, void *pdest) { + assert(count >= 0); + + DBusMessageIter iter = {}; + if (!dbus_message_iter_init(msg, &iter)) { + log_error("Message has no argument."); + return false; + } + + { + const int oldcount = count; + while (count) { + if (!dbus_message_iter_next(&iter)) { + log_error("Failed to find argument %d.", oldcount); + return false; + } + --count; + } + } + + if (type != dbus_message_iter_get_arg_type(&iter)) { + log_error("Argument has incorrect type."); + return false; + } + + dbus_message_iter_get_basic(&iter, pdest); + + return true; +} + +/** @name Message processing + */ +///@{ + +/** + * Process a list_win D-Bus request. + */ +static bool cdbus_process_list_win(session_t *ps, DBusMessage *msg) { + cdbus_reply(ps, msg, cdbus_apdarg_wids, NULL); + + return true; +} + +/** + * Process a win_get D-Bus request. + */ +static bool cdbus_process_win_get(session_t *ps, DBusMessage *msg) { + cdbus_window_t wid = XCB_NONE; + const char *target = NULL; + DBusError err = {}; + + if (!dbus_message_get_args(msg, &err, CDBUS_TYPE_WINDOW, &wid, DBUS_TYPE_STRING, + &target, DBUS_TYPE_INVALID)) { + log_error("Failed to parse argument of \"win_get\" (%s).", err.message); + dbus_error_free(&err); + return false; + } + + auto w = find_managed_win(ps, wid); + + if (!w) { + log_error("Window %#010x not found.", wid); + cdbus_reply_err(ps, msg, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); + return true; + } + +#define cdbus_m_win_get_do(tgt, apdarg_func) \ + if (!strcmp(#tgt, target)) { \ + apdarg_func(ps, msg, w->tgt); \ + return true; \ + } + + cdbus_m_win_get_do(base.id, cdbus_reply_wid); + + // next + if (!strcmp("next", target)) { + cdbus_reply_wid( + ps, msg, + (list_node_is_last(&ps->window_stack, &w->base.stack_neighbour) + ? 0 + : list_entry(w->base.stack_neighbour.next, struct win, stack_neighbour) + ->id)); + return true; + } + + // map_state + if (!strcmp("map_state", target)) { + cdbus_reply_bool(ps, msg, w->a.map_state); + return true; + } + + cdbus_m_win_get_do(mode, cdbus_reply_enum); + cdbus_m_win_get_do(client_win, cdbus_reply_wid); + cdbus_m_win_get_do(ever_damaged, cdbus_reply_bool); + cdbus_m_win_get_do(window_type, cdbus_reply_enum); + cdbus_m_win_get_do(wmwin, cdbus_reply_bool); + cdbus_m_win_get_do(leader, cdbus_reply_wid); + if (!strcmp("focused_raw", target)) { + cdbus_reply_bool(ps, msg, win_is_focused_raw(ps, w)); + return true; + } + cdbus_m_win_get_do(fade_force, cdbus_reply_enum); + cdbus_m_win_get_do(shadow_force, cdbus_reply_enum); + cdbus_m_win_get_do(focused_force, cdbus_reply_enum); + cdbus_m_win_get_do(invert_color_force, cdbus_reply_enum); + cdbus_m_win_get_do(name, cdbus_reply_string); + cdbus_m_win_get_do(class_instance, cdbus_reply_string); + cdbus_m_win_get_do(class_general, cdbus_reply_string); + cdbus_m_win_get_do(role, cdbus_reply_string); + + cdbus_m_win_get_do(opacity, cdbus_reply_double); + cdbus_m_win_get_do(opacity_target, cdbus_reply_double); + cdbus_m_win_get_do(has_opacity_prop, cdbus_reply_bool); + cdbus_m_win_get_do(opacity_prop, cdbus_reply_uint32); + cdbus_m_win_get_do(opacity_is_set, cdbus_reply_bool); + cdbus_m_win_get_do(opacity_set, cdbus_reply_double); + + cdbus_m_win_get_do(frame_opacity, cdbus_reply_double); + if (!strcmp("left_width", target)) { + cdbus_reply_int32(ps, msg, w->frame_extents.left); + return true; + } + if (!strcmp("right_width", target)) { + cdbus_reply_int32(ps, msg, w->frame_extents.right); + return true; + } + if (!strcmp("top_width", target)) { + cdbus_reply_int32(ps, msg, w->frame_extents.top); + return true; + } + if (!strcmp("bottom_width", target)) { + cdbus_reply_int32(ps, msg, w->frame_extents.bottom); + return true; + } + + cdbus_m_win_get_do(shadow, cdbus_reply_bool); + cdbus_m_win_get_do(invert_color, cdbus_reply_bool); + cdbus_m_win_get_do(blur_background, cdbus_reply_bool); +#undef cdbus_m_win_get_do + + log_error(CDBUS_ERROR_BADTGT_S, target); + cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + + return true; +} + +/** + * Process a win_set D-Bus request. + */ +static bool cdbus_process_win_set(session_t *ps, DBusMessage *msg) { + cdbus_window_t wid = XCB_NONE; + const char *target = NULL; + DBusError err = {}; + + if (!dbus_message_get_args(msg, &err, CDBUS_TYPE_WINDOW, &wid, DBUS_TYPE_STRING, + &target, DBUS_TYPE_INVALID)) { + log_error("(): Failed to parse argument of \"win_set\" (%s).", err.message); + dbus_error_free(&err); + return false; + } + + auto w = find_managed_win(ps, wid); + + if (!w) { + log_error("Window %#010x not found.", wid); + cdbus_reply_err(ps, msg, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); + return true; + } + +#define cdbus_m_win_set_do(tgt, type, real_type) \ + if (!strcmp(MSTR(tgt), target)) { \ + real_type val; \ + if (!cdbus_msg_get_arg(msg, 2, type, &val)) \ + return false; \ + w->tgt = val; \ + goto cdbus_process_win_set_success; \ + } + + if (!strcmp("shadow_force", target)) { + cdbus_enum_t val = UNSET; + if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) + return false; + win_set_shadow_force(ps, w, val); + goto cdbus_process_win_set_success; + } + + if (!strcmp("fade_force", target)) { + cdbus_enum_t val = UNSET; + if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) + return false; + win_set_fade_force(w, val); + goto cdbus_process_win_set_success; + } + + if (!strcmp("focused_force", target)) { + cdbus_enum_t val = UNSET; + if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) + return false; + win_set_focused_force(ps, w, val); + goto cdbus_process_win_set_success; + } + + if (!strcmp("invert_color_force", target)) { + cdbus_enum_t val = UNSET; + if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) + return false; + win_set_invert_color_force(ps, w, val); + goto cdbus_process_win_set_success; + } +#undef cdbus_m_win_set_do + + log_error(CDBUS_ERROR_BADTGT_S, target); + cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + + return true; + +cdbus_process_win_set_success: + if (!dbus_message_get_no_reply(msg)) + cdbus_reply_bool(ps, msg, true); + return true; +} + +/** + * Process a find_win D-Bus request. + */ +static bool cdbus_process_find_win(session_t *ps, DBusMessage *msg) { + const char *target = NULL; + + if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) + return false; + + xcb_window_t wid = XCB_NONE; + + // Find window by client window + if (!strcmp("client", target)) { + cdbus_window_t client = XCB_NONE; + if (!cdbus_msg_get_arg(msg, 1, CDBUS_TYPE_WINDOW, &client)) + return false; + auto w = find_toplevel(ps, client); + if (w) { + wid = w->base.id; + } + } + // Find focused window + else if (!strcmp("focused", target)) { + if (ps->active_win && ps->active_win->state != WSTATE_UNMAPPED) { + wid = ps->active_win->base.id; + } + } else { + log_error(CDBUS_ERROR_BADTGT_S, target); + cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + + return true; + } + + cdbus_reply_wid(ps, msg, wid); + + return true; +} + +/** + * Process a opts_get D-Bus request. + */ +static bool cdbus_process_opts_get(session_t *ps, DBusMessage *msg) { + const char *target = NULL; + + if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) + return false; + +#define cdbus_m_opts_get_do(tgt, apdarg_func) \ + if (!strcmp(#tgt, target)) { \ + apdarg_func(ps, msg, ps->o.tgt); \ + return true; \ + } + +#define cdbus_m_opts_get_stub(tgt, apdarg_func, ret) \ + if (!strcmp(#tgt, target)) { \ + apdarg_func(ps, msg, ret); \ + return true; \ + } + + // version + if (!strcmp("version", target)) { + cdbus_reply_string(ps, msg, COMPTON_VERSION); + return true; + } + + // pid + if (!strcmp("pid", target)) { + cdbus_reply_int32(ps, msg, getpid()); + return true; + } + + // display + if (!strcmp("display", target)) { + cdbus_reply_string(ps, msg, DisplayString(ps->dpy)); + return true; + } + + cdbus_m_opts_get_stub(config_file, cdbus_reply_string, "Unknown"); + cdbus_m_opts_get_do(write_pid_path, cdbus_reply_string); + cdbus_m_opts_get_do(mark_wmwin_focused, cdbus_reply_bool); + cdbus_m_opts_get_do(mark_ovredir_focused, cdbus_reply_bool); + cdbus_m_opts_get_do(detect_rounded_corners, cdbus_reply_bool); + cdbus_m_opts_get_stub(paint_on_overlay, cdbus_reply_bool, ps->overlay != XCB_NONE); + // paint_on_overlay_id: Get ID of the X composite overlay window + if (!strcmp("paint_on_overlay_id", target)) { + cdbus_reply_uint32(ps, msg, ps->overlay); + return true; + } + cdbus_m_opts_get_do(unredir_if_possible, cdbus_reply_bool); + cdbus_m_opts_get_do(unredir_if_possible_delay, cdbus_reply_int32l); + cdbus_m_opts_get_do(redirected_force, cdbus_reply_enum); + cdbus_m_opts_get_do(stoppaint_force, cdbus_reply_enum); + cdbus_m_opts_get_do(logpath, cdbus_reply_string); + + cdbus_m_opts_get_do(refresh_rate, cdbus_reply_int32); + cdbus_m_opts_get_do(sw_opti, cdbus_reply_bool); + cdbus_m_opts_get_do(vsync, cdbus_reply_bool); + if (!strcmp("backend", target)) { + assert(ps->o.backend < sizeof(BACKEND_STRS) / sizeof(BACKEND_STRS[0])); + cdbus_reply_string(ps, msg, BACKEND_STRS[ps->o.backend]); + return true; + } + + cdbus_m_opts_get_do(shadow_red, cdbus_reply_double); + cdbus_m_opts_get_do(shadow_green, cdbus_reply_double); + cdbus_m_opts_get_do(shadow_blue, cdbus_reply_double); + cdbus_m_opts_get_do(shadow_radius, cdbus_reply_int32); + cdbus_m_opts_get_do(shadow_offset_x, cdbus_reply_int32); + cdbus_m_opts_get_do(shadow_offset_y, cdbus_reply_int32); + cdbus_m_opts_get_do(shadow_opacity, cdbus_reply_double); + cdbus_m_opts_get_do(xinerama_shadow_crop, cdbus_reply_bool); + + cdbus_m_opts_get_do(fade_delta, cdbus_reply_int32); + cdbus_m_opts_get_do(fade_in_step, cdbus_reply_double); + cdbus_m_opts_get_do(fade_out_step, cdbus_reply_double); + cdbus_m_opts_get_do(no_fading_openclose, cdbus_reply_bool); + + cdbus_m_opts_get_do(blur_method, cdbus_reply_bool); + cdbus_m_opts_get_do(blur_background_frame, cdbus_reply_bool); + cdbus_m_opts_get_do(blur_background_fixed, cdbus_reply_bool); + + cdbus_m_opts_get_do(inactive_dim, cdbus_reply_double); + cdbus_m_opts_get_do(inactive_dim_fixed, cdbus_reply_bool); + + cdbus_m_opts_get_do(max_brightness, cdbus_reply_double); + + cdbus_m_opts_get_do(use_ewmh_active_win, cdbus_reply_bool); + cdbus_m_opts_get_do(detect_transient, cdbus_reply_bool); + cdbus_m_opts_get_do(detect_client_leader, cdbus_reply_bool); + cdbus_m_opts_get_do(use_damage, cdbus_reply_bool); + +#ifdef CONFIG_OPENGL + cdbus_m_opts_get_do(glx_no_stencil, cdbus_reply_bool); + cdbus_m_opts_get_do(glx_no_rebind_pixmap, cdbus_reply_bool); +#endif + +#undef cdbus_m_opts_get_do +#undef cdbus_m_opts_get_stub + + log_error(CDBUS_ERROR_BADTGT_S, target); + cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + + return true; +} + +// XXX Remove this after header clean up +void queue_redraw(session_t *ps); + +/** + * Process a opts_set D-Bus request. + */ +static bool cdbus_process_opts_set(session_t *ps, DBusMessage *msg) { + const char *target = NULL; + + if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) + return false; + +#define cdbus_m_opts_set_do(tgt, type, real_type) \ + if (!strcmp(#tgt, target)) { \ + real_type val; \ + if (!cdbus_msg_get_arg(msg, 1, type, &val)) \ + return false; \ + ps->o.tgt = val; \ + goto cdbus_process_opts_set_success; \ + } + + // fade_delta + if (!strcmp("fade_delta", target)) { + int32_t val = 0; + if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_INT32, &val)) { + return false; + } + if (val <= 0) { + return false; + } + ps->o.fade_delta = max2(val, 1); + goto cdbus_process_opts_set_success; + } + + // fade_in_step + if (!strcmp("fade_in_step", target)) { + double val = 0.0; + if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_DOUBLE, &val)) + return false; + ps->o.fade_in_step = normalize_d(val); + goto cdbus_process_opts_set_success; + } + + // fade_out_step + if (!strcmp("fade_out_step", target)) { + double val = 0.0; + if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_DOUBLE, &val)) + return false; + ps->o.fade_out_step = normalize_d(val); + goto cdbus_process_opts_set_success; + } + + // no_fading_openclose + if (!strcmp("no_fading_openclose", target)) { + dbus_bool_t val = FALSE; + if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_BOOLEAN, &val)) + return false; + opts_set_no_fading_openclose(ps, val); + goto cdbus_process_opts_set_success; + } + + // unredir_if_possible + if (!strcmp("unredir_if_possible", target)) { + dbus_bool_t val = FALSE; + if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_BOOLEAN, &val)) + return false; + if (ps->o.unredir_if_possible != val) { + ps->o.unredir_if_possible = val; + queue_redraw(ps); + } + goto cdbus_process_opts_set_success; + } + + // clear_shadow + if (!strcmp("clear_shadow", target)) { + goto cdbus_process_opts_set_success; + } + + // track_focus + if (!strcmp("track_focus", target)) { + goto cdbus_process_opts_set_success; + } + + // redirected_force + if (!strcmp("redirected_force", target)) { + cdbus_enum_t val = UNSET; + if (!cdbus_msg_get_arg(msg, 1, CDBUS_TYPE_ENUM, &val)) + return false; + ps->o.redirected_force = val; + force_repaint(ps); + goto cdbus_process_opts_set_success; + } + + // stoppaint_force + cdbus_m_opts_set_do(stoppaint_force, CDBUS_TYPE_ENUM, cdbus_enum_t); + +#undef cdbus_m_opts_set_do + + log_error(CDBUS_ERROR_BADTGT_S, target); + cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + + return true; + +cdbus_process_opts_set_success: + if (!dbus_message_get_no_reply(msg)) + cdbus_reply_bool(ps, msg, true); + return true; +} + +/** + * Process an Introspect D-Bus request. + */ +static bool cdbus_process_introspect(session_t *ps, DBusMessage *msg) { + static const char *str_introspect = + "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection " + "1.0//EN\"\n" + " \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n" + "<node name='" CDBUS_OBJECT_NAME "'>\n" + " <interface name='org.freedesktop.DBus.Introspectable'>\n" + " <method name='Introspect'>\n" + " <arg name='data' direction='out' type='s' />\n" + " </method>\n" + " </interface>\n" + " <interface name='org.freedesktop.DBus.Peer'>\n" + " <method name='Ping' />\n" + " <method name='GetMachineId'>\n" + " <arg name='machine_uuid' direction='out' type='s' />\n" + " </method>\n" + " </interface>\n" + " <interface name='" CDBUS_INTERFACE_NAME "'>\n" + " <signal name='win_added'>\n" + " <arg name='wid' type='" CDBUS_TYPE_WINDOW_STR "'/>\n" + " </signal>\n" + " <signal name='win_destroyed'>\n" + " <arg name='wid' type='" CDBUS_TYPE_WINDOW_STR "'/>\n" + " </signal>\n" + " <signal name='win_mapped'>\n" + " <arg name='wid' type='" CDBUS_TYPE_WINDOW_STR "'/>\n" + " </signal>\n" + " <signal name='win_unmapped'>\n" + " <arg name='wid' type='" CDBUS_TYPE_WINDOW_STR "'/>\n" + " </signal>\n" + " <signal name='win_focusin'>\n" + " <arg name='wid' type='" CDBUS_TYPE_WINDOW_STR "'/>\n" + " </signal>\n" + " <signal name='win_focusout'>\n" + " <arg name='wid' type='" CDBUS_TYPE_WINDOW_STR "'/>\n" + " </signal>\n" + " <method name='reset' />\n" + " <method name='repaint' />\n" + " </interface>\n" + "</node>\n"; + + cdbus_reply_string(ps, msg, str_introspect); + + return true; +} +///@} + +/** + * Process a message from D-Bus. + */ +static DBusHandlerResult +cdbus_process(DBusConnection *c attr_unused, DBusMessage *msg, void *ud) { + session_t *ps = ud; + bool handled = false; + +#define cdbus_m_ismethod(method) \ + dbus_message_is_method_call(msg, CDBUS_INTERFACE_NAME, method) + + if (cdbus_m_ismethod("reset")) { + log_info("picom is resetting..."); + ev_break(ps->loop, EVBREAK_ALL); + if (!dbus_message_get_no_reply(msg)) + cdbus_reply_bool(ps, msg, true); + handled = true; + } else if (cdbus_m_ismethod("repaint")) { + force_repaint(ps); + if (!dbus_message_get_no_reply(msg)) + cdbus_reply_bool(ps, msg, true); + handled = true; + } else if (cdbus_m_ismethod("list_win")) { + handled = cdbus_process_list_win(ps, msg); + } else if (cdbus_m_ismethod("win_get")) { + handled = cdbus_process_win_get(ps, msg); + } else if (cdbus_m_ismethod("win_set")) { + handled = cdbus_process_win_set(ps, msg); + } else if (cdbus_m_ismethod("find_win")) { + handled = cdbus_process_find_win(ps, msg); + } else if (cdbus_m_ismethod("opts_get")) { + handled = cdbus_process_opts_get(ps, msg); + } else if (cdbus_m_ismethod("opts_set")) { + handled = cdbus_process_opts_set(ps, msg); + } +#undef cdbus_m_ismethod + else if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Introspectable", + "Introspect")) { + handled = cdbus_process_introspect(ps, msg); + } else if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Peer", "Ping")) { + cdbus_reply(ps, msg, NULL, NULL); + handled = true; + } else if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Peer", + "GetMachineId")) { + char *uuid = dbus_get_local_machine_id(); + if (uuid) { + cdbus_reply_string(ps, msg, uuid); + dbus_free(uuid); + handled = true; + } + } else if (dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameAcquired") || + dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameLost")) { + handled = true; + } else { + if (DBUS_MESSAGE_TYPE_ERROR == dbus_message_get_type(msg)) { + log_error( + "Error message of path \"%s\" " + "interface \"%s\", member \"%s\", error \"%s\"", + dbus_message_get_path(msg), dbus_message_get_interface(msg), + dbus_message_get_member(msg), dbus_message_get_error_name(msg)); + } else { + log_error("Illegal message of type \"%s\", path \"%s\" " + "interface \"%s\", member \"%s\"", + cdbus_repr_msgtype(msg), dbus_message_get_path(msg), + dbus_message_get_interface(msg), + dbus_message_get_member(msg)); + } + if (DBUS_MESSAGE_TYPE_METHOD_CALL == dbus_message_get_type(msg) && + !dbus_message_get_no_reply(msg)) + cdbus_reply_err(ps, msg, CDBUS_ERROR_BADMSG, CDBUS_ERROR_BADMSG_S); + handled = true; + } + + // If the message could not be processed, and an reply is expected, return + // an empty reply. + if (!handled && DBUS_MESSAGE_TYPE_METHOD_CALL == dbus_message_get_type(msg) && + !dbus_message_get_no_reply(msg)) { + cdbus_reply_err(ps, msg, CDBUS_ERROR_UNKNOWN, CDBUS_ERROR_UNKNOWN_S); + handled = true; + } + + return handled ? DBUS_HANDLER_RESULT_HANDLED : DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +/** @name Core callbacks + */ +///@{ +void cdbus_ev_win_added(session_t *ps, struct win *w) { + struct cdbus_data *cd = ps->dbus_data; + if (cd->dbus_conn) + cdbus_signal_wid(ps, "win_added", w->id); +} + +void cdbus_ev_win_destroyed(session_t *ps, struct win *w) { + struct cdbus_data *cd = ps->dbus_data; + if (cd->dbus_conn) + cdbus_signal_wid(ps, "win_destroyed", w->id); +} + +void cdbus_ev_win_mapped(session_t *ps, struct win *w) { + struct cdbus_data *cd = ps->dbus_data; + if (cd->dbus_conn) + cdbus_signal_wid(ps, "win_mapped", w->id); +} + +void cdbus_ev_win_unmapped(session_t *ps, struct win *w) { + struct cdbus_data *cd = ps->dbus_data; + if (cd->dbus_conn) + cdbus_signal_wid(ps, "win_unmapped", w->id); +} + +void cdbus_ev_win_focusout(session_t *ps, struct win *w) { + struct cdbus_data *cd = ps->dbus_data; + if (cd->dbus_conn) + cdbus_signal_wid(ps, "win_focusout", w->id); +} + +void cdbus_ev_win_focusin(session_t *ps, struct win *w) { + struct cdbus_data *cd = ps->dbus_data; + if (cd->dbus_conn) + cdbus_signal_wid(ps, "win_focusin", w->id); +} +//!@} diff --git a/src/dbus.h b/src/dbus.h new file mode 100644 index 0000000..54a58af --- /dev/null +++ b/src/dbus.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +/* + * Compton - a compositor for X11 + * + * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard + * + * Copyright (c) 2011-2013, Christopher Jeffrey + * See LICENSE-mit for more information. + * + */ + +#include <stdbool.h> + +#include <dbus/dbus.h> + +typedef struct session session_t; +struct win; + +/** + * Return a string representation of a D-Bus message type. + */ +static inline const char *cdbus_repr_msgtype(DBusMessage *msg) { + return dbus_message_type_to_string(dbus_message_get_type(msg)); +} + +/** + * Initialize D-Bus connection. + */ +bool cdbus_init(session_t *ps, const char *uniq_name); + +/** + * Destroy D-Bus connection. + */ +void cdbus_destroy(session_t *ps); + +/// Generate dbus win_added signal +void cdbus_ev_win_added(session_t *ps, struct win *w); + +/// Generate dbus win_destroyed signal +void cdbus_ev_win_destroyed(session_t *ps, struct win *w); + +/// Generate dbus win_mapped signal +void cdbus_ev_win_mapped(session_t *ps, struct win *w); + +/// Generate dbus win_unmapped signal +void cdbus_ev_win_unmapped(session_t *ps, struct win *w); + +/// Generate dbus win_focusout signal +void cdbus_ev_win_focusout(session_t *ps, struct win *w); + +/// Generate dbus win_focusin signal +void cdbus_ev_win_focusin(session_t *ps, struct win *w); + +// vim: set noet sw=8 ts=8 : diff --git a/src/diagnostic.c b/src/diagnostic.c new file mode 100644 index 0000000..d275b1a --- /dev/null +++ b/src/diagnostic.c @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> + +#include <stdio.h> +#include <xcb/xcb.h> +#include <xcb/composite.h> + +#include "backend/driver.h" +#include "diagnostic.h" +#include "config.h" +#include "picom.h" +#include "common.h" + +void print_diagnostics(session_t *ps, const char *config_file, bool compositor_running) { + printf("**Version:** " COMPTON_VERSION "\n"); + //printf("**CFLAGS:** %s\n", "??"); + printf("\n### Extensions:\n\n"); + printf("* Shape: %s\n", ps->shape_exists ? "Yes" : "No"); + printf("* XRandR: %s\n", ps->randr_exists ? "Yes" : "No"); + printf("* Present: %s\n", ps->present_exists ? "Present" : "Not Present"); + printf("\n### Misc:\n\n"); + printf("* Use Overlay: %s\n", ps->overlay != XCB_NONE ? "Yes" : "No"); + if (ps->overlay == XCB_NONE) { + if (compositor_running) { + printf(" (Another compositor is already running)\n"); + } else if (session_redirection_mode(ps) != XCB_COMPOSITE_REDIRECT_MANUAL) { + printf(" (Not in manual redirection mode)\n"); + } else { + printf("\n"); + } + } +#ifdef __FAST_MATH__ + printf("* Fast Math: Yes\n"); +#endif + printf("* Config file used: %s\n", config_file ?: "None"); + printf("\n### Drivers (inaccurate):\n\n"); + print_drivers(ps->drivers); + + for (int i = 0; i < NUM_BKEND; i++) { + if (backend_list[i] && backend_list[i]->diagnostics) { + printf("\n### Backend: %s\n\n", BACKEND_STRS[i]); + auto data = backend_list[i]->init(ps); + if (!data) { + printf(" Cannot initialize this backend\n"); + } else { + backend_list[i]->diagnostics(data); + backend_list[i]->deinit(data); + } + } + } +} + +// vim: set noet sw=8 ts=8 : diff --git a/src/diagnostic.h b/src/diagnostic.h new file mode 100644 index 0000000..c958589 --- /dev/null +++ b/src/diagnostic.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> + +#pragma once +#include <stdbool.h> + +typedef struct session session_t; + +void print_diagnostics(session_t *, const char *config_file, bool compositor_running); diff --git a/src/err.h b/src/err.h new file mode 100644 index 0000000..f989bf9 --- /dev/null +++ b/src/err.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2019 Yuxuan Shui <[email protected]> + +#pragma once +#include <stdbool.h> +#include <stdint.h> +#include "compiler.h" + +// Functions for error reporting, adopted from Linux + +// INFO in user space we can probably be more liberal about what pointer we consider +// error. e.g. In x86_64 Linux, all addresses with the highest bit set is invalid in user +// space. +#define MAX_ERRNO 4095 + +static inline void *must_use ERR_PTR(intptr_t err) { + return (void *)err; +} + +static inline intptr_t must_use PTR_ERR(void *ptr) { + return (intptr_t)ptr; +} + +static inline bool must_use IS_ERR(void *ptr) { + return unlikely((uintptr_t)ptr > (uintptr_t)-MAX_ERRNO); +} + +static inline bool must_use IS_ERR_OR_NULL(void *ptr) { + return unlikely(!ptr) || IS_ERR(ptr); +} + +static inline intptr_t must_use PTR_ERR_OR_ZERO(void *ptr) { + if (IS_ERR(ptr)) { + return PTR_ERR(ptr); + } + return 0; +} diff --git a/src/event.c b/src/event.c new file mode 100644 index 0000000..5e4017f --- /dev/null +++ b/src/event.c @@ -0,0 +1,757 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2019, Yuxuan Shui <[email protected]> + +#include <stdio.h> + +#include <X11/Xlibint.h> +#include <X11/extensions/sync.h> +#include <xcb/damage.h> +#include <xcb/randr.h> + +#include "atom.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "event.h" +#include "log.h" +#include "picom.h" +#include "region.h" +#include "utils.h" +#include "win.h" +#include "x.h" + +/// Event handling with X is complicated. Handling events with other events possibly +/// in-flight is no good. Because your internal state won't be up to date. Also, querying +/// the server while events are in-flight is not good. Because events later in the queue +/// might container information you are querying. Thus those events will cause you to do +/// unnecessary updates even when you already have the latest information (remember, you +/// made the query when those events were already in the queue. so the reply you got is +/// more up-to-date than the events). Also, handling events when other client are making +/// concurrent requests is not good. Because the server states are changing without you +/// knowning them. This is super racy, and can cause lots of potential problems. +/// +/// All of above mandates we do these things: +/// 1. Grab server when handling events +/// 2. Make sure the event queue is empty before we make any query to the server +/// +/// Notice (2) has a dependency circle. To handle events, you sometimes need to make +/// queries. But to make queries you have to first handle events. +/// +/// To break that circle, we split all event handling into top and bottom halves. The +/// bottom half will just look at the event itself, update as much state as they can +/// without making queries, then queue up necessary works need to be done by the top half. +/// The top half will do all the other necessary updates. Before entering the top half, we +/// grab the server and make sure the event queue is empty. +/// +/// When top half finished, we enter the render stage, where no server state should be +/// queried. All rendering should be done with our internal knowledge of the server state. +/// + +// TODO(yshui) the things described above + +/** + * Get a window's name from window ID. + */ +static inline const char *ev_window_name(session_t *ps, xcb_window_t wid) { + char *name = ""; + if (wid) { + name = "(Failed to get title)"; + if (ps->root == wid) { + name = "(Root window)"; + } else if (ps->overlay == wid) { + name = "(Overlay)"; + } else { + auto w = find_managed_win(ps, wid); + if (!w) { + w = find_toplevel(ps, wid); + } + + if (w && w->name) { + name = w->name; + } + } + } + return name; +} + +static inline xcb_window_t attr_pure ev_window(session_t *ps, xcb_generic_event_t *ev) { + switch (ev->response_type) { + case FocusIn: + case FocusOut: return ((xcb_focus_in_event_t *)ev)->event; + case CreateNotify: return ((xcb_create_notify_event_t *)ev)->window; + case ConfigureNotify: return ((xcb_configure_notify_event_t *)ev)->window; + case DestroyNotify: return ((xcb_destroy_notify_event_t *)ev)->window; + case MapNotify: return ((xcb_map_notify_event_t *)ev)->window; + case UnmapNotify: return ((xcb_unmap_notify_event_t *)ev)->window; + case ReparentNotify: return ((xcb_reparent_notify_event_t *)ev)->window; + case CirculateNotify: return ((xcb_circulate_notify_event_t *)ev)->window; + case Expose: return ((xcb_expose_event_t *)ev)->window; + case PropertyNotify: return ((xcb_property_notify_event_t *)ev)->window; + case ClientMessage: return ((xcb_client_message_event_t *)ev)->window; + default: + if (ps->damage_event + XCB_DAMAGE_NOTIFY == ev->response_type) { + return ((xcb_damage_notify_event_t *)ev)->drawable; + } + + if (ps->shape_exists && ev->response_type == ps->shape_event) { + return ((xcb_shape_notify_event_t *)ev)->affected_window; + } + + return 0; + } +} + +#define CASESTRRET(s) \ + case s: return #s; + +static inline const char *ev_name(session_t *ps, xcb_generic_event_t *ev) { + static char buf[128]; + switch (ev->response_type & 0x7f) { + CASESTRRET(FocusIn); + CASESTRRET(FocusOut); + CASESTRRET(CreateNotify); + CASESTRRET(ConfigureNotify); + CASESTRRET(DestroyNotify); + CASESTRRET(MapNotify); + CASESTRRET(UnmapNotify); + CASESTRRET(ReparentNotify); + CASESTRRET(CirculateNotify); + CASESTRRET(Expose); + CASESTRRET(PropertyNotify); + CASESTRRET(ClientMessage); + } + + if (ps->damage_event + XCB_DAMAGE_NOTIFY == ev->response_type) + return "Damage"; + + if (ps->shape_exists && ev->response_type == ps->shape_event) + return "ShapeNotify"; + + if (ps->xsync_exists) { + int o = ev->response_type - ps->xsync_event; + switch (o) { + CASESTRRET(XSyncCounterNotify); + CASESTRRET(XSyncAlarmNotify); + } + } + + sprintf(buf, "Event %d", ev->response_type); + + return buf; +} + +static inline const char *attr_pure ev_focus_mode_name(xcb_focus_in_event_t *ev) { + switch (ev->mode) { + CASESTRRET(NotifyNormal); + CASESTRRET(NotifyWhileGrabbed); + CASESTRRET(NotifyGrab); + CASESTRRET(NotifyUngrab); + } + + return "Unknown"; +} + +static inline const char *attr_pure ev_focus_detail_name(xcb_focus_in_event_t *ev) { + switch (ev->detail) { + CASESTRRET(NotifyAncestor); + CASESTRRET(NotifyVirtual); + CASESTRRET(NotifyInferior); + CASESTRRET(NotifyNonlinear); + CASESTRRET(NotifyNonlinearVirtual); + CASESTRRET(NotifyPointer); + CASESTRRET(NotifyPointerRoot); + CASESTRRET(NotifyDetailNone); + } + + return "Unknown"; +} + +#undef CASESTRRET + +static inline void ev_focus_in(session_t *ps, xcb_focus_in_event_t *ev) { + log_debug("{ mode: %s, detail: %s }\n", ev_focus_mode_name(ev), + ev_focus_detail_name(ev)); + ps->pending_updates = true; +} + +static inline void ev_focus_out(session_t *ps, xcb_focus_out_event_t *ev) { + log_debug("{ mode: %s, detail: %s }\n", ev_focus_mode_name(ev), + ev_focus_detail_name(ev)); + ps->pending_updates = true; +} + +static inline void ev_create_notify(session_t *ps, xcb_create_notify_event_t *ev) { + if (ev->parent == ps->root) { + add_win_top(ps, ev->window); + } +} + +/// Handle configure event of a regular window +static void configure_win(session_t *ps, xcb_configure_notify_event_t *ce) { + auto w = find_win(ps, ce->window); + + if (!w) { + return; + } + + if (!w->managed) { + restack_above(ps, w, ce->above_sibling); + return; + } + + auto mw = (struct managed_win *)w; + + restack_above(ps, w, ce->above_sibling); + + // We check against pending_g here, because there might have been multiple + // configure notifies in this cycle, or the window could receive multiple updates + // while it's unmapped. + bool position_changed = mw->pending_g.x != ce->x || mw->pending_g.y != ce->y; + bool size_changed = mw->pending_g.width != ce->width || + mw->pending_g.height != ce->height || + mw->pending_g.border_width != ce->border_width; + if (position_changed || size_changed) { + // Queue pending updates + win_set_flags(mw, WIN_FLAGS_FACTOR_CHANGED); + // TODO(yshui) don't set pending_updates if the window is not + // visible/mapped + ps->pending_updates = true; + + // At least one of the following if's is true + if (position_changed) { + log_trace("Window position changed, %dx%d -> %dx%d", mw->g.x, + mw->g.y, ce->x, ce->y); + mw->pending_g.x = ce->x; + mw->pending_g.y = ce->y; + win_set_flags(mw, WIN_FLAGS_POSITION_STALE); + } + + if (size_changed) { + log_trace("Window size changed, %dx%d -> %dx%d", mw->g.width, + mw->g.height, ce->width, ce->height); + mw->pending_g.width = ce->width; + mw->pending_g.height = ce->height; + mw->pending_g.border_width = ce->border_width; + win_set_flags(mw, WIN_FLAGS_SIZE_STALE); + } + + // Recalculate which screen this window is on + win_update_screen(ps->xinerama_nscrs, ps->xinerama_scr_regs, mw); + } + + // override_redirect flag cannot be changed after window creation, as far + // as I know, so there's no point to re-match windows here. + mw->a.override_redirect = ce->override_redirect; +} + +static inline void ev_configure_notify(session_t *ps, xcb_configure_notify_event_t *ev) { + log_debug("{ send_event: %d, id: %#010x, above: %#010x, override_redirect: %d }", + ev->event, ev->window, ev->above_sibling, ev->override_redirect); + if (ev->window == ps->root) { + set_root_flags(ps, ROOT_FLAGS_CONFIGURED); + } else { + configure_win(ps, ev); + } +} + +static inline void ev_destroy_notify(session_t *ps, xcb_destroy_notify_event_t *ev) { + auto w = find_win(ps, ev->window); + auto mw = find_toplevel(ps, ev->window); + if (mw && mw->client_win == mw->base.id) { + // We only want _real_ frame window + assert(&mw->base == w); + mw = NULL; + } + assert(w == NULL || mw == NULL); + + if (w != NULL) { + auto _ attr_unused = destroy_win_start(ps, w); + } else if (mw != NULL) { + win_unmark_client(ps, mw); + win_set_flags(mw, WIN_FLAGS_CLIENT_STALE); + ps->pending_updates = true; + } else { + log_debug("Received a destroy notify from an unknown window, %#010x", + ev->window); + } +} + +static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { + // Unmap overlay window if it got mapped but we are currently not + // in redirected state. + if (ps->overlay && ev->window == ps->overlay && !ps->redirected) { + log_debug("Overlay is mapped while we are not redirected"); + auto e = xcb_request_check(ps->c, xcb_unmap_window(ps->c, ps->overlay)); + if (e) { + log_error("Failed to unmap the overlay window"); + free(e); + } + // We don't track the overlay window, so we can return + return; + } + + auto w = find_managed_win(ps, ev->window); + if (!w) { + return; + } + + win_set_flags(w, WIN_FLAGS_MAPPED); + + // FocusIn/Out may be ignored when the window is unmapped, so we must + // recheck focus here + ps->pending_updates = true; // to update focus +} + +static inline void ev_unmap_notify(session_t *ps, xcb_unmap_notify_event_t *ev) { + auto w = find_managed_win(ps, ev->window); + if (w) { + unmap_win_start(ps, w); + } +} + +static inline void ev_reparent_notify(session_t *ps, xcb_reparent_notify_event_t *ev) { + log_debug("Window %#010x has new parent: %#010x, override_redirect: %d", + ev->window, ev->parent, ev->override_redirect); + auto w_top = find_toplevel(ps, ev->window); + if (w_top) { + win_unmark_client(ps, w_top); + win_set_flags(w_top, WIN_FLAGS_CLIENT_STALE); + ps->pending_updates = true; + } + + if (ev->parent == ps->root) { + // X will generate reparent notifiy even if the parent didn't actually + // change (i.e. reparent again to current parent). So we check if that's + // the case + auto w = find_win(ps, ev->window); + if (w) { + // This window has already been reparented to root before, + // so we don't need to create a new window for it, we just need to + // move it to the top + restack_top(ps, w); + } else { + add_win_top(ps, ev->window); + } + } else { + // otherwise, find and destroy the window first + { + auto w = find_win(ps, ev->window); + if (w) { + auto ret = destroy_win_start(ps, w); + if (!ret && w->managed) { + auto mw = (struct managed_win *)w; + CHECK(win_skip_fading(ps, mw)); + } + } + } + + // Reset event mask in case something wrong happens + xcb_change_window_attributes( + ps->c, ev->window, XCB_CW_EVENT_MASK, + (const uint32_t[]){determine_evmask(ps, ev->window, WIN_EVMODE_UNKNOWN)}); + + if (!wid_has_prop(ps, ev->window, ps->atoms->aWM_STATE)) { + log_debug("Window %#010x doesn't have WM_STATE property, it is " + "probably not a client window. But we will listen for " + "property change in case it gains one.", + ev->window); + xcb_change_window_attributes( + ps->c, ev->window, XCB_CW_EVENT_MASK, + (const uint32_t[]){determine_evmask(ps, ev->window, WIN_EVMODE_UNKNOWN) | + XCB_EVENT_MASK_PROPERTY_CHANGE}); + } else { + auto w_real_top = find_managed_window_or_parent(ps, ev->parent); + if (w_real_top && w_real_top->state != WSTATE_UNMAPPED && + w_real_top->state != WSTATE_UNMAPPING) { + log_debug("Mark window %#010x (%s) as having a stale " + "client", + w_real_top->base.id, w_real_top->name); + win_set_flags(w_real_top, WIN_FLAGS_CLIENT_STALE); + ps->pending_updates = true; + } else { + if (!w_real_top) + log_debug("parent %#010x not found", ev->parent); + else { + // Window is not currently mapped, unmark its + // client to trigger a client recheck when it is + // mapped later. + win_unmark_client(ps, w_real_top); + log_debug("parent %#010x (%s) is in state %d", + w_real_top->base.id, w_real_top->name, + w_real_top->state); + } + } + } + } +} + +static inline void ev_circulate_notify(session_t *ps, xcb_circulate_notify_event_t *ev) { + auto w = find_win(ps, ev->window); + + if (!w) + return; + + if (ev->place == PlaceOnTop) { + restack_top(ps, w); + } else { + restack_bottom(ps, w); + } +} + +static inline void expose_root(session_t *ps, const rect_t *rects, int nrects) { + region_t region; + pixman_region32_init_rects(®ion, rects, nrects); + add_damage(ps, ®ion); + pixman_region32_fini(®ion); +} + +static inline void ev_expose(session_t *ps, xcb_expose_event_t *ev) { + if (ev->window == ps->root || (ps->overlay && ev->window == ps->overlay)) { + int more = ev->count + 1; + if (ps->n_expose == ps->size_expose) { + if (ps->expose_rects) { + ps->expose_rects = + crealloc(ps->expose_rects, ps->size_expose + more); + ps->size_expose += more; + } else { + ps->expose_rects = ccalloc(more, rect_t); + ps->size_expose = more; + } + } + + ps->expose_rects[ps->n_expose].x1 = ev->x; + ps->expose_rects[ps->n_expose].y1 = ev->y; + ps->expose_rects[ps->n_expose].x2 = ev->x + ev->width; + ps->expose_rects[ps->n_expose].y2 = ev->y + ev->height; + ps->n_expose++; + + if (ev->count == 0) { + expose_root(ps, ps->expose_rects, ps->n_expose); + ps->n_expose = 0; + } + } +} + +static inline void ev_property_notify(session_t *ps, xcb_property_notify_event_t *ev) { + if (unlikely(log_get_level_tls() <= LOG_LEVEL_TRACE)) { + // Print out changed atom + xcb_get_atom_name_reply_t *reply = + xcb_get_atom_name_reply(ps->c, xcb_get_atom_name(ps->c, ev->atom), NULL); + const char *name = "?"; + int name_len = 1; + if (reply) { + name = xcb_get_atom_name_name(reply); + name_len = xcb_get_atom_name_name_length(reply); + } + + log_debug("{ atom = %.*s }", name_len, name); + free(reply); + } + + if (ps->root == ev->window) { + // If desktop number property changes + if (ev->atom == ps->atoms->a_NET_CURRENT_DESKTOP) { + auto prop = x_get_prop(ps->c, ps->root, ps->atoms->a_NET_CURRENT_DESKTOP, + 1L, XCB_ATOM_CARDINAL, 32); + + if (prop.nitems) { + ps->root_desktop_switch_direction = ((int)*prop.c32) - ps->root_desktop_num; + ps->root_desktop_num = (int)*prop.c32; + } + } + + if (ps->o.use_ewmh_active_win && ps->atoms->a_NET_ACTIVE_WINDOW == ev->atom) { + // to update focus + ps->pending_updates = true; + } else { + // Destroy the root "image" if the wallpaper probably changed + if (x_is_root_back_pixmap_atom(ps->atoms, ev->atom)) { + root_damaged(ps); + } + } + + // Unconcerned about any other proprties on root window + return; + } + + ps->pending_updates = true; + // If WM_STATE changes + if (ev->atom == ps->atoms->aWM_STATE) { + // Check whether it could be a client window + if (!find_toplevel(ps, ev->window)) { + // Reset event mask anyway + xcb_change_window_attributes(ps->c, ev->window, XCB_CW_EVENT_MASK, + (const uint32_t[]){determine_evmask( + ps, ev->window, WIN_EVMODE_UNKNOWN)}); + + auto w_top = find_managed_window_or_parent(ps, ev->window); + // ev->window might have not been managed yet, in that case w_top + // would be NULL. + if (w_top) { + win_set_flags(w_top, WIN_FLAGS_CLIENT_STALE); + } + } + return; + } + + // If _NET_WM_WINDOW_TYPE changes... God knows why this would happen, but + // there are always some stupid applications. (#144) + if (ev->atom == ps->atoms->a_NET_WM_WINDOW_TYPE) { + struct managed_win *w = NULL; + if ((w = find_toplevel(ps, ev->window))) { + win_set_property_stale(w, ev->atom); + } + } + + if (ev->atom == ps->atoms->a_NET_WM_BYPASS_COMPOSITOR) { + // Unnecessay until we remove the queue_redraw in ev_handle + queue_redraw(ps); + } + + // If _NET_WM_OPACITY changes + if (ev->atom == ps->atoms->a_NET_WM_WINDOW_OPACITY) { + auto w = find_managed_win(ps, ev->window) ?: find_toplevel(ps, ev->window); + if (w) { + win_set_property_stale(w, ev->atom); + } + } + + // If frame extents property changes + if (ev->atom == ps->atoms->a_NET_FRAME_EXTENTS) { + auto w = find_toplevel(ps, ev->window); + if (w) { + win_set_property_stale(w, ev->atom); + } + } + + // If name changes + if (ps->atoms->aWM_NAME == ev->atom || ps->atoms->a_NET_WM_NAME == ev->atom) { + auto w = find_toplevel(ps, ev->window); + if (w) { + win_set_property_stale(w, ev->atom); + } + } + + // If class changes + if (ps->atoms->aWM_CLASS == ev->atom) { + auto w = find_toplevel(ps, ev->window); + if (w) { + win_set_property_stale(w, ev->atom); + } + } + + // If role changes + if (ps->atoms->aWM_WINDOW_ROLE == ev->atom) { + auto w = find_toplevel(ps, ev->window); + if (w) { + win_set_property_stale(w, ev->atom); + } + } + + // If _COMPTON_SHADOW changes + if (ps->atoms->a_COMPTON_SHADOW == ev->atom) { + auto w = find_managed_win(ps, ev->window); + if (w) { + win_set_property_stale(w, ev->atom); + } + } + + // If a leader property changes + if ((ps->o.detect_transient && ps->atoms->aWM_TRANSIENT_FOR == ev->atom) || + (ps->o.detect_client_leader && ps->atoms->aWM_CLIENT_LEADER == ev->atom)) { + auto w = find_toplevel(ps, ev->window); + if (w) { + win_set_property_stale(w, ev->atom); + } + } + + // Check for other atoms we are tracking + for (latom_t *platom = ps->track_atom_lst; platom; platom = platom->next) { + if (platom->atom == ev->atom) { + auto w = find_managed_win(ps, ev->window); + if (!w) { + w = find_toplevel(ps, ev->window); + } + if (w) { + // Set FACTOR_CHANGED so rules based on properties will be + // re-evaluated. + // Don't need to set property stale here, since that only + // concerns properties we explicitly check. + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } + break; + } + } +} + +static inline void repair_win(session_t *ps, struct managed_win *w) { + // Only mapped window can receive damages + assert(win_is_mapped_in_x(w)); + + region_t parts; + pixman_region32_init(&parts); + + if (!w->ever_damaged) { + win_extents(w, &parts); + set_ignore_cookie( + ps, xcb_damage_subtract(ps->c, w->damage, XCB_NONE, XCB_NONE)); + } else { + set_ignore_cookie( + ps, xcb_damage_subtract(ps->c, w->damage, XCB_NONE, ps->damaged_region)); + x_fetch_region(ps->c, ps->damaged_region, &parts); + pixman_region32_translate(&parts, w->g.x + w->g.border_width, + w->g.y + w->g.border_width); + } + + log_trace("Mark window %#010x (%s) as having received damage", w->base.id, w->name); + w->ever_damaged = true; + w->pixmap_damaged = true; + + // Why care about damage when screen is unredirected? + // We will force full-screen repaint on redirection. + if (!ps->redirected) { + pixman_region32_fini(&parts); + return; + } + + // Remove the part in the damage area that could be ignored + if (w->reg_ignore && win_is_region_ignore_valid(ps, w)) { + pixman_region32_subtract(&parts, &parts, w->reg_ignore); + } + + add_damage(ps, &parts); + pixman_region32_fini(&parts); +} + +static inline void ev_damage_notify(session_t *ps, xcb_damage_notify_event_t *de) { + /* + if (ps->root == de->drawable) { + root_damaged(); + return; + } */ + + auto w = find_managed_win(ps, de->drawable); + + if (!w) { + return; + } + + repair_win(ps, w); +} + +static inline void ev_shape_notify(session_t *ps, xcb_shape_notify_event_t *ev) { + auto w = find_managed_win(ps, ev->affected_window); + if (!w || w->a.map_state == XCB_MAP_STATE_UNMAPPED) { + return; + } + + /* + * Empty bounding_shape may indicated an + * unmapped/destroyed window, in which case + * seemingly BadRegion errors would be triggered + * if we attempt to rebuild border_size + */ + // Mark the old bounding shape as damaged + if (!win_check_flags_any(w, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { + region_t tmp = win_get_bounding_shape_global_by_val(w); + add_damage(ps, &tmp); + pixman_region32_fini(&tmp); + } + w->reg_ignore_valid = false; + + win_set_flags(w, WIN_FLAGS_SIZE_STALE); + ps->pending_updates = true; +} + +static inline void +ev_selection_clear(session_t *ps, xcb_selection_clear_event_t attr_unused *ev) { + // The only selection we own is the _NET_WM_CM_Sn selection. + // If we lose that one, we should exit. + log_fatal("Another composite manager started and took the _NET_WM_CM_Sn " + "selection."); + quit(ps); +} + +void ev_handle(session_t *ps, xcb_generic_event_t *ev) { + if ((ev->response_type & 0x7f) != KeymapNotify) { + discard_ignore(ps, ev->full_sequence); + } + + xcb_window_t wid = ev_window(ps, ev); + if (ev->response_type != ps->damage_event + XCB_DAMAGE_NOTIFY) { + log_debug("event %10.10s serial %#010x window %#010x \"%s\"", + ev_name(ps, ev), ev->full_sequence, wid, ev_window_name(ps, wid)); + } else { + log_trace("event %10.10s serial %#010x window %#010x \"%s\"", + ev_name(ps, ev), ev->full_sequence, wid, ev_window_name(ps, wid)); + } + + // Check if a custom XEvent constructor was registered in xlib for this event + // type, and call it discarding the constructed XEvent if any. XESetWireToEvent + // might be used by libraries to intercept messages from the X server e.g. the + // OpenGL lib waiting for DRI2 events. + + // XXX This exists to workaround compton issue #33, #34, #47 + // For even more details, see: + // https://bugs.freedesktop.org/show_bug.cgi?id=35945 + // https://lists.freedesktop.org/archives/xcb/2011-November/007337.html + auto proc = XESetWireToEvent(ps->dpy, ev->response_type, 0); + if (proc) { + XESetWireToEvent(ps->dpy, ev->response_type, proc); + XEvent dummy; + + // Stop Xlib from complaining about lost sequence numbers. + // proc might also just be Xlib internal event processing functions, and + // because they probably won't see all X replies, they will complain about + // missing sequence numbers. + // + // We only need the low 16 bits + ev->sequence = (uint16_t)(LastKnownRequestProcessed(ps->dpy) & 0xffff); + proc(ps->dpy, &dummy, (xEvent *)ev); + } + + // XXX redraw needs to be more fine grained + queue_redraw(ps); + + switch (ev->response_type) { + case FocusIn: ev_focus_in(ps, (xcb_focus_in_event_t *)ev); break; + case FocusOut: ev_focus_out(ps, (xcb_focus_out_event_t *)ev); break; + case CreateNotify: ev_create_notify(ps, (xcb_create_notify_event_t *)ev); break; + case ConfigureNotify: + ev_configure_notify(ps, (xcb_configure_notify_event_t *)ev); + break; + case DestroyNotify: + ev_destroy_notify(ps, (xcb_destroy_notify_event_t *)ev); + break; + case MapNotify: ev_map_notify(ps, (xcb_map_notify_event_t *)ev); break; + case UnmapNotify: ev_unmap_notify(ps, (xcb_unmap_notify_event_t *)ev); break; + case ReparentNotify: + ev_reparent_notify(ps, (xcb_reparent_notify_event_t *)ev); + break; + case CirculateNotify: + ev_circulate_notify(ps, (xcb_circulate_notify_event_t *)ev); + break; + case Expose: ev_expose(ps, (xcb_expose_event_t *)ev); break; + case PropertyNotify: + ev_property_notify(ps, (xcb_property_notify_event_t *)ev); + break; + case SelectionClear: + ev_selection_clear(ps, (xcb_selection_clear_event_t *)ev); + break; + case 0: ev_xcb_error(ps, (xcb_generic_error_t *)ev); break; + default: + if (ps->shape_exists && ev->response_type == ps->shape_event) { + ev_shape_notify(ps, (xcb_shape_notify_event_t *)ev); + break; + } + if (ps->randr_exists && + ev->response_type == (ps->randr_event + XCB_RANDR_SCREEN_CHANGE_NOTIFY)) { + set_root_flags(ps, ROOT_FLAGS_SCREEN_CHANGE); + break; + } + if (ps->damage_event + XCB_DAMAGE_NOTIFY == ev->response_type) { + ev_damage_notify(ps, (xcb_damage_notify_event_t *)ev); + break; + } + } +} diff --git a/src/event.h b/src/event.h new file mode 100644 index 0000000..629dec0 --- /dev/null +++ b/src/event.h @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2019, Yuxuan Shui <[email protected]> + +#include <xcb/xcb.h> + +#include "common.h" + +void ev_handle(session_t *ps, xcb_generic_event_t *ev); diff --git a/src/file_watch.c b/src/file_watch.c new file mode 100644 index 0000000..faa8f68 --- /dev/null +++ b/src/file_watch.c @@ -0,0 +1,188 @@ +#include <errno.h> +#include <string.h> +#ifdef HAS_INOTIFY +#include <sys/inotify.h> +#elif HAS_KQUEUE +// clang-format off +#include <sys/types.h> +// clang-format on +#include <sys/event.h> +#undef EV_ERROR // Avoid clashing with libev's EV_ERROR +#include <fcntl.h> // For O_RDONLY +#include <sys/time.h> // For struct timespec +#include <unistd.h> // For open +#endif + +#include <ev.h> +#include <uthash.h> + +#include "file_watch.h" +#include "list.h" +#include "log.h" +#include "utils.h" + +struct watched_file { + int wd; + void *ud; + file_watch_cb_t cb; + + UT_hash_handle hh; +}; + +struct file_watch_registry { + struct ev_io w; + + struct watched_file *reg; +}; + +static void file_watch_ev_cb(EV_P attr_unused, struct ev_io *w, int revent attr_unused) { + auto fwr = (struct file_watch_registry *)w; + + while (true) { + int wd = -1; +#ifdef HAS_INOTIFY + struct inotify_event inotify_event; + auto ret = read(w->fd, &inotify_event, sizeof(struct inotify_event)); + if (ret < 0) { + if (errno != EAGAIN) { + log_error_errno("Failed to read from inotify fd"); + } + break; + } + wd = inotify_event.wd; +#elif HAS_KQUEUE + struct kevent ev; + struct timespec timeout = {0}; + int ret = kevent(fwr->w.fd, NULL, 0, &ev, 1, &timeout); + if (ret <= 0) { + if (ret < 0) { + log_error_errno("Failed to get kevent"); + } + break; + } + wd = (int)ev.ident; +#else + assert(false); +#endif + + struct watched_file *wf = NULL; + HASH_FIND_INT(fwr->reg, &wd, wf); + if (!wf) { + log_warn("Got notification for a file I didn't watch."); + continue; + } + wf->cb(wf->ud); + } +} + +void *file_watch_init(EV_P) { + log_debug("Starting watching for file changes"); + int fd = -1; +#ifdef HAS_INOTIFY + fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (fd < 0) { + log_error_errno("inotify_init1 failed"); + return NULL; + } +#elif HAS_KQUEUE + fd = kqueue(); + if (fd < 0) { + log_error_errno("Failed to create kqueue"); + return NULL; + } +#else + log_info("No file watching support found on the host system."); + return NULL; +#endif + auto fwr = ccalloc(1, struct file_watch_registry); + ev_io_init(&fwr->w, file_watch_ev_cb, fd, EV_READ); + ev_io_start(EV_A_ & fwr->w); + + return fwr; +} + +void file_watch_destroy(EV_P_ void *_fwr) { + log_debug("Stopping watching for file changes"); + auto fwr = (struct file_watch_registry *)_fwr; + struct watched_file *i, *tmp; + + HASH_ITER(hh, fwr->reg, i, tmp) { + HASH_DEL(fwr->reg, i); +#ifdef HAS_KQUEUE + // kqueue watch descriptors are file descriptors of + // the files we are watching, so we need to close + // them + close(i->wd); +#endif + free(i); + } + + ev_io_stop(EV_A_ & fwr->w); + close(fwr->w.fd); + free(fwr); +} + +bool file_watch_add(void *_fwr, const char *filename, file_watch_cb_t cb, void *ud) { + log_debug("Adding \"%s\" to watched files", filename); + auto fwr = (struct file_watch_registry *)_fwr; + int wd = -1; + + struct stat statbuf; + int ret = stat(filename, &statbuf); + if (ret < 0) { + log_error_errno("Failed to retrieve information about file \"%s\"", filename); + return false; + } + if (!S_ISREG(statbuf.st_mode)) { + log_info("\"%s\" is not a regular file, not watching it.", filename); + return false; + } + +#ifdef HAS_INOTIFY + wd = inotify_add_watch(fwr->w.fd, filename, + IN_CLOSE_WRITE | IN_MOVE_SELF | IN_DELETE_SELF); + if (wd < 0) { + log_error_errno("Failed to watch file \"%s\"", filename); + return false; + } +#elif HAS_KQUEUE + wd = open(filename, O_RDONLY); + if (wd < 0) { + log_error_errno("Cannot open file \"%s\" for watching", filename); + return false; + } + + uint32_t fflags = NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE | NOTE_ATTRIB; + // NOTE_CLOSE_WRITE is relatively new, so we cannot just use it +#ifdef NOTE_CLOSE_WRITE + fflags |= NOTE_CLOSE_WRITE; +#else + // NOTE_WRITE will receive notification more frequent than necessary, so is less + // preferrable + fflags |= NOTE_WRITE; +#endif + struct kevent ev = { + .ident = (unsigned int)wd, // the wd < 0 case is checked above + .filter = EVFILT_VNODE, + .flags = EV_ADD | EV_CLEAR, + .fflags = fflags, + .data = 0, + .udata = NULL, + }; + if (kevent(fwr->w.fd, &ev, 1, NULL, 0, NULL) < 0) { + log_error_errno("Failed to register kevent"); + close(wd); + return false; + } +#else + assert(false); +#endif // HAS_KQUEUE + + auto w = ccalloc(1, struct watched_file); + w->wd = wd; + w->cb = cb; + w->ud = ud; + + HASH_ADD_INT(fwr->reg, wd, w); + return true; +} diff --git a/src/file_watch.h b/src/file_watch.h new file mode 100644 index 0000000..c249cd2 --- /dev/null +++ b/src/file_watch.h @@ -0,0 +1,10 @@ +#pragma once +#include <stdbool.h> + +#include <ev.h> + +typedef void (*file_watch_cb_t)(void *); + +void *file_watch_init(EV_P); +bool file_watch_add(void *, const char *, file_watch_cb_t, void *); +void file_watch_destroy(EV_P_ void *); diff --git a/src/kernel.c b/src/kernel.c new file mode 100644 index 0000000..5151045 --- /dev/null +++ b/src/kernel.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> + +#include <assert.h> +#include <math.h> + +#include "compiler.h" +#include "kernel.h" +#include "log.h" +#include "utils.h" + +/// Sum a region convolution kernel. Region is defined by a width x height rectangle whose +/// top left corner is at (x, y) +double sum_kernel(const conv *map, int x, int y, int width, int height) { + double ret = 0; + + // Compute sum of values which are "in range" + int xstart = normalize_i_range(x, 0, map->w), + xend = normalize_i_range(width + x, 0, map->w); + int ystart = normalize_i_range(y, 0, map->h), + yend = normalize_i_range(height + y, 0, map->h); + assert(yend >= ystart && xend >= xstart); + + int d = map->w; + if (map->rsum) { + // See sum_kernel_preprocess + double v1 = xstart ? map->rsum[(yend - 1) * d + xstart - 1] : 0; + double v2 = ystart ? map->rsum[(ystart - 1) * d + xend - 1] : 0; + double v3 = (xstart && ystart) ? map->rsum[(ystart - 1) * d + xstart - 1] : 0; + return map->rsum[(yend - 1) * d + xend - 1] - v1 - v2 + v3; + } + + for (int yi = ystart; yi < yend; yi++) { + for (int xi = xstart; xi < xend; xi++) { + ret += map->data[yi * d + xi]; + } + } + + return ret; +} + +double sum_kernel_normalized(const conv *map, int x, int y, int width, int height) { + double ret = sum_kernel(map, x, y, width, height); + if (ret < 0) { + ret = 0; + } + if (ret > 1) { + ret = 1; + } + return ret; +} + +static inline double attr_const gaussian(double r, double x, double y) { + // Formula can be found here: + // https://en.wikipedia.org/wiki/Gaussian_blur#Mathematics + // Except a special case for r == 0 to produce sharp shadows + if (r == 0) + return 1; + return exp(-0.5 * (x * x + y * y) / (r * r)) / (2 * M_PI * r * r); +} + +conv *gaussian_kernel(double r, int size) { + conv *c; + int center = size / 2; + double t; + assert(size % 2 == 1); + + c = cvalloc(sizeof(conv) + (size_t)(size * size) * sizeof(double)); + c->w = c->h = size; + c->rsum = NULL; + t = 0.0; + + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + double g = gaussian(r, x - center, y - center); + t += g; + c->data[y * size + x] = g; + } + } + + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + c->data[y * size + x] /= t; + } + } + + return c; +} + +/// Estimate the element of the sum of the first row in a gaussian kernel with standard +/// deviation `r` and size `size`, +static inline double estimate_first_row_sum(double size, double r) { + double factor = erf(size / r / sqrt(2)); + double a = exp(-0.5 * size * size / (r * r)) / sqrt(2 * M_PI) / r; + return a / factor; +} + +/// Pick a suitable gaussian kernel radius for a given kernel size. The returned radius +/// is the maximum possible radius (<= size*2) that satisfies no sum of the rows in +/// the kernel are less than `row_limit` (up to certain precision). +static inline double gaussian_kernel_std_for_size(int size, double row_limit) { + assert(size > 0); + if (row_limit >= 1.0 / 2.0 / size) { + return size * 2; + } + double l = 0, r = size * 2; + while (r - l > 1e-2) { + double mid = (l + r) / 2.0; + double vmid = estimate_first_row_sum(size, mid); + if (vmid > row_limit) { + r = mid; + } else { + l = mid; + } + } + return (l + r) / 2.0; +} + +/// Create a gaussian kernel with auto detected standard deviation. The choosen standard +/// deviation tries to make sure the outer most pixels of the shadow are completely +/// transparent, so the transition from shadow to the background is smooth. +/// +/// @param[in] shadow_radius the radius of the shadow +conv *gaussian_kernel_autodetect_deviation(int shadow_radius) { + assert(shadow_radius >= 0); + int size = shadow_radius * 2 + 1; + + if (shadow_radius == 0) { + return gaussian_kernel(0, size); + } + double std = gaussian_kernel_std_for_size(shadow_radius, 1.0 / 256.0); + return gaussian_kernel(std, size); +} + +/// preprocess kernels to make shadow generation faster +/// shadow_sum[x*d+y] is the sum of the kernel from (0, 0) to (x, y), inclusive +void sum_kernel_preprocess(conv *map) { + if (map->rsum) { + free(map->rsum); + } + + auto sum = map->rsum = ccalloc(map->w * map->h, double); + sum[0] = map->data[0]; + + for (int x = 1; x < map->w; x++) { + sum[x] = sum[x - 1] + map->data[x]; + } + + const int d = map->w; + for (int y = 1; y < map->h; y++) { + sum[y * d] = sum[(y - 1) * d] + map->data[y * d]; + for (int x = 1; x < map->w; x++) { + double tmp = sum[(y - 1) * d + x] + sum[y * d + x - 1] - + sum[(y - 1) * d + x - 1]; + sum[y * d + x] = tmp + map->data[y * d + x]; + } + } +} + +// vim: set noet sw=8 ts=8 : diff --git a/src/kernel.h b/src/kernel.h new file mode 100644 index 0000000..251d127 --- /dev/null +++ b/src/kernel.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> + +#pragma once +#include <stdlib.h> +#include "compiler.h" + +/// Code for generating convolution kernels + +typedef struct conv { + int w, h; + double *rsum; + double data[]; +} conv; + +/// Calculate the sum of a rectangle part of the convolution kernel +/// the rectangle is defined by top left (x, y), and a size (width x height) +double attr_pure sum_kernel(const conv *map, int x, int y, int width, int height); +double attr_pure sum_kernel_normalized(const conv *map, int x, int y, int width, int height); + +/// Create a kernel with gaussian distribution with standard deviation `r`, and size +/// `size`. +conv *gaussian_kernel(double r, int size); + +/// Create a gaussian kernel with auto detected standard deviation. The choosen standard +/// deviation tries to make sure the outer most pixels of the shadow are completely +/// transparent. +/// +/// @param[in] shadow_radius the radius of the shadow +conv *gaussian_kernel_autodetect_deviation(int shadow_radius); + +/// preprocess kernels to make shadow generation faster +/// shadow_sum[x*d+y] is the sum of the kernel from (0, 0) to (x, y), inclusive +void sum_kernel_preprocess(conv *map); + +static inline void free_conv(conv *k) { + free(k->rsum); + free(k); +} diff --git a/src/list.h b/src/list.h new file mode 100644 index 0000000..19e2c2c --- /dev/null +++ b/src/list.h @@ -0,0 +1,108 @@ +#pragma once +#include <stdbool.h> +#include <stddef.h> + +/** + * container_of - cast a member of a structure out to the containing structure + * @ptr: the pointer to the member. + * @type: the type of the container struct this is embedded in. + * @member: the name of the member within the struct. + * + */ +#define container_of(ptr, type, member) \ + ({ \ + const __typeof__(((type *)0)->member) *__mptr = (ptr); \ + (type *)((char *)__mptr - offsetof(type, member)); \ + }) + +struct list_node { + struct list_node *next, *prev; +}; + +#define list_entry(ptr, type, node) container_of(ptr, type, node) +#define list_next_entry(ptr, node) list_entry((ptr)->node.next, __typeof__(*(ptr)), node) +#define list_prev_entry(ptr, node) list_entry((ptr)->node.prev, __typeof__(*(ptr)), node) + +/// Insert a new node between two adjacent nodes in the list +static inline void __list_insert_between(struct list_node *prev, struct list_node *next, + struct list_node *new_) { + new_->prev = prev; + new_->next = next; + next->prev = new_; + prev->next = new_; +} + +/// Insert a new node after `curr` +static inline void list_insert_after(struct list_node *curr, struct list_node *new_) { + __list_insert_between(curr, curr->next, new_); +} + +/// Insert a new node before `curr` +static inline void list_insert_before(struct list_node *curr, struct list_node *new_) { + __list_insert_between(curr->prev, curr, new_); +} + +/// Link two nodes in the list, so `next` becomes the successor node of `prev` +static inline void __list_link(struct list_node *prev, struct list_node *next) { + next->prev = prev; + prev->next = next; +} + +/// Remove a node from the list +static inline void list_remove(struct list_node *to_remove) { + __list_link(to_remove->prev, to_remove->next); + to_remove->prev = (void *)-1; + to_remove->next = (void *)-2; +} + +/// Move `to_move` so that it's before `new_next` +static inline void list_move_before(struct list_node *to_move, struct list_node *new_next) { + list_remove(to_move); + list_insert_before(new_next, to_move); +} + +/// Move `to_move` so that it's after `new_prev` +static inline void list_move_after(struct list_node *to_move, struct list_node *new_prev) { + list_remove(to_move); + list_insert_after(new_prev, to_move); +} + +/// Initialize a list node that's intended to be the head node +static inline void list_init_head(struct list_node *head) { + head->next = head->prev = head; +} + +/// Replace list node `old` with `n` +static inline void list_replace(struct list_node *old, struct list_node *n) { + __list_insert_between(old->prev, old->next, n); + old->prev = (void *)-1; + old->next = (void *)-2; +} + +/// Return true if head is the only node in the list. Under usual circumstances this means +/// the list is empty +static inline bool list_is_empty(const struct list_node *head) { + return head->prev == head; +} + +/// Return true if `to_check` is the first node in list headed by `head` +static inline bool +list_node_is_first(const struct list_node *head, const struct list_node *to_check) { + return head->next == to_check; +} + +/// Return true if `to_check` is the last node in list headed by `head` +static inline bool +list_node_is_last(const struct list_node *head, const struct list_node *to_check) { + return head->prev == to_check; +} + +#define list_foreach(type, i, head, member) \ + for (type *i = list_entry((head)->next, type, member); &i->member != (head); \ + i = list_next_entry(i, member)) + +/// Like list_for_each, but it's safe to remove the current list node from the list +#define list_foreach_safe(type, i, head, member) \ + for (type *i = list_entry((head)->next, type, member), \ + *__tmp = list_next_entry(i, member); \ + &i->member != (head); i = __tmp, __tmp = list_next_entry(i, member)) diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..0b663e7 --- /dev/null +++ b/src/log.c @@ -0,0 +1,376 @@ +#include <assert.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/uio.h> +#include <time.h> +#include <unistd.h> + +#ifdef CONFIG_OPENGL +#include <GL/gl.h> +#include "backend/gl/gl_common.h" +#include "backend/gl/glx.h" +#endif + +#include "compiler.h" +#include "log.h" +#include "utils.h" + +thread_local struct log *tls_logger; + +struct log_target; + +struct log { + struct log_target *head; + + int log_level; +}; + +struct log_target { + const struct log_ops *ops; + struct log_target *next; +}; + +struct log_ops { + void (*write)(struct log_target *, const char *, size_t); + void (*writev)(struct log_target *, const struct iovec *, int vcnt); + void (*destroy)(struct log_target *); + + /// Additional strings to print around the log_level string + const char *(*colorize_begin)(enum log_level); + const char *(*colorize_end)(enum log_level); +}; + +/// Fallback writev for targets don't implement it +static attr_unused void +log_default_writev(struct log_target *tgt, const struct iovec *vec, int vcnt) { + size_t total = 0; + for (int i = 0; i < vcnt; i++) { + total += vec[i].iov_len; + } + + if (!total) { + // Nothing to write + return; + } + char *buf = ccalloc(total, char); + total = 0; + for (int i = 0; i < vcnt; i++) { + memcpy(buf + total, vec[i].iov_base, vec[i].iov_len); + total += vec[i].iov_len; + } + tgt->ops->write(tgt, buf, total); + free(buf); +} + +static attr_const const char *log_level_to_string(enum log_level level) { + switch (level) { + case LOG_LEVEL_TRACE: return "TRACE"; + case LOG_LEVEL_DEBUG: return "DEBUG"; + case LOG_LEVEL_INFO: return "INFO"; + case LOG_LEVEL_WARN: return "WARN"; + case LOG_LEVEL_ERROR: return "ERROR"; + case LOG_LEVEL_FATAL: return "FATAL ERROR"; + default: return "????"; + } +} + +enum log_level string_to_log_level(const char *str) { + if (strcasecmp(str, "TRACE") == 0) + return LOG_LEVEL_TRACE; + else if (strcasecmp(str, "DEBUG") == 0) + return LOG_LEVEL_DEBUG; + else if (strcasecmp(str, "INFO") == 0) + return LOG_LEVEL_INFO; + else if (strcasecmp(str, "WARN") == 0) + return LOG_LEVEL_WARN; + else if (strcasecmp(str, "ERROR") == 0) + return LOG_LEVEL_ERROR; + return LOG_LEVEL_INVALID; +} + +struct log *log_new(void) { + auto ret = cmalloc(struct log); + ret->log_level = LOG_LEVEL_WARN; + ret->head = NULL; + return ret; +} + +void log_add_target(struct log *l, struct log_target *tgt) { + assert(tgt->ops->writev); + tgt->next = l->head; + l->head = tgt; +} + +/// Remove a previously added log target for a log struct, and destroy it. If the log +/// target was never added, nothing happens. +void log_remove_target(struct log *l, struct log_target *tgt) { + struct log_target *now = l->head, **prev = &l->head; + while (now) { + if (now == tgt) { + *prev = now->next; + tgt->ops->destroy(tgt); + break; + } + prev = &now->next; + now = now->next; + } +} + +/// Destroy a log struct and every log target added to it +void log_destroy(struct log *l) { + // free all tgt + struct log_target *head = l->head; + while (head) { + auto next = head->next; + head->ops->destroy(head); + head = next; + } + free(l); +} + +void log_set_level(struct log *l, int level) { + assert(level <= LOG_LEVEL_FATAL && level >= 0); + l->log_level = level; +} + +enum log_level log_get_level(const struct log *l) { + return l->log_level; +} + +attr_printf(4, 5) void log_printf(struct log *l, int level, const char *func, + const char *fmt, ...) { + assert(level <= LOG_LEVEL_FATAL && level >= 0); + if (level < l->log_level) + return; + + char *buf = NULL; + va_list args; + + va_start(args, fmt); + int blen = vasprintf(&buf, fmt, args); + va_end(args); + + if (blen < 0 || !buf) { + free(buf); + return; + } + + struct timespec ts; + timespec_get(&ts, TIME_UTC); + struct tm now; + localtime_r(&ts.tv_sec, &now); + char time_buf[100]; + strftime(time_buf, sizeof time_buf, "%x %T", &now); + + char *time = NULL; + int tlen = asprintf(&time, "%s.%03ld", time_buf, ts.tv_nsec / 1000000); + if (tlen < 0 || !time) { + free(buf); + free(time); + return; + } + + const char *log_level_str = log_level_to_string(level); + size_t llen = strlen(log_level_str); + size_t flen = strlen(func); + + struct log_target *head = l->head; + while (head) { + const char *p = "", *s = ""; + size_t plen = 0, slen = 0; + + if (head->ops->colorize_begin) { + // construct target specific prefix + p = head->ops->colorize_begin(level); + plen = strlen(p); + if (head->ops->colorize_end) { + s = head->ops->colorize_end(level); + slen = strlen(s); + } + } + head->ops->writev( + head, + (struct iovec[]){{.iov_base = "[ ", .iov_len = 2}, + {.iov_base = time, .iov_len = (size_t)tlen}, + {.iov_base = " ", .iov_len = 1}, + {.iov_base = (void *)func, .iov_len = flen}, + {.iov_base = " ", .iov_len = 1}, + {.iov_base = (void *)p, .iov_len = plen}, + {.iov_base = (void *)log_level_str, .iov_len = llen}, + {.iov_base = (void *)s, .iov_len = slen}, + {.iov_base = " ] ", .iov_len = 3}, + {.iov_base = buf, .iov_len = (size_t)blen}, + {.iov_base = "\n", .iov_len = 1}}, + 11); + head = head->next; + } + free(time); + free(buf); +} + +/// A trivial deinitializer that simply frees the memory +static attr_unused void logger_trivial_destroy(struct log_target *tgt) { + free(tgt); +} + +/// A null log target that does nothing +static const struct log_ops null_logger_ops; +static struct log_target null_logger_target = { + .ops = &null_logger_ops, +}; + +struct log_target *null_logger_new(void) { + return &null_logger_target; +} + +static void null_logger_write(struct log_target *tgt attr_unused, + const char *str attr_unused, size_t len attr_unused) { + return; +} + +static void null_logger_writev(struct log_target *tgt attr_unused, + const struct iovec *vec attr_unused, int vcnt attr_unused) { + return; +} + +static const struct log_ops null_logger_ops = { + .write = null_logger_write, + .writev = null_logger_writev, +}; + +/// A file based logger that writes to file (or stdout/stderr) +struct file_logger { + struct log_target tgt; + FILE *f; + struct log_ops ops; +}; + +static void file_logger_write(struct log_target *tgt, const char *str, size_t len) { + auto f = (struct file_logger *)tgt; + fwrite(str, 1, len, f->f); +} + +static void file_logger_writev(struct log_target *tgt, const struct iovec *vec, int vcnt) { + auto f = (struct file_logger *)tgt; + fflush(f->f); + writev(fileno(f->f), vec, vcnt); +} + +static void file_logger_destroy(struct log_target *tgt) { + auto f = (struct file_logger *)tgt; + fclose(f->f); + free(tgt); +} + +#define ANSI(x) "\033[" x "m" +static const char *terminal_colorize_begin(enum log_level level) { + switch (level) { + case LOG_LEVEL_TRACE: return ANSI("30;2"); + case LOG_LEVEL_DEBUG: return ANSI("37;2"); + case LOG_LEVEL_INFO: return ANSI("92"); + case LOG_LEVEL_WARN: return ANSI("33"); + case LOG_LEVEL_ERROR: return ANSI("31;1"); + case LOG_LEVEL_FATAL: return ANSI("30;103;1"); + default: return ""; + } +} + +static const char *terminal_colorize_end(enum log_level level attr_unused) { + return ANSI("0"); +} +#undef PREFIX + +static const struct log_ops file_logger_ops = { + .write = file_logger_write, + .writev = file_logger_writev, + .destroy = file_logger_destroy, +}; + +struct log_target *file_logger_new(const char *filename) { + FILE *f = fopen(filename, "a"); + if (!f) { + return NULL; + } + + auto ret = cmalloc(struct file_logger); + ret->tgt.ops = &ret->ops; + ret->f = f; + + // Always assume a file is not a terminal + ret->ops = file_logger_ops; + + return &ret->tgt; +} + +struct log_target *stderr_logger_new(void) { + int fd = dup(STDERR_FILENO); + if (fd < 0) { + return NULL; + } + + FILE *f = fdopen(fd, "w"); + if (!f) { + return NULL; + } + + auto ret = cmalloc(struct file_logger); + ret->tgt.ops = &ret->ops; + ret->f = f; + ret->ops = file_logger_ops; + + if (isatty(fd)) { + ret->ops.colorize_begin = terminal_colorize_begin; + ret->ops.colorize_end = terminal_colorize_end; + } + return &ret->tgt; +} + +#ifdef CONFIG_OPENGL +/// An opengl logger that can be used for logging into opengl debugging tools, +/// such as apitrace +struct gl_string_marker_logger { + struct log_target tgt; + PFNGLSTRINGMARKERGREMEDYPROC gl_string_marker; +}; + +static void +gl_string_marker_logger_write(struct log_target *tgt, const char *str, size_t len) { + auto g = (struct gl_string_marker_logger *)tgt; + // strip newlines at the end of the string + while (len > 0 && str[len-1] == '\n') { + len--; + } + g->gl_string_marker((GLsizei)len, str); +} + +static const struct log_ops gl_string_marker_logger_ops = { + .write = gl_string_marker_logger_write, + .writev = log_default_writev, + .destroy = logger_trivial_destroy, +}; + +struct log_target *gl_string_marker_logger_new(void) { + if (!gl_has_extension("GL_GREMEDY_string_marker")) { + return NULL; + } + + void *fnptr = glXGetProcAddress((GLubyte *)"glStringMarkerGREMEDY"); + if (!fnptr) + return NULL; + + auto ret = cmalloc(struct gl_string_marker_logger); + ret->tgt.ops = &gl_string_marker_logger_ops; + ret->gl_string_marker = fnptr; + return &ret->tgt; +} + +#else +struct log_target *gl_string_marker_logger_new(void) { + return NULL; +} +#endif + +// vim: set noet sw=8 ts=8: diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..e40fe3c --- /dev/null +++ b/src/log.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> + +#pragma once +#include <assert.h> +#include <stdio.h> + +#include "compiler.h" + +enum log_level { + LOG_LEVEL_INVALID = -1, + LOG_LEVEL_TRACE = 0, + LOG_LEVEL_DEBUG, + LOG_LEVEL_INFO, + LOG_LEVEL_WARN, + LOG_LEVEL_ERROR, + LOG_LEVEL_FATAL, +}; + +#define LOG_UNLIKELY(level, x, ...) \ + do { \ + if (unlikely(LOG_LEVEL_##level >= log_get_level_tls())) { \ + log_printf(tls_logger, LOG_LEVEL_##level, __func__, x, ##__VA_ARGS__); \ + } \ + } while (0) + +#define LOG(level, x, ...) \ + do { \ + if (LOG_LEVEL_##level >= log_get_level_tls()) { \ + log_printf(tls_logger, LOG_LEVEL_##level, __func__, x, ##__VA_ARGS__); \ + } \ + } while (0) +#define log_trace(x, ...) LOG_UNLIKELY(TRACE, x, ##__VA_ARGS__) +#define log_debug(x, ...) LOG_UNLIKELY(DEBUG, x, ##__VA_ARGS__) +#define log_info(x, ...) LOG(INFO, x, ##__VA_ARGS__) +#define log_warn(x, ...) LOG(WARN, x, ##__VA_ARGS__) +#define log_error(x, ...) LOG(ERROR, x, ##__VA_ARGS__) +#define log_fatal(x, ...) LOG(FATAL, x, ##__VA_ARGS__) + +#define log_error_errno(x, ...) LOG(ERROR, x ": %s", ##__VA_ARGS__, strerror(errno)) + +struct log; +struct log_target; + +attr_printf(4, 5) void log_printf(struct log *, int level, const char *func, + const char *fmt, ...); + +attr_malloc struct log *log_new(void); +/// Destroy a log struct and every log target added to it +attr_nonnull_all void log_destroy(struct log *); +attr_nonnull(1) void log_set_level(struct log *l, int level); +attr_pure enum log_level log_get_level(const struct log *l); +attr_nonnull_all void log_add_target(struct log *, struct log_target *); +attr_pure enum log_level string_to_log_level(const char *); +/// Remove a previously added log target for a log struct, and destroy it. If the log +/// target was never added, nothing happens. +void log_remove_target(struct log *l, struct log_target *tgt); + +extern thread_local struct log *tls_logger; + +/// Create a thread local logger +static inline void log_init_tls(void) { + tls_logger = log_new(); +} +/// Set thread local logger log level +static inline void log_set_level_tls(int level) { + assert(tls_logger); + log_set_level(tls_logger, level); +} +static inline attr_nonnull_all void log_add_target_tls(struct log_target *tgt) { + assert(tls_logger); + log_add_target(tls_logger, tgt); +} + +static inline attr_nonnull_all void log_remove_target_tls(struct log_target *tgt) { + assert(tls_logger); + log_remove_target(tls_logger, tgt); +} + +static inline attr_pure enum log_level log_get_level_tls(void) { + assert(tls_logger); + return log_get_level(tls_logger); +} + +static inline void log_deinit_tls(void) { + assert(tls_logger); + log_destroy(tls_logger); + tls_logger = NULL; +} + +attr_malloc struct log_target *stderr_logger_new(void); +attr_malloc struct log_target *file_logger_new(const char *file); +attr_malloc struct log_target *null_logger_new(void); +attr_malloc struct log_target *gl_string_marker_logger_new(void); + +// vim: set noet sw=8 ts=8: diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..0a882f9 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,97 @@ +libev = dependency('libev', required: false) +if not libev.found() + libev = cc.find_library('ev') +endif +base_deps = [ + cc.find_library('m'), + libev +] + +srcs = [ files('picom.c', 'win.c', 'c2.c', 'x.c', 'config.c', 'vsync.c', 'utils.c', + 'diagnostic.c', 'string_utils.c', 'render.c', 'kernel.c', 'log.c', + 'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c') ] +picom_inc = include_directories('.') + +cflags = [] + +required_xcb_packages = [ + 'xcb-render', 'xcb-damage', 'xcb-randr', 'xcb-sync', 'xcb-composite', + 'xcb-shape', 'xcb-xinerama', 'xcb-xfixes', 'xcb-present', 'xcb-glx', 'xcb' +] + +required_packages = [ + 'x11', 'x11-xcb', 'xcb-renderutil', 'xcb-image', 'xext', 'pixman-1' +] + +foreach i : required_packages + base_deps += [dependency(i, required: true)] +endforeach + +foreach i : required_xcb_packages + base_deps += [dependency(i, version: '>=1.12.0', required: true)] +endforeach + +if not cc.has_header('uthash.h') + error('Dependency uthash not found') +endif + +deps = [] + +if get_option('config_file') + deps += [dependency('libconfig', version: '>=1.4', required: true)] + + cflags += ['-DCONFIG_LIBCONFIG'] + srcs += [ 'config_libconfig.c' ] +endif +if get_option('regex') + pcre = dependency('libpcre', required: true) + cflags += ['-DCONFIG_REGEX_PCRE'] + if pcre.version().version_compare('>=8.20') + cflags += ['-DCONFIG_REGEX_PCRE_JIT'] + endif + deps += [pcre] +endif + +if get_option('vsync_drm') + cflags += ['-DCONFIG_VSYNC_DRM'] + deps += [dependency('libdrm', required: true)] +endif + +if get_option('opengl') + cflags += ['-DCONFIG_OPENGL', '-DGL_GLEXT_PROTOTYPES'] + deps += [dependency('gl', required: true)] + srcs += [ 'opengl.c' ] +endif + +if get_option('dbus') + cflags += ['-DCONFIG_DBUS'] + deps += [dependency('dbus-1', required: true)] + srcs += [ 'dbus.c' ] +endif + +if get_option('xrescheck') + cflags += ['-DDEBUG_XRC'] + srcs += [ 'xrescheck.c' ] +endif + +if get_option('unittest') + cflags += ['-DUNIT_TEST'] +endif + +host_system = host_machine.system() +if host_system == 'linux' + cflags += ['-DHAS_INOTIFY'] +elif (host_system == 'freebsd' or host_system == 'netbsd' or + host_system == 'dragonfly' or host_system == 'openbsd') + cflags += ['-DHAS_KQUEUE'] +endif + +subdir('backend') + +picom = executable('picom', srcs, c_args: cflags, + dependencies: [ base_deps, deps, test_h_dep ], + install: true, include_directories: picom_inc) + +if get_option('unittest') + test('picom unittest', picom, args: [ '--unittest' ]) +endif diff --git a/src/meta.h b/src/meta.h new file mode 100644 index 0000000..4314356 --- /dev/null +++ b/src/meta.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2019, Yuxuan Shui <[email protected]> + +#pragma once + +/// Macro metaprogramming + +#define _APPLY1(a, ...) a(__VA_ARGS__) +#define _APPLY2(a, ...) a(__VA_ARGS__) +#define _APPLY3(a, ...) a(__VA_ARGS__) +#define _APPLY4(a, ...) a(__VA_ARGS__) + +#define RIOTA1(x) x +#define RIOTA2(x) RIOTA1(x##1), RIOTA1(x##0) +#define RIOTA4(x) RIOTA2(x##1), RIOTA2(x##0) +#define RIOTA8(x) RIOTA4(x##1), RIOTA4(x##0) +#define RIOTA16(x) RIOTA8(x##1), RIOTA8(x##0) +/// Generate a list containing 31, 30, ..., 0, in binary +#define RIOTA32(x) RIOTA16(x##1), RIOTA16(x##0) + +#define CONCAT2(a, b) a##b +#define CONCAT1(a, b) CONCAT2(a, b) +#define CONCAT(a, b) CONCAT1(a, b) + +#define _ARGS_HEAD(head, ...) head +#define _ARGS_SKIP4(_1, _2, _3, _4, ...) __VA_ARGS__ +#define _ARGS_SKIP8(...) _APPLY1(_ARGS_SKIP4, _ARGS_SKIP4(__VA_ARGS__)) +#define _ARGS_SKIP16(...) _APPLY2(_ARGS_SKIP8, _ARGS_SKIP8(__VA_ARGS__)) +#define _ARGS_SKIP32(...) _APPLY3(_ARGS_SKIP16, _ARGS_SKIP16(__VA_ARGS__)) + +/// Return the 33rd argument +#define _ARG33(...) _APPLY4(_ARGS_HEAD, _ARGS_SKIP32(__VA_ARGS__)) + +/// Return the number of arguments passed in binary, handles at most 31 elements +#define VA_ARGS_LENGTH(...) _ARG33(0, ##__VA_ARGS__, RIOTA32(0)) + +#define LIST_APPLY_000000(fn, sep, ...) +#define LIST_APPLY_000001(fn, sep, x, ...) fn(x) +#define LIST_APPLY_000010(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000001(fn, sep, __VA_ARGS__) +#define LIST_APPLY_000011(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000010(fn, sep, __VA_ARGS__) +#define LIST_APPLY_000100(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000011(fn, sep, __VA_ARGS__) +#define LIST_APPLY_000101(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000100(fn, sep, __VA_ARGS__) +#define LIST_APPLY_000110(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000101(fn, sep, __VA_ARGS__) +#define LIST_APPLY_000111(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000110(fn, sep, __VA_ARGS__) +#define LIST_APPLY_001000(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000111(fn, sep, __VA_ARGS__) +#define LIST_APPLY_001001(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001000(fn, sep, __VA_ARGS__) +#define LIST_APPLY_001010(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001001(fn, sep, __VA_ARGS__) +#define LIST_APPLY_001011(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001010(fn, sep, __VA_ARGS__) +#define LIST_APPLY_001100(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001011(fn, sep, __VA_ARGS__) +#define LIST_APPLY_001101(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001100(fn, sep, __VA_ARGS__) +#define LIST_APPLY_001110(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001101(fn, sep, __VA_ARGS__) +#define LIST_APPLY_001111(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001110(fn, sep, __VA_ARGS__) +#define LIST_APPLY_010000(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001111(fn, sep, __VA_ARGS__) +#define LIST_APPLY_010001(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010000(fn, sep, __VA_ARGS__) +#define LIST_APPLY_010010(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010001(fn, sep, __VA_ARGS__) +#define LIST_APPLY_010011(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010010(fn, sep, __VA_ARGS__) +#define LIST_APPLY_010100(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010011(fn, sep, __VA_ARGS__) +#define LIST_APPLY_010101(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010100(fn, sep, __VA_ARGS__) +#define LIST_APPLY_010110(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010101(fn, sep, __VA_ARGS__) +#define LIST_APPLY_010111(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010110(fn, sep, __VA_ARGS__) +#define LIST_APPLY_011000(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010111(fn, sep, __VA_ARGS__) +#define LIST_APPLY_011001(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011000(fn, sep, __VA_ARGS__) +#define LIST_APPLY_011010(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011001(fn, sep, __VA_ARGS__) +#define LIST_APPLY_011011(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011010(fn, sep, __VA_ARGS__) +#define LIST_APPLY_011100(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011011(fn, sep, __VA_ARGS__) +#define LIST_APPLY_011101(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011100(fn, sep, __VA_ARGS__) +#define LIST_APPLY_011110(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011101(fn, sep, __VA_ARGS__) +#define LIST_APPLY_011111(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011110(fn, sep, __VA_ARGS__) +#define LIST_APPLY_(N, fn, sep, ...) CONCAT(LIST_APPLY_, N)(fn, sep, __VA_ARGS__) +#define LIST_APPLY(fn, sep, ...) \ + LIST_APPLY_(VA_ARGS_LENGTH(__VA_ARGS__), fn, sep, __VA_ARGS__) + +#define SEP_COMMA() , +#define SEP_COLON() ; +#define SEP_NONE() diff --git a/src/opengl.c b/src/opengl.c new file mode 100644 index 0000000..5d2d66c --- /dev/null +++ b/src/opengl.c @@ -0,0 +1,1514 @@ +// SPDX-License-Identifier: MIT +/* + * Compton - a compositor for X11 + * + * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard + * + * Copyright (c) 2011-2013, Christopher Jeffrey + * See LICENSE-mit for more information. + * + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <xcb/render.h> +#include <xcb/xcb.h> + +#include "backend/gl/gl_common.h" +#include "backend/gl/glx.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "kernel.h" +#include "log.h" +#include "region.h" +#include "string_utils.h" +#include "uthash_extra.h" +#include "utils.h" +#include "win.h" + +#include "opengl.h" + +#ifndef GL_TEXTURE_RECTANGLE +#define GL_TEXTURE_RECTANGLE 0x84F5 +#endif + +static inline XVisualInfo *get_visualinfo_from_visual(session_t *ps, xcb_visualid_t visual) { + XVisualInfo vreq = {.visualid = visual}; + int nitems = 0; + + return XGetVisualInfo(ps->dpy, VisualIDMask, &vreq, &nitems); +} + +/** + * Initialize OpenGL. + */ +bool glx_init(session_t *ps, bool need_render) { + bool success = false; + XVisualInfo *pvis = NULL; + + // Check for GLX extension + if (!ps->glx_exists) { + log_error("No GLX extension."); + goto glx_init_end; + } + + // Get XVisualInfo + pvis = get_visualinfo_from_visual(ps, ps->vis); + if (!pvis) { + log_error("Failed to acquire XVisualInfo for current visual."); + goto glx_init_end; + } + + // Ensure the visual is double-buffered + if (need_render) { + int value = 0; + if (Success != glXGetConfig(ps->dpy, pvis, GLX_USE_GL, &value) || !value) { + log_error("Root visual is not a GL visual."); + goto glx_init_end; + } + + if (Success != glXGetConfig(ps->dpy, pvis, GLX_DOUBLEBUFFER, &value) || !value) { + log_error("Root visual is not a double buffered GL visual."); + goto glx_init_end; + } + } + + // Ensure GLX_EXT_texture_from_pixmap exists + if (need_render && !glxext.has_GLX_EXT_texture_from_pixmap) + goto glx_init_end; + + // Initialize GLX data structure + if (!ps->psglx) { + static const glx_session_t CGLX_SESSION_DEF = CGLX_SESSION_INIT; + ps->psglx = cmalloc(glx_session_t); + memcpy(ps->psglx, &CGLX_SESSION_DEF, sizeof(glx_session_t)); + + // +1 for the zero terminator + ps->psglx->blur_passes = ccalloc(ps->o.blur_kernel_count, glx_blur_pass_t); + + for (int i = 0; i < ps->o.blur_kernel_count; ++i) { + glx_blur_pass_t *ppass = &ps->psglx->blur_passes[i]; + ppass->unifm_factor_center = -1; + ppass->unifm_offset_x = -1; + ppass->unifm_offset_y = -1; + } + + ps->psglx->round_passes = ccalloc(1, glx_round_pass_t); + glx_round_pass_t *ppass = ps->psglx->round_passes; + ppass->unifm_radius = -1; + ppass->unifm_texcoord = -1; + ppass->unifm_texsize = -1; + ppass->unifm_borderw = -1; + ppass->unifm_borderc = -1; + ppass->unifm_resolution = -1; + ppass->unifm_tex_scr = -1; + } + + glx_session_t *psglx = ps->psglx; + + if (!psglx->context) { + // Get GLX context +#ifndef DEBUG_GLX_DEBUG_CONTEXT + psglx->context = glXCreateContext(ps->dpy, pvis, None, GL_TRUE); +#else + { + GLXFBConfig fbconfig = get_fbconfig_from_visualinfo(ps, pvis); + if (!fbconfig) { + log_error("Failed to get GLXFBConfig for root visual " + "%#lx.", + pvis->visualid); + goto glx_init_end; + } + + f_glXCreateContextAttribsARB p_glXCreateContextAttribsARB = + (f_glXCreateContextAttribsARB)glXGetProcAddress( + (const GLubyte *)"glXCreateContextAttribsARB"); + if (!p_glXCreateContextAttribsARB) { + log_error("Failed to get glXCreateContextAttribsARB()."); + goto glx_init_end; + } + + static const int attrib_list[] = { + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_DEBUG_BIT_ARB, None}; + psglx->context = p_glXCreateContextAttribsARB( + ps->dpy, fbconfig, NULL, GL_TRUE, attrib_list); + } +#endif + + if (!psglx->context) { + log_error("Failed to get GLX context."); + goto glx_init_end; + } + + // Attach GLX context + if (!glXMakeCurrent(ps->dpy, get_tgt_window(ps), psglx->context)) { + log_error("Failed to attach GLX context."); + goto glx_init_end; + } + +#ifdef DEBUG_GLX_DEBUG_CONTEXT + { + f_DebugMessageCallback p_DebugMessageCallback = + (f_DebugMessageCallback)glXGetProcAddress( + (const GLubyte *)"glDebugMessageCallback"); + if (!p_DebugMessageCallback) { + log_error("Failed to get glDebugMessageCallback(0."); + goto glx_init_end; + } + p_DebugMessageCallback(glx_debug_msg_callback, ps); + } +#endif + } + + // Ensure we have a stencil buffer. X Fixes does not guarantee rectangles + // in regions don't overlap, so we must use stencil buffer to make sure + // we don't paint a region for more than one time, I think? + if (need_render && !ps->o.glx_no_stencil) { + GLint val = 0; + glGetIntegerv(GL_STENCIL_BITS, &val); + if (!val) { + log_error("Target window doesn't have stencil buffer."); + goto glx_init_end; + } + } + + // Check GL_ARB_texture_non_power_of_two, requires a GLX context and + // must precede FBConfig fetching + if (need_render) + psglx->has_texture_non_power_of_two = + gl_has_extension("GL_ARB_texture_non_power_of_two"); + + // Render preparations + if (need_render) { + glx_on_root_change(ps); + + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + glDisable(GL_BLEND); + + if (!ps->o.glx_no_stencil) { + // Initialize stencil buffer + glClear(GL_STENCIL_BUFFER_BIT); + glDisable(GL_STENCIL_TEST); + glStencilMask(0x1); + glStencilFunc(GL_EQUAL, 0x1, 0x1); + } + + // Clear screen + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // glXSwapBuffers(ps->dpy, get_tgt_window(ps)); + } + + success = true; + +glx_init_end: + XFree(pvis); + + if (!success) + glx_destroy(ps); + + return success; +} + +static void glx_free_prog_main(glx_prog_main_t *pprogram) { + if (!pprogram) + return; + if (pprogram->prog) { + glDeleteProgram(pprogram->prog); + pprogram->prog = 0; + } + pprogram->unifm_opacity = -1; + pprogram->unifm_invert_color = -1; + pprogram->unifm_tex = -1; +} + +/** + * Destroy GLX related resources. + */ +void glx_destroy(session_t *ps) { + if (!ps->psglx) + return; + + // Free all GLX resources of windows + win_stack_foreach_managed(w, &ps->window_stack) { + free_win_res_glx(ps, w); + } + + // Free GLSL shaders/programs + for (int i = 0; i < ps->o.blur_kernel_count; ++i) { + glx_blur_pass_t *ppass = &ps->psglx->blur_passes[i]; + if (ppass->frag_shader) { + glDeleteShader(ppass->frag_shader); + } + if (ppass->prog) { + glDeleteProgram(ppass->prog); + } + } + free(ps->psglx->blur_passes); + + glx_round_pass_t *ppass = ps->psglx->round_passes; + if (ppass->frag_shader) { + glDeleteShader(ppass->frag_shader); + } + if (ppass->prog) { + glDeleteProgram(ppass->prog); + } + free(ps->psglx->round_passes); + + glx_free_prog_main(&ps->glx_prog_win); + + gl_check_err(); + + // Destroy GLX context + if (ps->psglx->context) { + glXMakeCurrent(ps->dpy, None, NULL); + glXDestroyContext(ps->dpy, ps->psglx->context); + ps->psglx->context = NULL; + } + + free(ps->psglx); + ps->psglx = NULL; + ps->argb_fbconfig = NULL; +} + +/** + * Callback to run on root window size change. + */ +void glx_on_root_change(session_t *ps) { + glViewport(0, 0, ps->root_width, ps->root_height); + + // Initialize matrix, copied from dcompmgr + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0, ps->root_width, 0, ps->root_height, -1000.0, 1000.0); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); +} + +/** + * Initialize GLX blur filter. + */ +bool glx_init_blur(session_t *ps) { + assert(ps->o.blur_kernel_count > 0); + assert(ps->o.blur_kerns); + assert(ps->o.blur_kerns[0]); + + // Allocate PBO if more than one blur kernel is present + if (ps->o.blur_kernel_count > 1) { + // Try to generate a framebuffer + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + if (!fbo) { + log_error("Failed to generate Framebuffer. Cannot do multi-pass " + "blur with GLX" + " backend."); + return false; + } + glDeleteFramebuffers(1, &fbo); + } + + { + 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"); + + static const char *FRAG_SHADER_BLUR_PREFIX = + "#version 110\n" + "%s" + "uniform float offset_x;\n" + "uniform float offset_y;\n" + "uniform float factor_center;\n" + "uniform %s tex_scr;\n" + "\n" + "void main() {\n" + " vec4 sum = vec4(0.0, 0.0, 0.0, 0.0);\n"; + static const char *FRAG_SHADER_BLUR_ADD = + " sum += float(%.7g) * %s(tex_scr, vec2(gl_TexCoord[0].x + offset_x " + "* float(%d), gl_TexCoord[0].y + offset_y * float(%d)));\n"; + static const char *FRAG_SHADER_BLUR_SUFFIX = + " sum += %s(tex_scr, vec2(gl_TexCoord[0].x, gl_TexCoord[0].y)) * " + "factor_center;\n" + " gl_FragColor = sum / (factor_center + float(%.7g));\n" + "}\n"; + + const bool use_texture_rect = !ps->psglx->has_texture_non_power_of_two; + const char *sampler_type = (use_texture_rect ? "sampler2DRect" : "sampler2D"); + const char *texture_func = (use_texture_rect ? "texture2DRect" : "texture2D"); + const char *shader_add = FRAG_SHADER_BLUR_ADD; + char *extension = NULL; + if (use_texture_rect) { + mstrextend(&extension, "#extension GL_ARB_texture_rectangle : " + "require\n"); + } + if (!extension) { + extension = strdup(""); + } + + for (int i = 0; i < ps->o.blur_kernel_count; ++i) { + auto kern = ps->o.blur_kerns[i]; + glx_blur_pass_t *ppass = &ps->psglx->blur_passes[i]; + + // Build shader + int width = kern->w, height = kern->h; + int nele = width * height - 1; + assert(nele >= 0); + auto len = + strlen(FRAG_SHADER_BLUR_PREFIX) + strlen(sampler_type) + + strlen(extension) + + (strlen(shader_add) + strlen(texture_func) + 42) * (uint)nele + + strlen(FRAG_SHADER_BLUR_SUFFIX) + strlen(texture_func) + 12 + 1; + char *shader_str = ccalloc(len, char); + char *pc = shader_str; + sprintf(pc, FRAG_SHADER_BLUR_PREFIX, extension, sampler_type); + pc += strlen(pc); + assert(strlen(shader_str) < len); + + double sum = 0.0; + for (int j = 0; j < height; ++j) { + for (int k = 0; k < width; ++k) { + if (height / 2 == j && width / 2 == k) + continue; + double val = kern->data[j * width + k]; + if (val == 0) { + continue; + } + sum += val; + sprintf(pc, shader_add, val, texture_func, + k - width / 2, j - height / 2); + pc += strlen(pc); + assert(strlen(shader_str) < len); + } + } + + sprintf(pc, FRAG_SHADER_BLUR_SUFFIX, texture_func, sum); + assert(strlen(shader_str) < len); + ppass->frag_shader = gl_create_shader(GL_FRAGMENT_SHADER, shader_str); + free(shader_str); + + if (!ppass->frag_shader) { + log_error("Failed to create fragment shader %d.", i); + free(extension); + free(lc_numeric_old); + return false; + } + + // Build program + ppass->prog = gl_create_program(&ppass->frag_shader, 1); + if (!ppass->prog) { + log_error("Failed to create GLSL program."); + free(extension); + free(lc_numeric_old); + return false; + } + + // Get uniform addresses +#define P_GET_UNIFM_LOC(name, target) \ + { \ + ppass->target = glGetUniformLocation(ppass->prog, name); \ + if (ppass->target < 0) { \ + log_error("Failed to get location of %d-th uniform '" name \ + "'. Might be troublesome.", \ + i); \ + } \ + } + + P_GET_UNIFM_LOC("factor_center", unifm_factor_center); + P_GET_UNIFM_LOC("offset_x", unifm_offset_x); + P_GET_UNIFM_LOC("offset_y", unifm_offset_y); + +#undef P_GET_UNIFM_LOC + } + free(extension); + + // Restore LC_NUMERIC + setlocale(LC_NUMERIC, lc_numeric_old); + free(lc_numeric_old); + } + + gl_check_err(); + + return true; +} + +/** + * Initialize GLX rounded corners filter. + */ +bool glx_init_rounded_corners(session_t *ps) { + 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"); + + static const char *FRAG_SHADER = + "#version 110\n" + "%s" // extensions + "uniform float u_radius;\n" + "uniform float u_borderw;\n" + "uniform vec4 u_borderc;\n" + "uniform vec2 u_texcoord;\n" + "uniform vec2 u_texsize;\n" + "uniform vec2 u_resolution;\n" + "uniform %s tex_scr;\n" // sampler2D | sampler2DRect + "\n" + "// https://www.shadertoy.com/view/ltS3zW\n" + "float RectSDF(vec2 p, vec2 b, float r) {\n" + " vec2 d = abs(p) - b + vec2(r);\n" + " return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;\n" + "}\n\n" + "void main()\n" + "{\n" + " vec2 coord = vec2(u_texcoord.x, " + "u_resolution.y-u_texsize.y-u_texcoord.y);\n" + " vec4 u_v4WndBgColor = %s(tex_scr, vec2(gl_TexCoord[0].st));\n" + " float u_fRadiusPx = u_radius;\n" + " float u_fHalfBorderThickness = u_borderw / 2.0;\n" + " vec4 u_v4BorderColor = u_borderc;\n" + " vec4 u_v4FillColor = vec4(0.0, 0.0, 0.0, 0.0);\n" + " vec4 v4FromColor = u_v4BorderColor; //Always the border " + "color. If no border, this still should be set\n" + " vec4 v4ToColor = u_v4WndBgColor; //Outside color is the " + "background texture\n" + "\n" + " vec2 u_v2HalfShapeSizePx = u_texsize/2.0 - " + "vec2(u_fHalfBorderThickness);\n" + " vec2 v_v2CenteredPos = (gl_FragCoord.xy - u_texsize.xy / 2.0 - " + "coord);\n" + "\n" + " float fDist = RectSDF(v_v2CenteredPos, u_v2HalfShapeSizePx, " + "u_fRadiusPx - u_fHalfBorderThickness);\n" + " if (u_fHalfBorderThickness > 0.0) {\n" + " if (fDist < 0.0) {\n" + " v4ToColor = u_v4FillColor;\n" + " }\n" + " fDist = abs(fDist) - u_fHalfBorderThickness;\n" + " } else {\n" + " v4FromColor = u_v4FillColor;\n" + " }\n" + " float fBlendAmount = smoothstep(-1.0, 1.0, fDist);\n" + " vec4 c = mix(v4FromColor, v4ToColor, fBlendAmount);\n" + "\n" + " // final color\n" + " gl_FragColor = c;\n" + "\n" + "}\n"; + + const bool use_texture_rect = !ps->psglx->has_texture_non_power_of_two; + const char *sampler_type = (use_texture_rect ? "sampler2DRect" : "sampler2D"); + const char *texture_func = (use_texture_rect ? "texture2DRect" : "texture2D"); + char *extension = NULL; + if (use_texture_rect) { + mstrextend(&extension, "#extension GL_ARB_texture_rectangle : " + "require\n"); + } + if (!extension) { + extension = strdup(""); + } + + bool success = false; + // Build rounded corners shader + auto ppass = ps->psglx->round_passes; + auto len = strlen(FRAG_SHADER) + strlen(extension) + strlen(sampler_type) + + strlen(texture_func) + 1; + char *shader_str = ccalloc(len, char); + + sprintf(shader_str, FRAG_SHADER, extension, sampler_type, texture_func); + assert(strlen(shader_str) < len); + + log_debug("Generated rounded corners shader:\n%s\n", shader_str); + + ppass->frag_shader = gl_create_shader(GL_FRAGMENT_SHADER, shader_str); + free(shader_str); + + if (!ppass->frag_shader) { + log_error("Failed to create rounded corners fragment shader."); + goto out; + } + + // Build program + ppass->prog = gl_create_program(&ppass->frag_shader, 1); + if (!ppass->prog) { + log_error("Failed to create GLSL program."); + goto out; + } + + // Get uniform addresses +#define P_GET_UNIFM_LOC(name, target) \ + { \ + ppass->target = glGetUniformLocation(ppass->prog, name); \ + if (ppass->target < 0) { \ + log_debug("Failed to get location of rounded corners uniform " \ + "'" name "'. Might be troublesome."); \ + } \ + } + P_GET_UNIFM_LOC("u_radius", unifm_radius); + P_GET_UNIFM_LOC("u_texcoord", unifm_texcoord); + P_GET_UNIFM_LOC("u_texsize", unifm_texsize); + P_GET_UNIFM_LOC("u_borderw", unifm_borderw); + P_GET_UNIFM_LOC("u_borderc", unifm_borderc); + P_GET_UNIFM_LOC("u_resolution", unifm_resolution); + P_GET_UNIFM_LOC("tex_scr", unifm_tex_scr); +#undef P_GET_UNIFM_LOC + + success = true; + +out: + free(extension); + + // Restore LC_NUMERIC + setlocale(LC_NUMERIC, lc_numeric_old); + free(lc_numeric_old); + + gl_check_err(); + + return success; +} + +/** + * Load a GLSL main program from shader strings. + */ +bool glx_load_prog_main(const char *vshader_str, const char *fshader_str, + glx_prog_main_t *pprogram) { + assert(pprogram); + + // Build program + pprogram->prog = gl_create_program_from_str(vshader_str, fshader_str); + if (!pprogram->prog) { + log_error("Failed to create GLSL program."); + return false; + } + + // Get uniform addresses +#define P_GET_UNIFM_LOC(name, target) \ + { \ + pprogram->target = glGetUniformLocation(pprogram->prog, name); \ + if (pprogram->target < 0) { \ + log_error("Failed to get location of uniform '" name \ + "'. Might be troublesome."); \ + } \ + } + P_GET_UNIFM_LOC("opacity", unifm_opacity); + P_GET_UNIFM_LOC("invert_color", unifm_invert_color); + P_GET_UNIFM_LOC("tex", unifm_tex); + P_GET_UNIFM_LOC("time", unifm_time); +#undef P_GET_UNIFM_LOC + + gl_check_err(); + + return true; +} + +static inline void glx_copy_region_to_tex(session_t *ps, GLenum tex_tgt, int basex, + int basey, int dx, int dy, int width, int height) { + if (width > 0 && height > 0) { + glCopyTexSubImage2D(tex_tgt, 0, dx - basex, dy - basey, dx, + ps->root_height - dy - height, width, height); + } +} + +static inline GLuint glx_gen_texture(GLenum tex_tgt, int width, int height) { + GLuint tex = 0; + glGenTextures(1, &tex); + if (!tex) { + return 0; + } + glEnable(tex_tgt); + glBindTexture(tex_tgt, tex); + glTexParameteri(tex_tgt, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(tex_tgt, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(tex_tgt, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(tex_tgt, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(tex_tgt, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + glBindTexture(tex_tgt, 0); + + return tex; +} + +/** + * Bind an OpenGL texture and fill it with pixel data from back buffer + */ +bool glx_bind_texture(session_t *ps attr_unused, glx_texture_t **pptex, int x, int y, + int width, int height) { + if (ps->o.backend != BKEND_GLX && ps->o.backend != BKEND_XR_GLX_HYBRID) { + return true; + } + + glx_texture_t *ptex = *pptex; + + // log_trace("Copying xy(%d %d) wh(%d %d) ptex(%p)", x, y, width, height, ptex); + + // Release texture if parameters are inconsistent + if (ptex && ptex->texture && (ptex->width != width || ptex->height != height)) { + free_texture(ps, &ptex); + } + + // Allocate structure + if (!ptex) { + ptex = ccalloc(1, glx_texture_t); + *pptex = ptex; + + ptex->width = width; + ptex->height = height; + ptex->target = GL_TEXTURE_RECTANGLE; + if (ps->psglx->has_texture_non_power_of_two) { + ptex->target = GL_TEXTURE_2D; + } + } + + // Create texture + if (!ptex->texture) { + ptex->texture = glx_gen_texture(ptex->target, width, height); + } + if (!ptex->texture) { + log_error("Failed to allocate texture."); + return false; + } + + // Read destination pixels into a texture + glEnable(ptex->target); + glBindTexture(ptex->target, ptex->texture); + if (width > 0 && height > 0) { + glx_copy_region_to_tex(ps, ptex->target, x, y, x, y, width, height); + } + + // Cleanup + glBindTexture(ptex->target, 0); + glDisable(ptex->target); + + gl_check_err(); + + return true; +} + +/** + * Bind a X pixmap to an OpenGL texture. + */ +bool glx_bind_pixmap(session_t *ps, glx_texture_t **pptex, xcb_pixmap_t pixmap, int width, + int height, bool repeat, const struct glx_fbconfig_info *fbcfg) { + if (ps->o.backend != BKEND_GLX && ps->o.backend != BKEND_XR_GLX_HYBRID) + return true; + + if (!pixmap) { + log_error("Binding to an empty pixmap %#010x. This can't work.", pixmap); + return false; + } + + assert(fbcfg); + glx_texture_t *ptex = *pptex; + bool need_release = true; + + // Release pixmap if parameters are inconsistent + if (ptex && ptex->texture && ptex->pixmap != pixmap) { + glx_release_pixmap(ps, ptex); + } + + // Allocate structure + if (!ptex) { + static const glx_texture_t GLX_TEX_DEF = { + .texture = 0, + .glpixmap = 0, + .pixmap = 0, + .target = 0, + .width = 0, + .height = 0, + .y_inverted = false, + }; + + ptex = cmalloc(glx_texture_t); + memcpy(ptex, &GLX_TEX_DEF, sizeof(glx_texture_t)); + *pptex = ptex; + } + + // Create GLX pixmap + int depth = 0; + if (!ptex->glpixmap) { + need_release = false; + + // Retrieve pixmap parameters, if they aren't provided + if (!width || !height) { + auto r = xcb_get_geometry_reply( + ps->c, xcb_get_geometry(ps->c, pixmap), NULL); + if (!r) { + log_error("Failed to query info of pixmap %#010x.", pixmap); + return false; + } + if (r->depth > OPENGL_MAX_DEPTH) { + log_error("Requested depth %d higher than %d.", depth, + OPENGL_MAX_DEPTH); + return false; + } + depth = r->depth; + width = r->width; + height = r->height; + free(r); + } + + // Determine texture target, copied from compiz + // The assumption we made here is the target never changes based on any + // pixmap-specific parameters, and this may change in the future + GLenum tex_tgt = 0; + if (GLX_TEXTURE_2D_BIT_EXT & fbcfg->texture_tgts && + ps->psglx->has_texture_non_power_of_two) + tex_tgt = GLX_TEXTURE_2D_EXT; + else if (GLX_TEXTURE_RECTANGLE_BIT_EXT & fbcfg->texture_tgts) + tex_tgt = GLX_TEXTURE_RECTANGLE_EXT; + else if (!(GLX_TEXTURE_2D_BIT_EXT & fbcfg->texture_tgts)) + tex_tgt = GLX_TEXTURE_RECTANGLE_EXT; + else + tex_tgt = GLX_TEXTURE_2D_EXT; + + log_debug("depth %d, tgt %#x, rgba %d", depth, tex_tgt, + (GLX_TEXTURE_FORMAT_RGBA_EXT == fbcfg->texture_fmt)); + + GLint attrs[] = { + GLX_TEXTURE_FORMAT_EXT, + fbcfg->texture_fmt, + GLX_TEXTURE_TARGET_EXT, + (GLint)tex_tgt, + 0, + }; + + ptex->glpixmap = glXCreatePixmap(ps->dpy, fbcfg->cfg, pixmap, attrs); + ptex->pixmap = pixmap; + ptex->target = + (GLX_TEXTURE_2D_EXT == tex_tgt ? GL_TEXTURE_2D : GL_TEXTURE_RECTANGLE); + ptex->width = width; + ptex->height = height; + ptex->y_inverted = fbcfg->y_inverted; + } + if (!ptex->glpixmap) { + log_error("Failed to allocate GLX pixmap."); + return false; + } + + glEnable(ptex->target); + + // Create texture + if (!ptex->texture) { + need_release = false; + + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(ptex->target, texture); + + glTexParameteri(ptex->target, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(ptex->target, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + if (repeat) { + glTexParameteri(ptex->target, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(ptex->target, GL_TEXTURE_WRAP_T, GL_REPEAT); + } else { + glTexParameteri(ptex->target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(ptex->target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + + glBindTexture(ptex->target, 0); + + ptex->texture = texture; + } + if (!ptex->texture) { + log_error("Failed to allocate texture."); + return false; + } + + glBindTexture(ptex->target, ptex->texture); + + // The specification requires rebinding whenever the content changes... + // We can't follow this, too slow. + if (need_release) + glXReleaseTexImageEXT(ps->dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT); + + glXBindTexImageEXT(ps->dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT, NULL); + + // Cleanup + glBindTexture(ptex->target, 0); + glDisable(ptex->target); + + gl_check_err(); + + return true; +} + +/** + * @brief Release binding of a texture. + */ +void glx_release_pixmap(session_t *ps, glx_texture_t *ptex) { + // Release binding + if (ptex->glpixmap && ptex->texture) { + glBindTexture(ptex->target, ptex->texture); + glXReleaseTexImageEXT(ps->dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT); + glBindTexture(ptex->target, 0); + } + + // Free GLX Pixmap + if (ptex->glpixmap) { + glXDestroyPixmap(ps->dpy, ptex->glpixmap); + ptex->glpixmap = 0; + } + + gl_check_err(); +} + +/** + * Set clipping region on the target window. + */ +void glx_set_clip(session_t *ps, const region_t *reg) { + // Quit if we aren't using stencils + if (ps->o.glx_no_stencil) + return; + + glDisable(GL_STENCIL_TEST); + glDisable(GL_SCISSOR_TEST); + + if (!reg) + return; + + int nrects; + const rect_t *rects = pixman_region32_rectangles((region_t *)reg, &nrects); + + if (nrects == 1) { + glEnable(GL_SCISSOR_TEST); + glScissor(rects[0].x1, ps->root_height - rects[0].y2, + rects[0].x2 - rects[0].x1, rects[0].y2 - rects[0].y1); + } + + gl_check_err(); +} + +#define P_PAINTREG_START(var) \ + region_t reg_new; \ + int nrects; \ + const rect_t *rects; \ + assert(width >= 0 && height >= 0); \ + pixman_region32_init_rect(®_new, dx, dy, (uint)width, (uint)height); \ + pixman_region32_intersect(®_new, ®_new, (region_t *)reg_tgt); \ + rects = pixman_region32_rectangles(®_new, &nrects); \ + glBegin(GL_QUADS); \ + \ + for (int ri = 0; ri < nrects; ++ri) { \ + rect_t var = rects[ri]; + +#define P_PAINTREG_END() \ + } \ + glEnd(); \ + \ + pixman_region32_fini(®_new); + +/** + * Blur contents in a particular region. + * + * XXX seems to be way to complex for what it does + */ +bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, + GLfloat factor_center, const region_t *reg_tgt, glx_blur_cache_t *pbc) { + assert(ps->psglx->blur_passes[0].prog); + const bool more_passes = ps->o.blur_kernel_count > 1; + const bool have_scissors = glIsEnabled(GL_SCISSOR_TEST); + const bool have_stencil = glIsEnabled(GL_STENCIL_TEST); + bool ret = false; + + // Calculate copy region size + glx_blur_cache_t ibc = {.width = 0, .height = 0}; + if (!pbc) + pbc = &ibc; + + int mdx = dx, mdy = dy, mwidth = width, mheight = height; + // log_trace("%d, %d, %d, %d", mdx, mdy, mwidth, mheight); + + /* + if (ps->o.resize_damage > 0) { + int inc_x = 0, inc_y = 0; + for (int i = 0; i < MAX_BLUR_PASS; ++i) { + XFixed *kern = ps->o.blur_kerns[i]; + if (!kern) break; + inc_x += XFIXED_TO_DOUBLE(kern[0]) / 2; + inc_y += XFIXED_TO_DOUBLE(kern[1]) / 2; + } + inc_x = min2(ps->o.resize_damage, inc_x); + inc_y = min2(ps->o.resize_damage, inc_y); + + mdx = max2(dx - inc_x, 0); + mdy = max2(dy - inc_y, 0); + int mdx2 = min2(dx + width + inc_x, ps->root_width), + mdy2 = min2(dy + height + inc_y, ps->root_height); + mwidth = mdx2 - mdx; + mheight = mdy2 - mdy; + } + */ + + GLenum tex_tgt = GL_TEXTURE_RECTANGLE; + if (ps->psglx->has_texture_non_power_of_two) + tex_tgt = GL_TEXTURE_2D; + + // Free textures if size inconsistency discovered + if (mwidth != pbc->width || mheight != pbc->height) + free_glx_bc_resize(ps, pbc); + + // Generate FBO and textures if needed + if (!pbc->textures[0]) + pbc->textures[0] = glx_gen_texture(tex_tgt, mwidth, mheight); + GLuint tex_scr = pbc->textures[0]; + if (more_passes && !pbc->textures[1]) + pbc->textures[1] = glx_gen_texture(tex_tgt, mwidth, mheight); + pbc->width = mwidth; + pbc->height = mheight; + GLuint tex_scr2 = pbc->textures[1]; + if (more_passes && !pbc->fbo) + glGenFramebuffers(1, &pbc->fbo); + const GLuint fbo = pbc->fbo; + + if (!tex_scr || (more_passes && !tex_scr2)) { + log_error("Failed to allocate texture."); + goto glx_blur_dst_end; + } + if (more_passes && !fbo) { + log_error("Failed to allocate framebuffer."); + goto glx_blur_dst_end; + } + + // Read destination pixels into a texture + glEnable(tex_tgt); + glBindTexture(tex_tgt, tex_scr); + glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, mdx, mdy, mwidth, mheight); + /* + if (tex_scr2) { + glBindTexture(tex_tgt, tex_scr2); + glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, mdx, mdy, mwidth, dx - mdx); + glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, mdx, dy + height, + mwidth, mdy + mheight - dy - height); + glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, mdx, dy, dx - mdx, height); + glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, dx + width, dy, + mdx + mwidth - dx - width, height); + } */ + + // Texture scaling factor + GLfloat texfac_x = 1.0f, texfac_y = 1.0f; + if (tex_tgt == GL_TEXTURE_2D) { + texfac_x /= (GLfloat)mwidth; + texfac_y /= (GLfloat)mheight; + } + + // Paint it back + if (more_passes) { + glDisable(GL_STENCIL_TEST); + glDisable(GL_SCISSOR_TEST); + } + + bool last_pass = false; + for (int i = 0; i < ps->o.blur_kernel_count; ++i) { + last_pass = (i == ps->o.blur_kernel_count - 1); + const glx_blur_pass_t *ppass = &ps->psglx->blur_passes[i]; + assert(ppass->prog); + + assert(tex_scr); + glBindTexture(tex_tgt, tex_scr); + + if (!last_pass) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, tex_scr2, 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + log_error("Framebuffer attachment failed."); + goto glx_blur_dst_end; + } + } else { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDrawBuffer(GL_BACK); + if (have_scissors) + glEnable(GL_SCISSOR_TEST); + if (have_stencil) + glEnable(GL_STENCIL_TEST); + } + + // Color negation for testing... + // glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); + // glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE); + // glTexEnvf(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_ONE_MINUS_SRC_COLOR); + + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + glUseProgram(ppass->prog); + if (ppass->unifm_offset_x >= 0) + glUniform1f(ppass->unifm_offset_x, texfac_x); + if (ppass->unifm_offset_y >= 0) + glUniform1f(ppass->unifm_offset_y, texfac_y); + if (ppass->unifm_factor_center >= 0) + glUniform1f(ppass->unifm_factor_center, factor_center); + + P_PAINTREG_START(crect) { + auto rx = (GLfloat)(crect.x1 - mdx) * texfac_x; + auto ry = (GLfloat)(mheight - (crect.y1 - mdy)) * texfac_y; + auto rxe = rx + (GLfloat)(crect.x2 - crect.x1) * texfac_x; + auto rye = ry - (GLfloat)(crect.y2 - crect.y1) * texfac_y; + auto rdx = (GLfloat)(crect.x1 - mdx); + auto rdy = (GLfloat)(mheight - crect.y1 + mdy); + if (last_pass) { + rdx = (GLfloat)crect.x1; + rdy = (GLfloat)(ps->root_height - crect.y1); + } + auto rdxe = rdx + (GLfloat)(crect.x2 - crect.x1); + auto rdye = rdy - (GLfloat)(crect.y2 - crect.y1); + + // log_trace("%f, %f, %f, %f -> %f, %f, %f, %f", rx, ry, + // rxe, rye, rdx, + // rdy, rdxe, rdye); + + glTexCoord2f(rx, ry); + glVertex3f(rdx, rdy, z); + + glTexCoord2f(rxe, ry); + glVertex3f(rdxe, rdy, z); + + glTexCoord2f(rxe, rye); + glVertex3f(rdxe, rdye, z); + + glTexCoord2f(rx, rye); + glVertex3f(rdx, rdye, z); + } + P_PAINTREG_END(); + + glUseProgram(0); + + // Swap tex_scr and tex_scr2 + { + GLuint tmp = tex_scr2; + tex_scr2 = tex_scr; + tex_scr = tmp; + } + } + + ret = true; + +glx_blur_dst_end: + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glBindTexture(tex_tgt, 0); + glDisable(tex_tgt); + if (have_scissors) + glEnable(GL_SCISSOR_TEST); + if (have_stencil) + glEnable(GL_STENCIL_TEST); + + if (&ibc == pbc) { + free_glx_bc(ps, pbc); + } + + gl_check_err(); + + return ret; +} + +// TODO(bhagwan) this is a mess and needs a more consistent way of getting the border +// pixel I tried looking for a notify event for XCB_CW_BORDER_PIXEL (in +// xcb_create_window()) or a way to get the pixels from xcb_render_picture_t but the +// documentation for the xcb_xrender extension is literaly non existent... +// +// NOTE(yshui) There is no consistent way to get the "border" color of a X window. From +// the WM's perspective there are multiple ways to implement window borders. Using +// glReadPixel is probably the most reliable way. +void glx_read_border_pixel(int root_height, int root_width, int x, int y, int width, + int height, float *ppixel) { + assert(ppixel); + + // Reset the color so the shader doesn't use it + ppixel[0] = ppixel[1] = ppixel[2] = ppixel[3] = -1.0F; + + // First try bottom left corner past the + // circle radius (after the rounded corner ends) + auto screen_x = x; + auto screen_y = root_height - height - y; + + // X is out of bounds + // move to the right side + if (screen_x < 0) { + screen_x += width; + } + + // Y is out of bounds + // move to to top part + if (screen_y < 0) { + screen_y += height; + } + + // All corners are out of bounds, give up + if (screen_x < 0 || screen_y < 0 || screen_x >= root_width || screen_y >= root_height) { + return; + } + + // Invert Y-axis so we can query border color from texture (0,0) + glReadPixels(screen_x, screen_y, 1, 1, GL_RGBA, GL_FLOAT, (void *)ppixel); + + log_trace("xy(%d, %d), glxy(%d %d) wh(%d %d), border_col(%.2f, %.2f, %.2f, %.2f)", + x, y, screen_x, screen_y, width, height, (float)ppixel[0], + (float)ppixel[1], (float)ppixel[2], (float)ppixel[3]); + + gl_check_err(); +} + +bool glx_round_corners_dst(session_t *ps, struct managed_win *w, + const glx_texture_t *ptex, int dx, int dy, int width, + int height, float z, float cr, const region_t *reg_tgt) { + assert(ps->psglx->round_passes->prog); + bool ret = false; + + // log_warn("dxy(%d, %d) wh(%d %d) rwh(%d %d) b(%d), f(%d)", + // dx, dy, width, height, ps->root_width, ps->root_height, w->g.border_width, + // w->focused); + + int mdx = dx, mdy = dy, mwidth = width, mheight = height; + log_trace("%d, %d, %d, %d", mdx, mdy, mwidth, mheight); + + if (w->g.border_width > 0) { + glx_read_border_pixel(ps->root_height, ps->root_width, dx, dy, width, + height, &w->border_col[0]); + } + + { + const glx_round_pass_t *ppass = ps->psglx->round_passes; + assert(ppass->prog); + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + glUseProgram(ppass->prog); + + // If caller specified a texture use it as source + log_trace("ptex: %p wh(%d %d) %d %d", ptex, ptex->width, ptex->height, + ptex->target, ptex->texture); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(ptex->target, ptex->texture); + + if (ppass->unifm_tex_scr >= 0) { + glUniform1i(ppass->unifm_tex_scr, (GLint)0); + } + if (ppass->unifm_radius >= 0) { + glUniform1f(ppass->unifm_radius, cr); + } + if (ppass->unifm_texcoord >= 0) { + glUniform2f(ppass->unifm_texcoord, (float)dx, (float)dy); + } + if (ppass->unifm_texsize >= 0) { + glUniform2f(ppass->unifm_texsize, (float)mwidth, (float)mheight); + } + if (ppass->unifm_borderw >= 0) { + // Don't render rounded border if we don't know the border color + glUniform1f(ppass->unifm_borderw, + w->border_col[0] != -1. ? (GLfloat)w->g.border_width : 0); + } + if (ppass->unifm_borderc >= 0) { + glUniform4f(ppass->unifm_borderc, w->border_col[0], + w->border_col[1], w->border_col[2], w->border_col[3]); + } + if (ppass->unifm_resolution >= 0) { + glUniform2f(ppass->unifm_resolution, (float)ps->root_width, + (float)ps->root_height); + } + + // Painting + { + P_PAINTREG_START(crect) { + // texture-local coordinates + auto rx = (GLfloat)(crect.x1 - dx); + auto ry = (GLfloat)(crect.y1 - dy); + auto rxe = rx + (GLfloat)(crect.x2 - crect.x1); + auto rye = ry + (GLfloat)(crect.y2 - crect.y1); + if (GL_TEXTURE_2D == ptex->target) { + rx = rx / (GLfloat)width; + ry = ry / (GLfloat)height; + rxe = rxe / (GLfloat)width; + rye = rye / (GLfloat)height; + } + + // coordinates for the texture in the target + auto rdx = (GLfloat)crect.x1; + auto rdy = (GLfloat)(ps->root_height - crect.y1); + auto rdxe = (GLfloat)rdx + (GLfloat)(crect.x2 - crect.x1); + auto rdye = (GLfloat)rdy - (GLfloat)(crect.y2 - crect.y1); + + // Invert Y if needed, this may not work as expected, + // though. I don't have such a FBConfig to test with. + ry = 1.0F - ry; + rye = 1.0F - rye; + + // log_trace("Rect %d (i:%d): %f, %f, %f, %f -> %f, %f, + // %f, %f", ri ,ptex ? ptex->y_inverted : -1, rx, ry, + // rxe, + // rye, rdx, rdy, rdxe, rdye); + + glTexCoord2f(rx, ry); + glVertex3f(rdx, rdy, z); + + glTexCoord2f(rxe, ry); + glVertex3f(rdxe, rdy, z); + + glTexCoord2f(rxe, rye); + glVertex3f(rdxe, rdye, z); + + glTexCoord2f(rx, rye); + glVertex3f(rdx, rdye, z); + } + P_PAINTREG_END(); + } + + glUseProgram(0); + glDisable(GL_BLEND); + } + + ret = true; + + glBindTexture(ptex->target, 0); + glDisable(ptex->target); + glDisable(GL_BLEND); + + gl_check_err(); + + return ret; +} + +bool glx_dim_dst(session_t *ps, int dx, int dy, int width, int height, int z, + GLfloat factor, const region_t *reg_tgt) { + // It's possible to dim in glx_render(), but it would be over-complicated + // considering all those mess in color negation and modulation + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glColor4f(0.0f, 0.0f, 0.0f, factor); + + P_PAINTREG_START(crect) { + // XXX what does all of these variables mean? + GLint rdx = crect.x1; + GLint rdy = ps->root_height - crect.y1; + GLint rdxe = rdx + (crect.x2 - crect.x1); + GLint rdye = rdy - (crect.y2 - crect.y1); + + glVertex3i(rdx, rdy, z); + glVertex3i(rdxe, rdy, z); + glVertex3i(rdxe, rdye, z); + glVertex3i(rdx, rdye, z); + } + P_PAINTREG_END(); + + glColor4f(0.0f, 0.0f, 0.0f, 0.0f); + glDisable(GL_BLEND); + + gl_check_err(); + + return true; +} + +/** + * @brief Render a region with texture data. + */ +bool glx_render(session_t *ps, const glx_texture_t *ptex, int x, int y, int dx, int dy, + int width, int height, int z, double opacity, bool argb, bool neg, + const region_t *reg_tgt, const glx_prog_main_t *pprogram) { + if (!ptex || !ptex->texture) { + log_error("Missing texture."); + return false; + } + + const bool has_prog = pprogram && pprogram->prog; + bool dual_texture = false; + + // It's required by legacy versions of OpenGL to enable texture target + // before specifying environment. Thanks to madsy for telling me. + glEnable(ptex->target); + + // Enable blending if needed + if (opacity < 1.0 || argb) { + + glEnable(GL_BLEND); + + // Needed for handling opacity of ARGB texture + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + + // This is all weird, but X Render is using premultiplied ARGB format, and + // we need to use those things to correct it. Thanks to derhass for help. + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glColor4d(opacity, opacity, opacity, opacity); + } + + if (!has_prog) { + // The default, fixed-function path + // Color negation + if (neg) { + // Simple color negation + if (!glIsEnabled(GL_BLEND)) { + glEnable(GL_COLOR_LOGIC_OP); + glLogicOp(GL_COPY_INVERTED); + } + // ARGB texture color negation + else if (argb) { + dual_texture = true; + + // Use two texture stages because the calculation is too + // complicated, thanks to madsy for providing code Texture + // stage 0 + glActiveTexture(GL_TEXTURE0); + + // Negation for premultiplied color: color = A - C + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); + glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_SUBTRACT); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_ALPHA); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_TEXTURE); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); + + // Pass texture alpha through + glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); + + // Texture stage 1 + glActiveTexture(GL_TEXTURE1); + glEnable(ptex->target); + glBindTexture(ptex->target, ptex->texture); + + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); + + // Modulation with constant factor + glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PRIMARY_COLOR); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_ALPHA); + + // Modulation with constant factor + glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PRIMARY_COLOR); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA); + + glActiveTexture(GL_TEXTURE0); + } + // RGB blend color negation + else { + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); + + // Modulation with constant factor + glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, + GL_ONE_MINUS_SRC_COLOR); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PRIMARY_COLOR); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); + + // Modulation with constant factor + glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); + glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PRIMARY_COLOR); + glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA); + } + } + } else { + // Programmable path + assert(pprogram->prog); + glUseProgram(pprogram->prog); + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + if (pprogram->unifm_opacity >= 0) + glUniform1f(pprogram->unifm_opacity, (float)opacity); + if (pprogram->unifm_invert_color >= 0) + glUniform1i(pprogram->unifm_invert_color, neg); + if (pprogram->unifm_tex >= 0) + glUniform1i(pprogram->unifm_tex, 0); + if (pprogram->unifm_time >= 0) + glUniform1f(pprogram->unifm_time, (float)ts.tv_sec * 1000.0f + + (float)ts.tv_nsec / 1.0e6f); + } + + // log_trace("Draw: %d, %d, %d, %d -> %d, %d (%d, %d) z %d", x, y, width, height, + // dx, dy, ptex->width, ptex->height, z); + + // Bind texture + glBindTexture(ptex->target, ptex->texture); + if (dual_texture) { + glActiveTexture(GL_TEXTURE1); + glBindTexture(ptex->target, ptex->texture); + glActiveTexture(GL_TEXTURE0); + } + + // Painting + { + P_PAINTREG_START(crect) { + // texture-local coordinates + auto rx = (GLfloat)(crect.x1 - dx + x); + auto ry = (GLfloat)(crect.y1 - dy + y); + auto rxe = rx + (GLfloat)(crect.x2 - crect.x1); + auto rye = ry + (GLfloat)(crect.y2 - crect.y1); + // Rectangle textures have [0-w] [0-h] while 2D texture has [0-1] + // [0-1] Thanks to amonakov for pointing out! + if (GL_TEXTURE_2D == ptex->target) { + rx = rx / (GLfloat)ptex->width; + ry = ry / (GLfloat)ptex->height; + rxe = rxe / (GLfloat)ptex->width; + rye = rye / (GLfloat)ptex->height; + } + + // coordinates for the texture in the target + GLint rdx = crect.x1; + GLint rdy = ps->root_height - crect.y1; + GLint rdxe = rdx + (crect.x2 - crect.x1); + GLint rdye = rdy - (crect.y2 - crect.y1); + + // Invert Y if needed, this may not work as expected, though. I + // don't have such a FBConfig to test with. + if (!ptex->y_inverted) { + ry = 1.0f - ry; + rye = 1.0f - rye; + } + + // log_trace("Rect %d: %f, %f, %f, %f -> %d, %d, %d, %d", ri, rx, + // ry, rxe, rye, + // rdx, rdy, rdxe, rdye); + +#define P_TEXCOORD(cx, cy) \ + { \ + if (dual_texture) { \ + glMultiTexCoord2f(GL_TEXTURE0, cx, cy); \ + glMultiTexCoord2f(GL_TEXTURE1, cx, cy); \ + } else \ + glTexCoord2f(cx, cy); \ + } + P_TEXCOORD(rx, ry); + glVertex3i(rdx, rdy, z); + + P_TEXCOORD(rxe, ry); + glVertex3i(rdxe, rdy, z); + + P_TEXCOORD(rxe, rye); + glVertex3i(rdxe, rdye, z); + + P_TEXCOORD(rx, rye); + glVertex3i(rdx, rdye, z); + } + P_PAINTREG_END(); + } + + // Cleanup + glBindTexture(ptex->target, 0); + glColor4f(0.0f, 0.0f, 0.0f, 0.0f); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + glDisable(GL_BLEND); + glDisable(GL_COLOR_LOGIC_OP); + glDisable(ptex->target); + + if (dual_texture) { + glActiveTexture(GL_TEXTURE1); + glBindTexture(ptex->target, 0); + glDisable(ptex->target); + glActiveTexture(GL_TEXTURE0); + } + + if (has_prog) + glUseProgram(0); + + gl_check_err(); + + return true; +} diff --git a/src/opengl.h b/src/opengl.h new file mode 100644 index 0000000..dcd8697 --- /dev/null +++ b/src/opengl.h @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT +/* + * Compton - a compositor for X11 + * + * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard + * + * Copyright (c) 2011-2013, Christopher Jeffrey + * See LICENSE-mit for more information. + * + */ + +#pragma once + +#include "common.h" +#include "compiler.h" +#include "log.h" +#include "region.h" +#include "render.h" +#include "win.h" + +#include <GL/gl.h> +#include <GL/glx.h> +#include <ctype.h> +#include <locale.h> +#include <stdlib.h> +#include <string.h> +#include <xcb/render.h> +#include <xcb/xcb.h> + +typedef struct { + /// Fragment shader for blur. + GLuint frag_shader; + /// GLSL program for blur. + GLuint prog; + /// Location of uniform "offset_x" in blur GLSL program. + GLint unifm_offset_x; + /// Location of uniform "offset_y" in blur GLSL program. + GLint unifm_offset_y; + /// Location of uniform "factor_center" in blur GLSL program. + GLint unifm_factor_center; +} glx_blur_pass_t; + +typedef struct { + /// Fragment shader for rounded corners. + GLuint frag_shader; + /// GLSL program for rounded corners. + GLuint prog; + /// Location of uniform "radius" in rounded-corners GLSL program. + GLint unifm_radius; + /// Location of uniform "texcoord" in rounded-corners GLSL program. + GLint unifm_texcoord; + /// Location of uniform "texsize" in rounded-corners GLSL program. + GLint unifm_texsize; + /// Location of uniform "borderw" in rounded-corners GLSL program. + GLint unifm_borderw; + /// Location of uniform "borderc" in rounded-corners GLSL program. + GLint unifm_borderc; + /// Location of uniform "resolution" in rounded-corners GLSL program. + GLint unifm_resolution; + /// Location of uniform "texture_scr" in rounded-corners GLSL program. + GLint unifm_tex_scr; + +} glx_round_pass_t; + +/// Structure containing GLX-dependent data for a session. +typedef struct glx_session { + // === OpenGL related === + /// GLX context. + GLXContext context; + /// Whether we have GL_ARB_texture_non_power_of_two. + bool has_texture_non_power_of_two; + /// Current GLX Z value. + int z; + glx_blur_pass_t *blur_passes; + glx_round_pass_t *round_passes; +} glx_session_t; + +/// @brief Wrapper of a binded GLX texture. +typedef struct _glx_texture { + GLuint texture; + GLXPixmap glpixmap; + xcb_pixmap_t pixmap; + GLenum target; + int width; + int height; + bool y_inverted; +} glx_texture_t; + +#define CGLX_SESSION_INIT \ + { .context = NULL } + +bool glx_dim_dst(session_t *ps, int dx, int dy, int width, int height, int z, + GLfloat factor, const region_t *reg_tgt); + +bool glx_render(session_t *ps, const glx_texture_t *ptex, int x, int y, int dx, int dy, + int width, int height, int z, double opacity, bool argb, bool neg, + const region_t *reg_tgt, const glx_prog_main_t *pprogram); + +bool glx_init(session_t *ps, bool need_render); + +void glx_destroy(session_t *ps); + +void glx_on_root_change(session_t *ps); + +bool glx_init_blur(session_t *ps); + +bool glx_init_rounded_corners(session_t *ps); + +#ifdef CONFIG_OPENGL +bool glx_load_prog_main(const char *vshader_str, const char *fshader_str, + glx_prog_main_t *pprogram); +#endif + +bool glx_bind_pixmap(session_t *ps, glx_texture_t **pptex, xcb_pixmap_t pixmap, int width, + int height, bool repeat, const struct glx_fbconfig_info *); + +void glx_release_pixmap(session_t *ps, glx_texture_t *ptex); + +bool glx_bind_texture(session_t *ps, glx_texture_t **pptex, int x, int y, int width, int height); + +void glx_paint_pre(session_t *ps, region_t *preg) attr_nonnull(1, 2); + +/** + * Check if a texture is binded, or is binded to the given pixmap. + */ +static inline bool glx_tex_binded(const glx_texture_t *ptex, xcb_pixmap_t pixmap) { + return ptex && ptex->glpixmap && ptex->texture && (!pixmap || pixmap == ptex->pixmap); +} + +void glx_set_clip(session_t *ps, const region_t *reg); + +bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, + GLfloat factor_center, const region_t *reg_tgt, glx_blur_cache_t *pbc); + +bool glx_round_corners_dst(session_t *ps, struct managed_win *w, + const glx_texture_t *ptex, int dx, int dy, int width, + int height, float z, float cr, const region_t *reg_tgt); + +GLuint glx_create_shader(GLenum shader_type, const char *shader_str); + +GLuint glx_create_program(const GLuint *const shaders, int nshaders); + +GLuint glx_create_program_from_str(const char *vert_shader_str, const char *frag_shader_str); + +unsigned char *glx_take_screenshot(session_t *ps, int *out_length); + +/** + * Check if there's a GLX context. + */ +static inline bool glx_has_context(session_t *ps) { + return ps->psglx && ps->psglx->context; +} + +/** + * Ensure we have a GLX context. + */ +static inline bool ensure_glx_context(session_t *ps) { + // Create GLX context + if (!glx_has_context(ps)) + glx_init(ps, false); + + return ps->psglx->context; +} + +/** + * Free a GLX texture. + */ +static inline void free_texture_r(session_t *ps attr_unused, GLuint *ptexture) { + if (*ptexture) { + assert(glx_has_context(ps)); + glDeleteTextures(1, ptexture); + *ptexture = 0; + } +} + +/** + * Free a GLX Framebuffer object. + */ +static inline void free_glx_fbo(GLuint *pfbo) { + if (*pfbo) { + glDeleteFramebuffers(1, pfbo); + *pfbo = 0; + } + assert(!*pfbo); +} + +/** + * Free data in glx_blur_cache_t on resize. + */ +static inline void free_glx_bc_resize(session_t *ps, glx_blur_cache_t *pbc) { + free_texture_r(ps, &pbc->textures[0]); + free_texture_r(ps, &pbc->textures[1]); + pbc->width = 0; + pbc->height = 0; +} + +/** + * Free a glx_blur_cache_t + */ +static inline void free_glx_bc(session_t *ps, glx_blur_cache_t *pbc) { + free_glx_fbo(&pbc->fbo); + free_glx_bc_resize(ps, pbc); +} + +/** + * Free a glx_texture_t. + */ +static inline void free_texture(session_t *ps, glx_texture_t **pptex) { + glx_texture_t *ptex = *pptex; + + // Quit if there's nothing + if (!ptex) { + return; + } + + glx_release_pixmap(ps, ptex); + + free_texture_r(ps, &ptex->texture); + + // Free structure itself + free(ptex); + *pptex = NULL; +} + +/** + * Free GLX part of paint_t. + */ +static inline void free_paint_glx(session_t *ps, paint_t *ppaint) { + free_texture(ps, &ppaint->ptex); +#ifdef CONFIG_OPENGL + free(ppaint->fbcfg); +#endif + ppaint->fbcfg = NULL; +} + +/** + * Free GLX part of win. + */ +static inline void free_win_res_glx(session_t *ps, struct managed_win *w) { + free_paint_glx(ps, &w->paint); + free_paint_glx(ps, &w->shadow_paint); +#ifdef CONFIG_OPENGL + free_glx_bc(ps, &w->glx_blur_cache); + free_texture(ps, &w->glx_texture_bg); +#endif +} diff --git a/src/options.c b/src/options.c new file mode 100644 index 0000000..6a7bb49 --- /dev/null +++ b/src/options.c @@ -0,0 +1,1128 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> + +#include <getopt.h> +#include <locale.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <xcb/render.h> // for xcb_render_fixed_t, XXX + +#include "backend/backend.h" +#include "common.h" +#include "config.h" +#include "log.h" +#include "options.h" +#include "utils.h" +#include "win.h" + +#pragma GCC diagnostic error "-Wunused-parameter" + +/** + * Print usage text. + */ +static void usage(const char *argv0, int ret) { +#define WARNING_DISABLED " (DISABLED AT COMPILE TIME)" + static const char *usage_text = + "picom (" COMPTON_VERSION ")\n" + "Please report bugs to https://github.com/yshui/picom\n\n" + "usage: %s [options]\n" + "Options:\n" + "\n" + "-r radius\n" + " The blur radius for shadows. (default 12)\n" + "\n" + "-o opacity\n" + " The translucency for shadows. (default .75)\n" + "\n" + "-l left-offset\n" + " The left offset for shadows. (default -15)\n" + "\n" + "-t top-offset\n" + " The top offset for shadows. (default -15)\n" + "\n" + "-I fade-in-step\n" + " Opacity change between steps while fading in. (default 0.028)\n" + "\n" + "-O fade-out-step\n" + " Opacity change between steps while fading out. (default 0.03)\n" + "\n" + "-D fade-delta-time\n" + " The time between steps in a fade in milliseconds. (default 10)\n" + "\n" + "-m opacity\n" + " The opacity for menus. (default 1.0)\n" + "\n" + "-c\n" + " Enabled client-side shadows on windows.\n" + "\n" + "-C\n" + " Avoid drawing shadows on dock/panel windows.\n" + "\n" + "-z\n" + " Zero the part of the shadow's mask behind the window.\n" + "\n" + "-f\n" + " Fade windows in/out when opening/closing and when opacity\n" + " changes, unless --no-fading-openclose is used.\n" + "\n" + "-F\n" + " Equals to -f. Deprecated.\n" + "\n" + "--animations\n" + " Run animations for window geometry changes (movement and scaling).\n" + "\n" + "--animation-for-open-window\n" + " Which animation to run when opening a window.\n" + " Must be one of `none`, `fly-in`, `zoom`,\n" + " `slide-down`, `slide-up`, `slide-left`, `slide-right`\n" + " (default: none).\n" + "\n" + "--animation-for-transient-window\n" + " Which animation to run when opening a transient window.\n" + " Must be one of `none`, `fly-in`, `zoom`,\n" + " `slide-down`, `slide-up`, `slide-left`, `slide-right`\n" + " (default: none).\n" + "\n" + "--animation-for-unmap-window\n" + " Which animation to run when hiding (e.g. minimize) a window.\n" + " Must be one of `auto`, `none`, `fly-in`, `zoom`,\n" + " `slide-down`, `slide-up`, `slide-left`, `slide-right`\n" + " `slide-in`, `slide-out`\n" + " (default: auto).\n" + "\n" + "--animation-for-workspace-switch-in\n" + " Which animation to run on switching workspace for windows\n" + " comming into view.\n" + " IMPORTANT: window manager must set _NET_CURRENT_DESKTOP\n" + " before doing the hide/show of windows\n" + " Must be one of `auto`, `none`, `fly-in`, `zoom`,\n" + " `slide-down`, `slide-up`, `slide-left`, `slide-right`\n" + " `slide-in`, `slide-out`\n" + " (default: auto).\n" + "\n" + "--animation-for-workspace-switch-out\n" + " Which animation to run on switching workspace for windows\n" + " going out of view.\n" + " IMPORTANT: window manager must set _NET_CURRENT_DESKTOP\n" + " before doing the hide/show of windows\n" + " Must be one of `auto`, `none`, `fly-in`, `zoom`,\n" + " `slide-down`, `slide-up`, `slide-left`, `slide-right`\n" + " `slide-in`, `slide-out`\n" + " (default: auto).\n" + "\n" + "--animation-stiffness\n" + " Stiffness (a.k.a. tension) parameter for animation (default: 200.0).\n" + "\n" + "--animation-dampening\n" + " Dampening (a.k.a. friction) parameter for animation (default: 25.0).\n" + "\n" + "--animation-window-mass\n" + " Mass parameter for animation (default: 1.0).\n" + "\n" + "--animation-delta\n" + " The time between steps in animation, in milliseconds. (> 0, defaults to 10).\n" + "\n" + "--animation-force-steps\n" + " Force animations to go step by step even if cpu usage is high \n" + " (default: false)\n" + "\n" + "--animation-clamping\n" + " Whether to clamp animations (default: true)\n" + "\n" + "-i opacity\n" + " Opacity of inactive windows. (0.1 - 1.0)\n" + "\n" + "-e opacity\n" + " Opacity of window titlebars and borders. (0.1 - 1.0)\n" + "\n" + "-G\n" + " Don't draw shadows on DND windows\n" + "\n" + "-b\n" + " Daemonize process.\n" + "\n" + "--show-all-xerrors\n" + " Show all X errors (for debugging).\n" + "\n" + "--config path\n" + " Look for configuration file at the path. Use /dev/null to avoid\n" + " loading configuration file." +#ifndef CONFIG_LIBCONFIG + WARNING_DISABLED +#endif + "\n\n" + "--write-pid-path path\n" + " Write process ID to a file.\n" + "\n" + "--shadow-color color\n" + " Color of shadow, as a hex RGB string (defaults to #000000)\n" + "\n" + "--shadow-red value\n" + " Red color value of shadow (0.0 - 1.0, defaults to 0).\n" + "\n" + "--shadow-green value\n" + " Green color value of shadow (0.0 - 1.0, defaults to 0).\n" + "\n" + "--shadow-blue value\n" + " Blue color value of shadow (0.0 - 1.0, defaults to 0).\n" + "\n" + "--inactive-opacity-override\n" + " Inactive opacity set by -i overrides value of _NET_WM_OPACITY.\n" + "\n" + "--inactive-dim value\n" + " Dim inactive windows. (0.0 - 1.0, defaults to 0)\n" + "\n" + "--active-opacity opacity\n" + " Default opacity for active windows. (0.0 - 1.0)\n" + "\n" + "--corner-radius value\n" + " Sets the radius of rounded window corners. When > 0, the compositor\n" + " will round the corners of windows. (defaults to 0).\n" + "\n" + "--rounded-corners-exclude condition\n" + " Exclude conditions for rounded corners.\n" + "\n" + "--mark-wmwin-focused\n" + " Try to detect WM windows and mark them as active.\n" + "\n" + "--shadow-exclude condition\n" + " Exclude conditions for shadows.\n" + "\n" + "--fade-exclude condition\n" + " Exclude conditions for fading.\n" + "\n" + "--mark-ovredir-focused\n" + " Mark windows that have no WM frame as active.\n" + "\n" + "--no-fading-openclose\n" + " Do not fade on window open/close.\n" + "\n" + "--no-fading-destroyed-argb\n" + " Do not fade destroyed ARGB windows with WM frame. Workaround of bugs\n" + " in Openbox, Fluxbox, etc.\n" + "\n" + "--shadow-ignore-shaped\n" + " Do not paint shadows on shaped windows. (Deprecated, use\n" + " --shadow-exclude \'bounding_shaped\' or\n" + " --shadow-exclude \'bounding_shaped && !rounded_corners\' instead.)\n" + "\n" + "--detect-rounded-corners\n" + " Try to detect windows with rounded corners and don't consider\n" + " them shaped windows. Affects --shadow-ignore-shaped,\n" + " --unredir-if-possible, and possibly others. You need to turn this\n" + " on manually if you want to match against rounded_corners in\n" + " conditions.\n" + "\n" + "--detect-client-opacity\n" + " Detect _NET_WM_OPACITY on client windows, useful for window\n" + " managers not passing _NET_WM_OPACITY of client windows to frame\n" + " windows.\n" + "\n" + "--refresh-rate val\n" + " Specify refresh rate of the screen. If not specified or 0, we\n" + " will try detecting this with X RandR extension.\n" + "\n" + "--vsync\n" + " Enable VSync\n" + "\n" + "--paint-on-overlay\n" + " Painting on X Composite overlay window.\n" + "\n" + "--use-ewmh-active-win\n" + " Use _NET_WM_ACTIVE_WINDOW on the root window to determine which\n" + " window is focused instead of using FocusIn/Out events.\n" + "\n" + "--unredir-if-possible\n" + " Unredirect all windows if a full-screen opaque window is\n" + " detected, to maximize performance for full-screen windows.\n" + "\n" + "--unredir-if-possible-delay ms\n" + " Delay before unredirecting the window, in milliseconds.\n" + " Defaults to 0.\n" + "\n" + "--unredir-if-possible-exclude condition\n" + " Conditions of windows that shouldn't be considered full-screen\n" + " for unredirecting screen.\n" + "\n" + "--focus-exclude condition\n" + " Specify a list of conditions of windows that should always be\n" + " considered focused.\n" + "\n" + "--inactive-dim-fixed\n" + " Use fixed inactive dim value.\n" + "\n" + "--max-brightness\n" + " Dims windows which average brightness is above this threshold.\n" + " Requires --no-use-damage.\n" + " Default: 1.0 or no dimming.\n" + "\n" + "--detect-transient\n" + " Use WM_TRANSIENT_FOR to group windows, and consider windows in\n" + " the same group focused at the same time.\n" + "\n" + "--detect-client-leader\n" + " Use WM_CLIENT_LEADER to group windows, and consider windows in\n" + " the same group focused at the same time. WM_TRANSIENT_FOR has\n" + " higher priority if --detect-transient is enabled, too.\n" + "\n" + "--blur-method\n" + " The algorithm used for background bluring. Available choices are:\n" + " 'none' to disable, 'gaussian', 'box' or 'kernel' for custom\n" + " convolution blur with --blur-kern.\n" + " Note: 'gaussian' and 'box' require --experimental-backends.\n" + "\n" + "--blur-size\n" + " The radius of the blur kernel for 'box' and 'gaussian' blur method.\n" + "\n" + "--blur-deviation\n" + " The standard deviation for the 'gaussian' blur method.\n" + "\n" + "--blur-strength\n" + " The strength level of the 'dual_kawase' blur method.\n" + "\n" + "--blur-background\n" + " Blur background of semi-transparent / ARGB windows. Bad in\n" + " performance. The switch name may change without prior\n" + " notifications.\n" + "\n" + "--blur-background-frame\n" + " Blur background of windows when the window frame is not opaque.\n" + " Implies --blur-background. Bad in performance. The switch name\n" + " may change.\n" + "\n" + "--blur-background-fixed\n" + " Use fixed blur strength instead of adjusting according to window\n" + " opacity.\n" + "\n" + "--blur-kern matrix\n" + " Specify the blur convolution kernel, with the following format:\n" + " WIDTH,HEIGHT,ELE1,ELE2,ELE3,ELE4,ELE5...\n" + " The element in the center must not be included, it will be forever\n" + " 1.0 or changing based on opacity, depending on whether you have\n" + " --blur-background-fixed.\n" + " A 7x7 Gaussian blur kernel looks like:\n" + " --blur-kern " + "'7,7,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0." + "000102,0.003494,0.029143,0.059106,0.029143,0.003494,0.000102,0.000849,0." + "029143,0.243117,0.493069,0.243117,0.029143,0.000849,0.001723,0.059106,0." + "493069,0.493069,0.059106,0.001723,0.000849,0.029143,0.243117,0.493069,0." + "243117,0.029143,0.000849,0.000102,0.003494,0.029143,0.059106,0.029143,0." + "003494,0.000102,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0." + "000003'\n" + " Up to 4 blur kernels may be specified, separated with semicolon, for\n" + " multi-pass blur.\n" + " May also be one the predefined kernels: 3x3box (default), 5x5box,\n" + " 7x7box, 3x3gaussian, 5x5gaussian, 7x7gaussian, 9x9gaussian,\n" + " 11x11gaussian.\n" + "\n" + "--blur-background-exclude condition\n" + " Exclude conditions for background blur.\n" + "\n" + "--resize-damage integer\n" + " Resize damaged region by a specific number of pixels. A positive\n" + " value enlarges it while a negative one shrinks it. Useful for\n" + " fixing the line corruption issues of blur. May or may not\n" + " work with --glx-no-stencil. Shrinking doesn't function correctly.\n" + "\n" + "--invert-color-include condition\n" + " Specify a list of conditions of windows that should be painted with\n" + " inverted color. Resource-hogging, and is not well tested.\n" + "\n" + "--opacity-rule opacity:condition\n" + " Specify a list of opacity rules, in the format \"PERCENT:PATTERN\",\n" + " like \'50:name *= \"Firefox\"'. picom-trans is recommended over\n" + " this. Note we do not distinguish 100%% and unset, and we don't make\n" + " any guarantee about possible conflicts with other programs that set\n" + " _NET_WM_WINDOW_OPACITY on frame or client windows.\n" + "\n" + "--shadow-exclude-reg geometry\n" + " Specify a X geometry that describes the region in which shadow\n" + " should not be painted in, such as a dock window region.\n" + " Use --shadow-exclude-reg \'x10+0-0\', for example, if the 10 pixels\n" + " on the bottom of the screen should not have shadows painted on.\n" + "\n" + "--clip-shadow-above condition\n" + " Specify a list of conditions of windows to not paint a shadow over,\n" + " such as a dock window.\n" + "\n" + "--xinerama-shadow-crop\n" + " Crop shadow of a window fully on a particular Xinerama screen to the\n" + " screen.\n" + "\n" + "--backend backend\n" + " Choose backend. Possible choices are xrender, glx, and\n" + " xr_glx_hybrid." +#ifndef CONFIG_OPENGL + " (GLX BACKENDS DISABLED AT COMPILE TIME)" +#endif + "\n\n" + "--glx-no-stencil\n" + " GLX backend: Avoid using stencil buffer. Might cause issues\n" + " when rendering transparent content. My tests show a 15%% performance\n" + " boost.\n" + "\n" + "--glx-no-rebind-pixmap\n" + " GLX backend: Avoid rebinding pixmap on window damage. Probably\n" + " could improve performance on rapid window content changes, but is\n" + " known to break things on some drivers (LLVMpipe, xf86-video-intel,\n" + " etc.).\n" + "\n" + "--no-use-damage\n" + " Disable the use of damage information. This cause the whole screen to\n" + " be redrawn everytime, instead of the part of the screen that has\n" + " actually changed. Potentially degrades the performance, but might fix\n" + " some artifacts.\n" + "\n" + "--xrender-sync-fence\n" + " Additionally use X Sync fence to sync clients' draw calls. Needed\n" + " on nvidia-drivers with GLX backend for some users.\n" + "\n" + "--force-win-blend\n" + " Force all windows to be painted with blending. Useful if you have a\n" + " --glx-fshader-win that could turn opaque pixels transparent.\n" + "\n" + "--dbus\n" + " Enable remote control via D-Bus. See the D-BUS API section in the\n" + " man page for more details." +#ifndef CONFIG_DBUS + WARNING_DISABLED +#endif + "\n\n" + "--benchmark cycles\n" + " Benchmark mode. Repeatedly paint until reaching the specified cycles.\n" + "\n" + "--benchmark-wid window-id\n" + " Specify window ID to repaint in benchmark mode. If omitted or is 0,\n" + " the whole screen is repainted.\n" + "\n" + "--monitor-repaint\n" + " Highlight the updated area of the screen. For debugging the xrender\n" + " backend only.\n" + "\n" + "--debug-mode\n" + " Render into a separate window, and don't take over the screen. Useful\n" + " when you want to attach a debugger to picom\n" + "\n" + "--no-ewmh-fullscreen\n" + " Do not use EWMH to detect fullscreen windows. Reverts to checking\n" + " if a window is fullscreen based only on its size and coordinates.\n" + "\n" + "--transparent-clipping\n" + " Make transparent windows clip other windows like non-transparent windows\n" + " do, instead of blending on top of them\n"; + FILE *f = (ret ? stderr : stdout); + fprintf(f, usage_text, argv0); +#undef WARNING_DISABLED +} + +static const char *shortopts = "D:I:O:d:r:o:m:l:t:i:e:hscnfFCaSzGb"; +static const struct option longopts[] = { + {"help", no_argument, NULL, 'h'}, + {"config", required_argument, NULL, 256}, + {"shadow-radius", required_argument, NULL, 'r'}, + {"shadow-opacity", required_argument, NULL, 'o'}, + {"shadow-offset-x", required_argument, NULL, 'l'}, + {"shadow-offset-y", required_argument, NULL, 't'}, + {"fade-in-step", required_argument, NULL, 'I'}, + {"fade-out-step", required_argument, NULL, 'O'}, + {"fade-delta", required_argument, NULL, 'D'}, + {"menu-opacity", required_argument, NULL, 'm'}, + {"shadow", no_argument, NULL, 'c'}, + {"no-dock-shadow", no_argument, NULL, 'C'}, + {"clear-shadow", no_argument, NULL, 'z'}, + {"fading", no_argument, NULL, 'f'}, + {"inactive-opacity", required_argument, NULL, 'i'}, + {"frame-opacity", required_argument, NULL, 'e'}, + {"daemon", no_argument, NULL, 'b'}, + {"no-dnd-shadow", no_argument, NULL, 'G'}, + {"shadow-red", required_argument, NULL, 257}, + {"shadow-green", required_argument, NULL, 258}, + {"shadow-blue", required_argument, NULL, 259}, + {"inactive-opacity-override", no_argument, NULL, 260}, + {"inactive-dim", required_argument, NULL, 261}, + {"mark-wmwin-focused", no_argument, NULL, 262}, + {"shadow-exclude", required_argument, NULL, 263}, + {"mark-ovredir-focused", no_argument, NULL, 264}, + {"no-fading-openclose", no_argument, NULL, 265}, + {"shadow-ignore-shaped", no_argument, NULL, 266}, + {"detect-rounded-corners", no_argument, NULL, 267}, + {"detect-client-opacity", no_argument, NULL, 268}, + {"refresh-rate", required_argument, NULL, 269}, + {"vsync", optional_argument, NULL, 270}, + {"alpha-step", required_argument, NULL, 271}, + {"dbe", no_argument, NULL, 272}, + {"paint-on-overlay", no_argument, NULL, 273}, + {"sw-opti", no_argument, NULL, 274}, + {"vsync-aggressive", no_argument, NULL, 275}, + {"use-ewmh-active-win", no_argument, NULL, 276}, + {"respect-prop-shadow", no_argument, NULL, 277}, + {"unredir-if-possible", no_argument, NULL, 278}, + {"focus-exclude", required_argument, NULL, 279}, + {"inactive-dim-fixed", no_argument, NULL, 280}, + {"detect-transient", no_argument, NULL, 281}, + {"detect-client-leader", no_argument, NULL, 282}, + {"blur-background", no_argument, NULL, 283}, + {"blur-background-frame", no_argument, NULL, 284}, + {"blur-background-fixed", no_argument, NULL, 285}, + {"dbus", no_argument, NULL, 286}, + {"logpath", required_argument, NULL, 287}, + {"invert-color-include", required_argument, NULL, 288}, + {"opengl", no_argument, NULL, 289}, + {"backend", required_argument, NULL, 290}, + {"glx-no-stencil", no_argument, NULL, 291}, + {"benchmark", required_argument, NULL, 293}, + {"benchmark-wid", required_argument, NULL, 294}, + {"blur-background-exclude", required_argument, NULL, 296}, + {"active-opacity", required_argument, NULL, 297}, + {"glx-no-rebind-pixmap", no_argument, NULL, 298}, + {"glx-swap-method", required_argument, NULL, 299}, + {"fade-exclude", required_argument, NULL, 300}, + {"blur-kern", required_argument, NULL, 301}, + {"resize-damage", required_argument, NULL, 302}, + {"glx-use-gpushader4", no_argument, NULL, 303}, + {"opacity-rule", required_argument, NULL, 304}, + {"shadow-exclude-reg", required_argument, NULL, 305}, + {"paint-exclude", required_argument, NULL, 306}, + {"xinerama-shadow-crop", no_argument, NULL, 307}, + {"unredir-if-possible-exclude", required_argument, NULL, 308}, + {"unredir-if-possible-delay", required_argument, NULL, 309}, + {"write-pid-path", required_argument, NULL, 310}, + {"vsync-use-glfinish", no_argument, NULL, 311}, + {"xrender-sync", no_argument, NULL, 312}, + {"xrender-sync-fence", no_argument, NULL, 313}, + {"show-all-xerrors", no_argument, NULL, 314}, + {"no-fading-destroyed-argb", no_argument, NULL, 315}, + {"force-win-blend", no_argument, NULL, 316}, + {"glx-fshader-win", required_argument, NULL, 317}, + {"version", no_argument, NULL, 318}, + {"no-x-selection", no_argument, NULL, 319}, + {"no-name-pixmap", no_argument, NULL, 320}, + {"log-level", required_argument, NULL, 321}, + {"log-file", required_argument, NULL, 322}, + {"use-damage", no_argument, NULL, 323}, + {"no-use-damage", no_argument, NULL, 324}, + {"no-vsync", no_argument, NULL, 325}, + {"max-brightness", required_argument, NULL, 326}, + {"transparent-clipping", no_argument, NULL, 327}, + {"blur-method", required_argument, NULL, 328}, + {"blur-size", required_argument, NULL, 329}, + {"blur-deviation", required_argument, NULL, 330}, + {"blur-strength", required_argument, NULL, 331}, + {"shadow-color", required_argument, NULL, 332}, + {"corner-radius", required_argument, NULL, 333}, + {"rounded-corners-exclude", required_argument, NULL, 334}, + {"clip-shadow-above", required_argument, NULL, 335}, + {"experimental-backends", no_argument, NULL, 733}, + {"monitor-repaint", no_argument, NULL, 800}, + {"diagnostics", no_argument, NULL, 801}, + {"debug-mode", no_argument, NULL, 802}, + {"no-ewmh-fullscreen", no_argument, NULL, 803}, + {"animations", no_argument, NULL, 804}, + {"animation-stiffness", required_argument, NULL, 805}, + {"animation-dampening", required_argument, NULL, 806}, + {"animation-window-mass", required_argument, NULL, 807}, + {"animation-clamping", no_argument, NULL, 808}, + {"animation-for-open-window", required_argument, NULL, 809}, + {"animation-for-transient-window", required_argument, NULL, 810}, + // Must terminate with a NULL entry + {NULL, 0, NULL, 0}, +}; + +/// Get config options that are needed to parse the rest of the options +/// Return true if we should quit +bool get_early_config(int argc, char *const *argv, char **config_file, bool *all_xerrors, + bool *fork, int *exit_code) { + int o = 0, longopt_idx = -1; + + // Pre-parse the commandline arguments to check for --config and invalid + // switches + // Must reset optind to 0 here in case we reread the commandline + // arguments + optind = 1; + *config_file = NULL; + *exit_code = 0; + while (-1 != (o = getopt_long(argc, argv, shortopts, longopts, &longopt_idx))) { + if (o == 256) { + *config_file = strdup(optarg); + } else if (o == 'h') { + usage(argv[0], 0); + return true; + + } else if (o == 'b') { + *fork = true; + } else if (o == 'd') { + log_error("-d is removed, please use the DISPLAY " + "environment variable"); + goto err; + } else if (o == 314) { + *all_xerrors = true; + } else if (o == 318) { + printf("%s\n", COMPTON_VERSION); + return true; + } else if (o == 'S') { + log_error("-S is no longer available"); + goto err; + } else if (o == 320) { + log_error("--no-name-pixmap is no longer available"); + goto err; + } else if (o == '?' || o == ':') { + usage(argv[0], 1); + goto err; + } + } + + // Check for abundant positional arguments + if (optind < argc) { + // log is not initialized here yet + fprintf(stderr, "picom doesn't accept positional arguments.\n"); + goto err; + } + + return false; +err: + *exit_code = 1; + return true; +} + +/** + * Process arguments and configuration files. + */ +bool get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, + bool fading_enable, bool conv_kern_hasneg, win_option_mask_t *winopt_mask) { + + int o = 0, longopt_idx = -1; + + char *lc_numeric_old = strdup(setlocale(LC_NUMERIC, NULL)); + + // Enforce LC_NUMERIC locale "C" here to make sure dots are recognized + // instead of commas in atof(). + setlocale(LC_NUMERIC, "C"); + + // Parse commandline arguments. Range checking will be done later. + + bool failed = false; + const char *deprecation_message attr_unused = + "has been removed. If you encounter problems " + "without this feature, please feel free to " + "open a bug report."; + optind = 1; + while (-1 != (o = getopt_long(argc, argv, shortopts, longopts, &longopt_idx))) { + switch (o) { +#define P_CASEBOOL(idx, option) \ + case idx: \ + opt->option = true; \ + break +#define P_CASELONG(idx, option) \ + case idx: \ + if (!parse_long(optarg, &opt->option)) { \ + exit(1); \ + } \ + break +#define P_CASEINT(idx, option) \ + case idx: \ + if (!parse_int(optarg, &opt->option)) { \ + exit(1); \ + } \ + break + + // clang-format off + // Short options + case 318: + case 'h': + // These options should cause us to exit early, + // so assert(false) here + assert(false); + break; + case 'd': + case 'b': + case 'S': + case 314: + case 320: + // These options are handled by get_early_config() + break; + P_CASEINT('D', fade_delta); + case 'I': opt->fade_in_step = normalize_d(atof(optarg)); break; + case 'O': opt->fade_out_step = normalize_d(atof(optarg)); break; + case 'c': shadow_enable = true; break; + case 'C': + log_error("Option `--no-dock-shadow`/`-C` has been removed. Please" + " use the wintype option `shadow` of `dock` instead."); + failed = true; break; + case 'G': + log_error("Option `--no-dnd-shadow`/`-G` has been removed. Please " + "use the wintype option `shadow` of `dnd` instead."); + failed = true; break; + case 'm':; + double tmp; + tmp = normalize_d(atof(optarg)); + winopt_mask[WINTYPE_DROPDOWN_MENU].opacity = true; + winopt_mask[WINTYPE_POPUP_MENU].opacity = true; + opt->wintype_option[WINTYPE_POPUP_MENU].opacity = tmp; + opt->wintype_option[WINTYPE_DROPDOWN_MENU].opacity = tmp; + break; + case 'f': + case 'F': + fading_enable = true; + break; + P_CASEINT('r', shadow_radius); + case 'o': + opt->shadow_opacity = atof(optarg); + break; + P_CASEINT('l', shadow_offset_x); + P_CASEINT('t', shadow_offset_y); + case 'i': + opt->inactive_opacity = normalize_d(atof(optarg)); + break; + case 'e': opt->frame_opacity = atof(optarg); break; + case 'z': + log_warn("clear-shadow is removed, shadows are automatically " + "cleared now. If you want to prevent shadow from been " + "cleared under certain types of windows, you can use " + "the \"full-shadow\" per window type option."); + break; + case 'n': + case 'a': + case 's': + log_error("-n, -a, and -s have been removed."); + failed = true; break; + // Long options + case 256: + // --config + break; + case 332:; + // --shadow-color + struct color rgb; + rgb = hex_to_rgb(optarg); + opt->shadow_red = rgb.red; + opt->shadow_green = rgb.green; + opt->shadow_blue = rgb.blue; + break; + case 257: + // --shadow-red + opt->shadow_red = atof(optarg); + break; + case 258: + // --shadow-green + opt->shadow_green = atof(optarg); + break; + case 259: + // --shadow-blue + opt->shadow_blue = atof(optarg); + break; + P_CASEBOOL(260, inactive_opacity_override); + case 261: + // --inactive-dim + opt->inactive_dim = atof(optarg); + break; + P_CASEBOOL(262, mark_wmwin_focused); + case 263: + // --shadow-exclude + condlst_add(&opt->shadow_blacklist, optarg); + break; + P_CASEBOOL(264, mark_ovredir_focused); + P_CASEBOOL(265, no_fading_openclose); + P_CASEBOOL(266, shadow_ignore_shaped); + P_CASEBOOL(267, detect_rounded_corners); + P_CASEBOOL(268, detect_client_opacity); + P_CASEINT(269, refresh_rate); + case 270: + if (optarg) { + opt->vsync = parse_vsync(optarg); + log_warn("--vsync doesn't take argument anymore. \"%s\" " + "is interpreted as \"%s\" for compatibility, but " + "this will stop working soon", + optarg, opt->vsync ? "true" : "false"); + } else { + opt->vsync = true; + } + break; + case 271: + // --alpha-step + log_error("--alpha-step has been removed, we now tries to " + "make use of all alpha values"); + failed = true; break; + case 272: + log_error("--dbe has been removed"); + failed = true; break; + case 273: + log_error("--paint-on-overlay has been removed, the feature is enabled " + "whenever possible"); + failed = true; break; + P_CASEBOOL(274, sw_opti); + case 275: + // --vsync-aggressive + log_warn("--vsync-aggressive has been deprecated, please remove it" + " from the command line options"); + break; + P_CASEBOOL(276, use_ewmh_active_win); + case 277: + // --respect-prop-shadow + log_warn("--respect-prop-shadow option has been deprecated, its " + "functionality will always be enabled. Please remove it " + "from the command line options"); + break; + P_CASEBOOL(278, unredir_if_possible); + case 279: + // --focus-exclude + condlst_add(&opt->focus_blacklist, optarg); + break; + P_CASEBOOL(280, inactive_dim_fixed); + P_CASEBOOL(281, detect_transient); + P_CASEBOOL(282, detect_client_leader); + case 283: + // --blur_background + opt->blur_method = BLUR_METHOD_KERNEL; + break; + P_CASEBOOL(284, blur_background_frame); + P_CASEBOOL(285, blur_background_fixed); + P_CASEBOOL(286, dbus); + case 287: + log_warn("Please use --log-file instead of --logpath"); + // fallthrough + case 322: + // --logpath, --log-file + free(opt->logpath); + opt->logpath = strdup(optarg); + break; + case 288: + // --invert-color-include + condlst_add(&opt->invert_color_list, optarg); + break; + case 289: + // --opengl + opt->backend = BKEND_GLX; + break; + case 290: + // --backend + opt->backend = parse_backend(optarg); + if (opt->backend >= NUM_BKEND) + exit(1); + break; + P_CASEBOOL(291, glx_no_stencil); + P_CASEINT(293, benchmark); + case 294: + // --benchmark-wid + opt->benchmark_wid = (xcb_window_t)strtol(optarg, NULL, 0); + break; + case 296: + // --blur-background-exclude + condlst_add(&opt->blur_background_blacklist, optarg); + break; + case 297: + // --active-opacity + opt->active_opacity = normalize_d(atof(optarg)); + break; + P_CASEBOOL(298, glx_no_rebind_pixmap); + case 299: { + // --glx-swap-method + char *endptr; + long tmpval = strtol(optarg, &endptr, 10); + bool should_remove = true; + if (*endptr || !(*optarg)) { + // optarg is not a number, or an empty string + tmpval = -1; + } + if (strcmp(optarg, "undefined") != 0 && tmpval != 0) { + // If not undefined, we will use damage and buffer-age to + // limit the rendering area. + opt->use_damage = true; + should_remove = false; + } + log_warn("--glx-swap-method has been deprecated, your setting " + "\"%s\" should be %s.", + optarg, + !should_remove ? "replaced by `--use-damage`" : + "removed"); + break; + } + case 300: + // --fade-exclude + condlst_add(&opt->fade_blacklist, optarg); + break; + case 301: + // --blur-kern + opt->blur_kerns = parse_blur_kern_lst(optarg, &conv_kern_hasneg, + &opt->blur_kernel_count); + if (!opt->blur_kerns) { + exit(1); + } + break; + P_CASEINT(302, resize_damage); + case 303: + // --glx-use-gpushader4 + log_warn("--glx-use-gpushader4 is deprecated since v6." + " Please remove it from command line options."); + break; + case 304: + // --opacity-rule + if (!parse_rule_opacity(&opt->opacity_rules, optarg)) + exit(1); + break; + case 305: + // --shadow-exclude-reg + free(opt->shadow_exclude_reg_str); + opt->shadow_exclude_reg_str = strdup(optarg); + log_warn("--shadow-exclude-reg is deprecated. You are likely " + "better off using --clip-shadow-above anyway"); + break; + case 306: + // --paint-exclude + condlst_add(&opt->paint_blacklist, optarg); + break; + P_CASEBOOL(307, xinerama_shadow_crop); + case 308: + // --unredir-if-possible-exclude + condlst_add(&opt->unredir_if_possible_blacklist, optarg); + break; + P_CASELONG(309, unredir_if_possible_delay); + case 310: + // --write-pid-path + free(opt->write_pid_path); + opt->write_pid_path = strdup(optarg); + if (*opt->write_pid_path != '/') { + log_warn("--write-pid-path is not an absolute path"); + } + break; + P_CASEBOOL(311, vsync_use_glfinish); + case 312: + // --xrender-sync + log_error("Please use --xrender-sync-fence instead of --xrender-sync"); + failed = true; break; + P_CASEBOOL(313, xrender_sync_fence); + P_CASEBOOL(315, no_fading_destroyed_argb); + P_CASEBOOL(316, force_win_blend); + case 317: + opt->glx_fshader_win_str = strdup(optarg); + break; + case 321: { + enum log_level tmp_level = string_to_log_level(optarg); + if (tmp_level == LOG_LEVEL_INVALID) { + log_warn("Invalid log level, defaults to WARN"); + } else { + log_set_level_tls(tmp_level); + } + break; + } + P_CASEBOOL(319, no_x_selection); + P_CASEBOOL(323, use_damage); + case 324: + opt->use_damage = false; + break; + case 325: + opt->vsync = false; + break; + + case 326: + opt->max_brightness = atof(optarg); + break; + P_CASEBOOL(327, transparent_clipping); + case 328: { + // --blur-method + enum blur_method method = parse_blur_method(optarg); + if (method >= BLUR_METHOD_INVALID) { + log_warn("Invalid blur method %s, ignoring.", optarg); + } else { + opt->blur_method = method; + } + break; + } + case 329: + // --blur-size + opt->blur_radius = atoi(optarg); + break; + case 330: + // --blur-deviation + opt->blur_deviation = atof(optarg); + break; + case 331: + // --blur-strength + opt->blur_strength = atoi(optarg); + break; + case 333: + // --cornor-radius + opt->corner_radius = atoi(optarg); + break; + case 334: + // --rounded-corners-exclude + condlst_add(&opt->rounded_corners_blacklist, optarg); + break; + case 335: + // --clip-shadow-above + condlst_add(&opt->shadow_clip_list, optarg); + break; + P_CASEBOOL(733, experimental_backends); + P_CASEBOOL(800, monitor_repaint); + case 801: opt->print_diagnostics = true; break; + P_CASEBOOL(802, debug_mode); + P_CASEBOOL(803, no_ewmh_fullscreen); + P_CASEBOOL(804, animations); + case 805: + // --animation-stiffness + opt->animation_stiffness = atof(optarg); + break; + case 806: + // --animation-dampening + opt->animation_dampening = atof(optarg); + break; + case 807: + // --animation-window-masss + opt->animation_window_mass = atof(optarg); + break; + case 808: + // --animation-clamping + opt->animation_clamping = true; + break; + case 809: { + // --animation-for-open-window + enum open_window_animation animation = parse_open_window_animation(optarg); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) { + log_warn("Invalid open-window animation %s, ignoring.", optarg); + } else { + opt->animation_for_open_window = animation; + } + break; + } + case 810: { + // --animation-for-transient-window + enum open_window_animation animation = parse_open_window_animation(optarg); + if (animation >= OPEN_WINDOW_ANIMATION_INVALID) { + log_warn("Invalid transient-window animation %s, ignoring.", optarg); + } else { + opt->animation_for_transient_window = animation; + } + break; + } + default: usage(argv[0], 1); break; +#undef P_CASEBOOL + } + // clang-format on + + if (failed) { + // Parsing this option has failed, break the loop + break; + } + } + + // Restore LC_NUMERIC + setlocale(LC_NUMERIC, lc_numeric_old); + free(lc_numeric_old); + + if (failed) { + return false; + } + + if (opt->monitor_repaint && opt->backend != BKEND_XRENDER && + !opt->experimental_backends) { + log_warn("--monitor-repaint has no effect when backend is not xrender"); + } + + if (opt->experimental_backends && !backend_list[opt->backend]) { + log_error("Backend \"%s\" is not available as part of the experimental " + "backends.", + BACKEND_STRS[opt->backend]); + return false; + } + + if (opt->debug_mode && !opt->experimental_backends) { + log_error("Debug mode only works with the experimental backends."); + return false; + } + + if (opt->transparent_clipping && !opt->experimental_backends) { + log_error("Transparent clipping only works with the experimental " + "backends"); + return false; + } + + // Range checking and option assignments + opt->fade_delta = max2(opt->fade_delta, 1); + opt->shadow_radius = max2(opt->shadow_radius, 0); + opt->shadow_red = normalize_d(opt->shadow_red); + opt->shadow_green = normalize_d(opt->shadow_green); + opt->shadow_blue = normalize_d(opt->shadow_blue); + opt->inactive_dim = normalize_d(opt->inactive_dim); + opt->frame_opacity = normalize_d(opt->frame_opacity); + opt->shadow_opacity = normalize_d(opt->shadow_opacity); + opt->refresh_rate = normalize_i_range(opt->refresh_rate, 0, 300); + + opt->max_brightness = normalize_d(opt->max_brightness); + if (opt->max_brightness < 1.0) { + if (opt->use_damage) { + log_warn("--max-brightness requires --no-use-damage. Falling " + "back to 1.0"); + opt->max_brightness = 1.0; + } + + if (!opt->experimental_backends || opt->backend != BKEND_GLX) { + log_warn("--max-brightness requires the experimental glx " + "backend. Falling back to 1.0"); + opt->max_brightness = 1.0; + } + } + + // --blur-background-frame implies --blur-background + if (opt->blur_background_frame && opt->blur_method == BLUR_METHOD_NONE) { + opt->blur_method = BLUR_METHOD_KERNEL; + } + + // Apply default wintype options that are dependent on global options + set_default_winopts(opt, winopt_mask, shadow_enable, fading_enable, + opt->blur_method != BLUR_METHOD_NONE); + + // Other variables determined by options + + // Determine whether we track window grouping + if (opt->detect_transient || opt->detect_client_leader) { + opt->track_leader = true; + } + + // Fill default blur kernel + if (opt->blur_method == BLUR_METHOD_KERNEL && + (!opt->blur_kerns || !opt->blur_kerns[0])) { + opt->blur_kerns = parse_blur_kern_lst("3x3box", &conv_kern_hasneg, + &opt->blur_kernel_count); + CHECK(opt->blur_kerns); + CHECK(opt->blur_kernel_count); + } + + // Sanitize parameters for dual-filter kawase blur + if (opt->blur_method == BLUR_METHOD_DUAL_KAWASE) { + if (opt->blur_strength <= 0 && opt->blur_radius > 500) { + log_warn("Blur radius >500 not supported by dual_kawase method, " + "capping to 500."); + opt->blur_radius = 500; + } + if (opt->blur_strength > 20) { + log_warn("Blur strength >20 not supported by dual_kawase method, " + "capping to 20."); + opt->blur_strength = 20; + } + if (!opt->experimental_backends) { + log_warn("Dual-kawase blur is not implemented by the legacy " + "backends, you must use the `experimental-backends` " + "option."); + } + } + + if (opt->resize_damage < 0) { + log_warn("Negative --resize-damage will not work correctly."); + } + + if (opt->backend == BKEND_XRENDER && conv_kern_hasneg) { + log_warn("A convolution kernel with negative values may not work " + "properly under X Render backend."); + } + + if (opt->corner_radius > 0 && opt->experimental_backends) { + log_warn("Rounded corner is only supported on legacy backends, it " + "will be disabled"); + opt->corner_radius = 0; + } + + return true; +} + +// vim: set noet sw=8 ts=8 : diff --git a/src/options.h b/src/options.h new file mode 100644 index 0000000..08aa15e --- /dev/null +++ b/src/options.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> +#pragma once + +/// Parse command line options + +#include <stdbool.h> +#include <xcb/render.h> // for xcb_render_fixed_t + +#include "compiler.h" +#include "config.h" +#include "types.h" +#include "win.h" // for wintype_t + +typedef struct session session_t; + +/// Get config options that are needed to parse the rest of the options +/// Return true if we should quit +bool get_early_config(int argc, char *const *argv, char **config_file, bool *all_xerrors, + bool *fork, int *exit_code); + +/** + * Process arguments and configuration files. + * + * Parameters: + * shadow_enable = Carry overs from parse_config + * fading_enable + * conv_kern_hasneg + * winopt_mask + * Returns: + * Whether configuration are processed successfully. + */ +bool must_use get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, + bool fading_enable, bool conv_kern_hasneg, + win_option_mask_t *winopt_mask); + +// vim: set noet sw=8 ts=8: diff --git a/src/picom.c b/src/picom.c new file mode 100644 index 0000000..eb479f3 --- /dev/null +++ b/src/picom.c @@ -0,0 +1,2790 @@ +// SPDX-License-Identifier: MIT +/* + * Compton - a compositor for X11 + * + * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard + * + * Copyright (c) 2011-2013, Christopher Jeffrey + * See LICENSE-mit for more information. + * + */ + +#include <X11/Xlib-xcb.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/extensions/sync.h> +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <xcb/composite.h> +#include <xcb/damage.h> +#include <xcb/glx.h> +#include <xcb/present.h> +#include <xcb/randr.h> +#include <xcb/render.h> +#include <xcb/sync.h> +#include <xcb/xfixes.h> +#include <xcb/xinerama.h> + +#include <ev.h> +#include <test.h> + +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "err.h" +#include "kernel.h" +#include "picom.h" +#ifdef CONFIG_OPENGL +#include "opengl.h" +#endif +#include "backend/backend.h" +#include "c2.h" +#include "config.h" +#include "diagnostic.h" +#include "log.h" +#include "region.h" +#include "render.h" +#include "types.h" +#include "utils.h" +#include "win.h" +#include "x.h" +#ifdef CONFIG_DBUS +#include "dbus.h" +#endif +#include "atom.h" +#include "event.h" +#include "file_watch.h" +#include "list.h" +#include "options.h" +#include "uthash_extra.h" + +/// Get session_t pointer from a pointer to a member of session_t +#define session_ptr(ptr, member) \ + ({ \ + const __typeof__(((session_t *)0)->member) *__mptr = (ptr); \ + (session_t *)((char *)__mptr - offsetof(session_t, member)); \ + }) + +static const long SWOPTI_TOLERANCE = 3000; + +static bool must_use redirect_start(session_t *ps); + +static void unredirect(session_t *ps); + +// === Global constants === + +/// Name strings for window types. +const char *const WINTYPES[NUM_WINTYPES] = { + "unknown", "desktop", "dock", "toolbar", "menu", + "utility", "splash", "dialog", "normal", "dropdown_menu", + "popup_menu", "tooltip", "notification", "combo", "dnd", +}; + +// clang-format off +/// Names of backends. +const char *const BACKEND_STRS[] = {[BKEND_XRENDER] = "xrender", + [BKEND_GLX] = "glx", + [BKEND_XR_GLX_HYBRID] = "xr_glx_hybrid", + [BKEND_DUMMY] = "dummy", + NULL}; +// clang-format on + +// === Global variables === + +/// Pointer to current session, as a global variable. Only used by +/// xerror(), which could not have a pointer to current session passed in. +/// XXX Limit what xerror can access by not having this pointer +session_t *ps_g = NULL; + +void set_root_flags(session_t *ps, uint64_t flags) { + log_debug("Setting root flags: %" PRIu64, flags); + ps->root_flags |= flags; + ps->pending_updates = true; +} + +void quit(session_t *ps) { + ps->quit = true; + ev_break(ps->loop, EVBREAK_ALL); +} + +/** + * Free Xinerama screen info. + * + * XXX consider moving to x.c + */ +static inline void free_xinerama_info(session_t *ps) { + if (ps->xinerama_scr_regs) { + for (int i = 0; i < ps->xinerama_nscrs; ++i) + pixman_region32_fini(&ps->xinerama_scr_regs[i]); + free(ps->xinerama_scr_regs); + ps->xinerama_scr_regs = NULL; + } + ps->xinerama_nscrs = 0; +} + +/** + * Get current system clock in milliseconds. + */ +static inline int64_t get_time_ms(void) { + struct timespec tp; + clock_gettime(CLOCK_MONOTONIC, &tp); + return (int64_t)tp.tv_sec * 1000 + (int64_t)tp.tv_nsec / 1000000; +} + +// XXX Move to x.c +void cxinerama_upd_scrs(session_t *ps) { + // XXX Consider deprecating Xinerama, switch to RandR when necessary + free_xinerama_info(ps); + + if (!ps->o.xinerama_shadow_crop || !ps->xinerama_exists) + return; + + xcb_xinerama_is_active_reply_t *active = + xcb_xinerama_is_active_reply(ps->c, xcb_xinerama_is_active(ps->c), NULL); + if (!active || !active->state) { + free(active); + return; + } + free(active); + + auto xinerama_scrs = + xcb_xinerama_query_screens_reply(ps->c, xcb_xinerama_query_screens(ps->c), NULL); + if (!xinerama_scrs) { + return; + } + + xcb_xinerama_screen_info_t *scrs = + xcb_xinerama_query_screens_screen_info(xinerama_scrs); + ps->xinerama_nscrs = xcb_xinerama_query_screens_screen_info_length(xinerama_scrs); + + ps->xinerama_scr_regs = ccalloc(ps->xinerama_nscrs, region_t); + for (int i = 0; i < ps->xinerama_nscrs; ++i) { + const xcb_xinerama_screen_info_t *const s = &scrs[i]; + pixman_region32_init_rect(&ps->xinerama_scr_regs[i], s->x_org, s->y_org, + s->width, s->height); + } + free(xinerama_scrs); +} + +/** + * Find matched window. + * + * XXX move to win.c + */ +static inline struct managed_win *find_win_all(session_t *ps, const xcb_window_t wid) { + if (!wid || PointerRoot == wid || wid == ps->root || wid == ps->overlay) + return NULL; + + auto w = find_managed_win(ps, wid); + if (!w) + w = find_toplevel(ps, wid); + if (!w) + w = find_managed_window_or_parent(ps, wid); + return w; +} + +void queue_redraw(session_t *ps) { + // If --benchmark is used, redraw is always queued + if (!ps->redraw_needed && !ps->o.benchmark) { + ev_idle_start(ps->loop, &ps->draw_idle); + } + ps->redraw_needed = true; +} + +/** + * Get a region of the screen size. + */ +static inline void get_screen_region(session_t *ps, region_t *res) { + pixman_box32_t b = {.x1 = 0, .y1 = 0, .x2 = ps->root_width, .y2 = ps->root_height}; + pixman_region32_fini(res); + pixman_region32_init_rects(res, &b, 1); +} + +void add_damage(session_t *ps, const region_t *damage) { + // Ignore damage when screen isn't redirected + if (!ps->redirected) { + return; + } + + if (!damage) { + return; + } + log_trace("Adding damage: "); + dump_region(damage); + pixman_region32_union(ps->damage, ps->damage, (region_t *)damage); +} + +// === Fading === + +/** + * Get the time left before next fading point. + * + * In milliseconds. + */ +static double fade_timeout(session_t *ps) { + auto now = get_time_ms(); + if (ps->o.fade_delta + ps->fade_time < now) + return 0; + + auto diff = ps->o.fade_delta + ps->fade_time - now; + + diff = clamp(diff, 0, ps->o.fade_delta * 2); + + return (double)diff / 1000.0; +} + +/** + * Run fading on a window. + * + * @param steps steps of fading + * @return whether we are still in fading mode + */ +static bool run_fade(session_t *ps, struct managed_win **_w, long steps) { + auto w = *_w; + if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { + // We are not fading + assert(w->opacity_target == w->opacity); + return false; + } + + if (!win_should_fade(ps, w)) { + log_debug("Window %#010x %s doesn't need fading", w->base.id, w->name); + w->opacity = w->opacity_target; + } + if (w->opacity == w->opacity_target) { + // We have reached target opacity. + // We don't call win_check_fade_finished here because that could destroy + // the window, but we still need the damage info from this window + log_debug("Fading finished for window %#010x %s", w->base.id, w->name); + return false; + } + + if (steps) { + log_trace("Window %#010x (%s) opacity was: %lf", w->base.id, w->name, + w->opacity); + if (w->opacity < w->opacity_target) { + w->opacity = clamp(w->opacity + ps->o.fade_in_step * (double)steps, + 0.0, w->opacity_target); + } else { + w->opacity = clamp(w->opacity - ps->o.fade_out_step * (double)steps, + w->opacity_target, 1); + } + log_trace("... updated to: %lf", w->opacity); + } + + // Note even if opacity == opacity_target here, we still want to run preprocess + // one last time to finish state transition. So return true in that case too. + return true; +} + +// === Error handling === + +void discard_ignore(session_t *ps, unsigned long sequence) { + while (ps->ignore_head) { + if (sequence > ps->ignore_head->sequence) { + ignore_t *next = ps->ignore_head->next; + free(ps->ignore_head); + ps->ignore_head = next; + if (!ps->ignore_head) { + ps->ignore_tail = &ps->ignore_head; + } + } else { + break; + } + } +} + +static int should_ignore(session_t *ps, unsigned long sequence) { + if (ps == NULL) { + // Do not ignore errors until the session has been initialized + return false; + } + discard_ignore(ps, sequence); + return ps->ignore_head && ps->ignore_head->sequence == sequence; +} + +// === Windows === + +/** + * Determine the event mask for a window. + */ +uint32_t determine_evmask(session_t *ps, xcb_window_t wid, win_evmode_t mode) { + uint32_t evmask = 0; + struct managed_win *w = NULL; + + // Check if it's a mapped frame window + if (mode == WIN_EVMODE_FRAME || + ((w = find_managed_win(ps, wid)) && w->a.map_state == XCB_MAP_STATE_VIEWABLE)) { + evmask |= XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY; + if (!ps->o.use_ewmh_active_win) { + evmask |= XCB_EVENT_MASK_FOCUS_CHANGE; + } + } + + // Check if it's a mapped client window + if (mode == WIN_EVMODE_CLIENT || + ((w = find_toplevel(ps, wid)) && w->a.map_state == XCB_MAP_STATE_VIEWABLE)) { + evmask |= XCB_EVENT_MASK_PROPERTY_CHANGE; + } + + return evmask; +} + +/** + * Update current active window based on EWMH _NET_ACTIVE_WIN. + * + * Does not change anything if we fail to get the attribute or the window + * returned could not be found. + */ +void update_ewmh_active_win(session_t *ps) { + // Search for the window + xcb_window_t wid = + wid_get_prop_window(ps->c, ps->root, ps->atoms->a_NET_ACTIVE_WINDOW); + auto w = find_win_all(ps, wid); + + // Mark the window focused. No need to unfocus the previous one. + if (w) { + win_set_focused(ps, w); + } +} + +/** + * Recheck currently focused window and set its <code>w->focused</code> + * to true. + * + * @param ps current session + * @return struct _win of currently focused window, NULL if not found + */ +static void recheck_focus(session_t *ps) { + // Use EWMH _NET_ACTIVE_WINDOW if enabled + if (ps->o.use_ewmh_active_win) { + update_ewmh_active_win(ps); + return; + } + + // Determine the currently focused window so we can apply appropriate + // opacity on it + xcb_window_t wid = XCB_NONE; + xcb_get_input_focus_reply_t *reply = + xcb_get_input_focus_reply(ps->c, xcb_get_input_focus(ps->c), NULL); + + if (reply) { + wid = reply->focus; + free(reply); + } + + auto w = find_win_all(ps, wid); + + log_trace("%#010" PRIx32 " (%#010lx \"%s\") focused.", wid, + (w ? w->base.id : XCB_NONE), (w ? w->name : NULL)); + + // And we set the focus state here + if (w) { + win_set_focused(ps, w); + return; + } +} + +/** + * Rebuild cached <code>screen_reg</code>. + */ +static void rebuild_screen_reg(session_t *ps) { + get_screen_region(ps, &ps->screen_reg); +} + +/** + * Rebuild <code>shadow_exclude_reg</code>. + */ +static void rebuild_shadow_exclude_reg(session_t *ps) { + bool ret = parse_geometry(ps, ps->o.shadow_exclude_reg_str, &ps->shadow_exclude_reg); + if (!ret) + exit(1); +} + +/// Free up all the images and deinit the backend +static void destroy_backend(session_t *ps) { + win_stack_foreach_managed_safe(w, &ps->window_stack) { + // Wrapping up fading in progress + if (win_skip_fading(ps, w)) { + // `w` is freed by win_skip_fading + continue; + } + + if (ps->backend_data) { + // Unmapped windows could still have shadow images, but not pixmap + // images + assert(!w->win_image || w->state != WSTATE_UNMAPPED); + if (win_check_flags_any(w, WIN_FLAGS_IMAGES_STALE) && + w->state == WSTATE_MAPPED) { + log_warn("Stale flags set for mapped window %#010x " + "during backend destruction", + w->base.id); + assert(false); + } + // Unmapped windows can still have stale flags set, because their + // stale flags aren't handled until they are mapped. + win_clear_flags(w, WIN_FLAGS_IMAGES_STALE); + win_release_images(ps->backend_data, w); + } + free_paint(ps, &w->paint); + } + + if (ps->backend_data && ps->root_image) { + ps->backend_data->ops->release_image(ps->backend_data, ps->root_image); + ps->root_image = NULL; + } + + if (ps->backend_data) { + // deinit backend + if (ps->backend_blur_context) { + ps->backend_data->ops->destroy_blur_context( + ps->backend_data, ps->backend_blur_context); + ps->backend_blur_context = NULL; + } + ps->backend_data->ops->deinit(ps->backend_data); + ps->backend_data = NULL; + } +} + +static bool initialize_blur(session_t *ps) { + struct kernel_blur_args kargs; + struct gaussian_blur_args gargs; + struct box_blur_args bargs; + struct dual_kawase_blur_args dkargs; + + void *args = NULL; + switch (ps->o.blur_method) { + case BLUR_METHOD_BOX: + bargs.size = ps->o.blur_radius; + args = (void *)&bargs; + break; + case BLUR_METHOD_KERNEL: + kargs.kernel_count = ps->o.blur_kernel_count; + kargs.kernels = ps->o.blur_kerns; + args = (void *)&kargs; + break; + case BLUR_METHOD_GAUSSIAN: + gargs.size = ps->o.blur_radius; + gargs.deviation = ps->o.blur_deviation; + args = (void *)&gargs; + break; + case BLUR_METHOD_DUAL_KAWASE: + dkargs.size = ps->o.blur_radius; + dkargs.strength = ps->o.blur_strength; + args = (void *)&dkargs; + break; + default: return true; + } + + ps->backend_blur_context = ps->backend_data->ops->create_blur_context( + ps->backend_data, ps->o.blur_method, args); + return ps->backend_blur_context != NULL; +} + +/// Init the backend and bind all the window pixmap to backend images +static bool initialize_backend(session_t *ps) { + if (ps->o.experimental_backends) { + assert(!ps->backend_data); + // Reinitialize win_data + assert(backend_list[ps->o.backend]); + ps->backend_data = backend_list[ps->o.backend]->init(ps); + if (!ps->backend_data) { + log_fatal("Failed to initialize backend, aborting..."); + quit(ps); + return false; + } + ps->backend_data->ops = backend_list[ps->o.backend]; + + if (!initialize_blur(ps)) { + log_fatal("Failed to prepare for background blur, aborting..."); + ps->backend_data->ops->deinit(ps->backend_data); + ps->backend_data = NULL; + quit(ps); + return false; + } + + // window_stack shouldn't include window that's + // not in the hash table at this point. Since + // there cannot be any fading windows. + HASH_ITER2(ps->windows, _w) { + if (!_w->managed) { + continue; + } + auto w = (struct managed_win *)_w; + assert(w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED); + // We need to reacquire image + log_debug("Marking window %#010x (%s) for update after " + "redirection", + w->base.id, w->name); + win_set_flags(w, WIN_FLAGS_IMAGES_STALE); + ps->pending_updates = true; + } + } + + // The old backends binds pixmap lazily, nothing to do here + return true; +} + +/// Handle configure event of the root window +static void configure_root(session_t *ps) { + auto r = XCB_AWAIT(xcb_get_geometry, ps->c, ps->root); + if (!r) { + log_fatal("Failed to fetch root geometry"); + abort(); + } + + log_info("Root configuration changed, new geometry: %dx%d", r->width, r->height); + bool has_root_change = false; + if (ps->redirected) { + // On root window changes + if (ps->o.experimental_backends) { + assert(ps->backend_data); + has_root_change = ps->backend_data->ops->root_change != NULL; + } else { + // Old backend can handle root change + has_root_change = true; + } + + if (!has_root_change) { + // deinit/reinit backend and free up resources if the backend + // cannot handle root change + destroy_backend(ps); + } + free_paint(ps, &ps->tgt_buffer); + } + + ps->root_width = r->width; + ps->root_height = r->height; + + auto prop = x_get_prop(ps->c, ps->root, ps->atoms->a_NET_CURRENT_DESKTOP, + 1L, XCB_ATOM_CARDINAL, 32); + + ps->root_desktop_switch_direction = 0; + if (prop.nitems) { + ps->root_desktop_num = (int)*prop.c32; + } + + rebuild_screen_reg(ps); + rebuild_shadow_exclude_reg(ps); + + // Invalidate reg_ignore from the top + auto top_w = win_stack_find_next_managed(ps, &ps->window_stack); + if (top_w) { + rc_region_unref(&top_w->reg_ignore); + top_w->reg_ignore_valid = false; + } + + if (ps->redirected) { + for (int i = 0; i < ps->ndamage; i++) { + pixman_region32_clear(&ps->damage_ring[i]); + } + ps->damage = ps->damage_ring + ps->ndamage - 1; +#ifdef CONFIG_OPENGL + // GLX root change callback + if (BKEND_GLX == ps->o.backend && !ps->o.experimental_backends) { + glx_on_root_change(ps); + } +#endif + if (has_root_change) { + if (ps->backend_data != NULL) { + ps->backend_data->ops->root_change(ps->backend_data, ps); + } + // Old backend's root_change is not a specific function + } else { + if (!initialize_backend(ps)) { + log_fatal("Failed to re-initialize backend after root " + "change, aborting..."); + ps->quit = true; + /* TODO(yshui) only event handlers should request + * ev_break, otherwise it's too hard to keep track of what + * can break the event loop */ + ev_break(ps->loop, EVBREAK_ALL); + return; + } + + // Re-acquire the root pixmap. + root_damaged(ps); + } + force_repaint(ps); + } + return; +} + +static void handle_root_flags(session_t *ps) { + if ((ps->root_flags & ROOT_FLAGS_SCREEN_CHANGE) != 0) { + if (ps->o.xinerama_shadow_crop) { + cxinerama_upd_scrs(ps); + } + + if (ps->o.sw_opti && !ps->o.refresh_rate) { + update_refresh_rate(ps); + if (!ps->refresh_rate) { + log_warn("Refresh rate detection failed. swopti will be " + "temporarily disabled"); + } + } + ps->root_flags &= ~(uint64_t)ROOT_FLAGS_SCREEN_CHANGE; + } + + if ((ps->root_flags & ROOT_FLAGS_CONFIGURED) != 0) { + configure_root(ps); + ps->root_flags &= ~(uint64_t)ROOT_FLAGS_CONFIGURED; + } +} + +static struct managed_win * +paint_preprocess(session_t *ps, bool *fade_running, bool *animation_running) { + // XXX need better, more general name for `fade_running`. It really + // means if fade is still ongoing after the current frame is rendered. + // Same goes for `animation_running`. + struct managed_win *bottom = NULL; + *fade_running = false; + *animation_running = false; + auto now = get_time_ms(); + + // Fading step calculation + long steps = 0L; + if (ps->fade_time) { + assert(now >= ps->fade_time); + steps = (now - ps->fade_time) / ps->o.fade_delta; + } else { + // Reset fade_time if unset + ps->fade_time = now; + steps = 0L; + } + ps->fade_time += steps * ps->o.fade_delta; + + double animation_delta = 0; + if (ps->o.animations) { + if (!ps->animation_time) + ps->animation_time = now; + + animation_delta = (double)(now - ps->animation_time) / + (ps->o.animation_delta*100); + + if (ps->o.animation_force_steps) + animation_delta = min2(animation_delta, ps->o.animation_delta/1000); + } + + // First, let's process fading + win_stack_foreach_managed_safe(w, &ps->window_stack) { + const winmode_t mode_old = w->mode; + const bool was_painted = w->to_paint; + const double opacity_old = w->opacity; + + // IMPORTANT: These window animation steps must happen before any other + // [pre]processing. This is because it changes the window's geometry. + if (ps->o.animations && + !isnan(w->animation_progress) && w->animation_progress != 1.0 && + ps->o.wintype_option[w->window_type].animation != 0 && + win_is_mapped_in_x(w)) + { + double neg_displacement_x = + w->animation_dest_center_x - w->animation_center_x; + double neg_displacement_y = + w->animation_dest_center_y - w->animation_center_y; + double neg_displacement_w = w->animation_dest_w - w->animation_w; + double neg_displacement_h = w->animation_dest_h - w->animation_h; + double acceleration_x = + (ps->o.animation_stiffness * neg_displacement_x - + ps->o.animation_dampening * w->animation_velocity_x) / + ps->o.animation_window_mass; + double acceleration_y = + (ps->o.animation_stiffness * neg_displacement_y - + ps->o.animation_dampening * w->animation_velocity_y) / + ps->o.animation_window_mass; + double acceleration_w = + (ps->o.animation_stiffness * neg_displacement_w - + ps->o.animation_dampening * w->animation_velocity_w) / + ps->o.animation_window_mass; + double acceleration_h = + (ps->o.animation_stiffness * neg_displacement_h - + ps->o.animation_dampening * w->animation_velocity_h) / + ps->o.animation_window_mass; + w->animation_velocity_x += acceleration_x * animation_delta; + w->animation_velocity_y += acceleration_y * animation_delta; + w->animation_velocity_w += acceleration_w * animation_delta; + w->animation_velocity_h += acceleration_h * animation_delta; + + // Animate window geometry + double new_animation_x = + w->animation_center_x + w->animation_velocity_x * animation_delta; + double new_animation_y = + w->animation_center_y + w->animation_velocity_y * animation_delta; + double new_animation_w = + w->animation_w + w->animation_velocity_w * animation_delta; + double new_animation_h = + w->animation_h + w->animation_velocity_h * animation_delta; + + // Negative new width/height causes segfault and it can happen + // when clamping disabled and shading a window + if (new_animation_h < 0) + new_animation_h = 0; + + if (new_animation_w < 0) + new_animation_w = 0; + + if (ps->o.animation_clamping) { + w->animation_center_x = clamp( + new_animation_x, + min2(w->animation_center_x, w->animation_dest_center_x), + max2(w->animation_center_x, w->animation_dest_center_x)); + w->animation_center_y = clamp( + new_animation_y, + min2(w->animation_center_y, w->animation_dest_center_y), + max2(w->animation_center_y, w->animation_dest_center_y)); + w->animation_w = + clamp(new_animation_w, + min2(w->animation_w, w->animation_dest_w), + max2(w->animation_w, w->animation_dest_w)); + w->animation_h = + clamp(new_animation_h, + min2(w->animation_h, w->animation_dest_h), + max2(w->animation_h, w->animation_dest_h)); + } else { + w->animation_center_x = new_animation_x; + w->animation_center_y = new_animation_y; + w->animation_w = new_animation_w; + w->animation_h = new_animation_h; + } + + // Now we are done doing the math; we just need to submit our + // changes (if there are any). + + struct win_geometry old_g = w->g; + double old_animation_progress = w->animation_progress; + new_animation_x = round(w->animation_center_x - w->animation_w * 0.5); + new_animation_y = round(w->animation_center_y - w->animation_h * 0.5); + new_animation_w = round(w->animation_w); + new_animation_h = round(w->animation_h); + + bool position_changed = + new_animation_x != old_g.x || new_animation_y != old_g.y; + bool size_changed = + new_animation_w != old_g.width || new_animation_h != old_g.height; + bool geometry_changed = position_changed || size_changed; + + // Mark past window region with damage + if (was_painted && geometry_changed) + add_damage_from_win(ps, w); + + double x_dist = w->animation_dest_center_x - w->animation_center_x; + double y_dist = w->animation_dest_center_y - w->animation_center_y; + double w_dist = w->animation_dest_w - w->animation_w; + double h_dist = w->animation_dest_h - w->animation_h; + w->animation_progress = + 1.0 - w->animation_inv_og_distance * + sqrt(x_dist * x_dist + y_dist * y_dist + + w_dist * w_dist + h_dist * h_dist); + + // When clamping disabled we don't want the overlayed image to + // fade in again because process is moving to negative value + if (w->animation_progress < old_animation_progress) + w->animation_progress = old_animation_progress; + + w->g.x = (int16_t)new_animation_x; + w->g.y = (int16_t)new_animation_y; + w->g.width = (uint16_t)new_animation_w; + w->g.height = (uint16_t)new_animation_h; + + // Submit window size change + if (size_changed) { + win_on_win_size_change(ps, w); + + pixman_region32_clear(&w->bounding_shape); + pixman_region32_fini(&w->bounding_shape); + pixman_region32_init_rect(&w->bounding_shape, 0, 0, + (uint)w->widthb, (uint)w->heightb); + + if (w->state != WSTATE_DESTROYING) + win_clear_flags(w, WIN_FLAGS_PIXMAP_STALE); + + win_process_image_flags(ps, w); + } + // Mark new window region with damage + if (was_painted && geometry_changed) { + add_damage_from_win(ps, w); + w->reg_ignore_valid = false; + } + + // We can't check for 1 here as sometimes 1 = 0.999999999999999 + // in case of floating numbers + if (w->animation_progress >= 0.999999999) { + w->animation_progress = 1; + w->animation_velocity_x = 0.0; + w->animation_velocity_y = 0.0; + w->animation_velocity_w = 0.0; + w->animation_velocity_h = 0.0; + } + + if (!ps->root_desktop_switch_direction) { + if (w->state == WSTATE_UNMAPPING || w->state == WSTATE_DESTROYING) { + steps = 0; + double new_opacity = clamp( + w->opacity_target_old-w->animation_progress, + w->opacity_target, 1); + + if (new_opacity < w->opacity) + w->opacity = new_opacity; + + } else if (w->state == WSTATE_MAPPING) { + steps = 0; + double new_opacity = clamp( + w->animation_progress, + 0.0, w->opacity_target); + + if (new_opacity > w->opacity) + w->opacity = new_opacity; + } + } + + *animation_running = true; + } + + if (win_should_dim(ps, w) != w->dim) { + w->dim = win_should_dim(ps, w); + add_damage_from_win(ps, w); + } + + if (w->opacity != w->opacity_target) { + // Run fading + if (run_fade(ps, &w, steps)) { + *fade_running = true; + } + + // Add window to damaged area if its opacity changes + // If was_painted == false, and to_paint is also false, we don't care + // If was_painted == false, but to_paint is true, damage will be added in + // the loop below + if (was_painted && w->opacity != opacity_old) { + add_damage_from_win(ps, w); + } + + if (win_check_fade_finished(ps, w)) { + // the window has been destroyed because fading finished + continue; + } + + if (win_has_frame(w)) { + w->frame_opacity = ps->o.frame_opacity; + } else { + w->frame_opacity = 1.0; + } + + // Update window mode + w->mode = win_calc_mode(w); + + // Destroy all reg_ignore above when frame opaque state changes on + // SOLID mode + if (was_painted && w->mode != mode_old) { + w->reg_ignore_valid = false; + } + } + } + + if (*animation_running) + ps->animation_time = now; + + // Opacity will not change, from now on. + rc_region_t *last_reg_ignore = rc_region_new(); + + bool unredir_possible = false; + // Track whether it's the highest window to paint + bool is_highest = true; + bool reg_ignore_valid = true; + win_stack_foreach_managed(w, &ps->window_stack) { + __label__ skip_window; + bool to_paint = true; + // w->to_paint remembers whether this window is painted last time + const bool was_painted = w->to_paint; + + // Destroy reg_ignore if some window above us invalidated it + if (!reg_ignore_valid) { + rc_region_unref(&w->reg_ignore); + } + + // log_trace("%d %d %s", w->a.map_state, w->ever_damaged, w->name); + + // Give up if it's not damaged or invisible, or it's unmapped and its + // pixmap is gone (for example due to a ConfigureNotify), or when it's + // excluded + if (w->state == WSTATE_UNMAPPED || + unlikely(w->base.id == ps->debug_window || + w->client_win == ps->debug_window)) { + + if (!*fade_running || w->opacity == w->opacity_target) + to_paint = false; + + } else if (!w->ever_damaged && w->state != WSTATE_UNMAPPING && + w->state != WSTATE_DESTROYING) { + // Unmapping clears w->ever_damaged, but the fact that the window + // is fading out means it must have been damaged when it was still + // mapped (because unmap_win_start will skip fading if it wasn't), + // so we still need to paint it. + log_trace("Window %#010x (%s) will not be painted because it has " + "not received any damages", + w->base.id, w->name); + to_paint = false; + } else if (unlikely(w->g.x + w->g.width < 1 || w->g.y + w->g.height < 1 || + w->g.x >= ps->root_width || w->g.y >= ps->root_height)) { + log_trace("Window %#010x (%s) will not be painted because it is " + "positioned outside of the screen", + w->base.id, w->name); + to_paint = false; + } else if (unlikely((double)w->opacity * MAX_ALPHA < 1 && !w->blur_background)) { + /* TODO(yshui) for consistency, even a window has 0 opacity, we + * still probably need to blur its background, so to_paint + * shouldn't be false for them. */ + log_trace("Window %#010x (%s) will not be painted because it has " + "0 opacity", + w->base.id, w->name); + to_paint = false; + } else if (w->paint_excluded) { + log_trace("Window %#010x (%s) will not be painted because it is " + "excluded from painting", + w->base.id, w->name); + to_paint = false; + } else if (unlikely((w->flags & WIN_FLAGS_IMAGE_ERROR) != 0)) { + log_trace("Window %#010x (%s) will not be painted because it has " + "image errors", + w->base.id, w->name); + to_paint = false; + } + // log_trace("%s %d %d %d", w->name, to_paint, w->opacity, + // w->paint_excluded); + + // Add window to damaged area if its painting status changes + // or opacity changes + if (to_paint != was_painted) { + w->reg_ignore_valid = false; + add_damage_from_win(ps, w); + } + + // to_paint will never change after this point + if (!to_paint) { + goto skip_window; + } + + log_trace("Window %#010x (%s) will be painted", w->base.id, w->name); + + // Calculate shadow opacity + w->shadow_opacity = ps->o.shadow_opacity * w->opacity * ps->o.frame_opacity; + + // Generate ignore region for painting to reduce GPU load + if (!w->reg_ignore) { + w->reg_ignore = rc_region_ref(last_reg_ignore); + } + + // If the window is solid, or we enabled clipping for transparent windows, + // we add the window region to the ignored region + // Otherwise last_reg_ignore shouldn't change + if ((w->mode != WMODE_TRANS && !ps->o.force_win_blend) || + ps->o.transparent_clipping) { + // w->mode == WMODE_SOLID or WMODE_FRAME_TRANS + region_t *tmp = rc_region_new(); + if (w->mode == WMODE_SOLID) { + *tmp = + win_get_bounding_shape_global_without_corners_by_val(w); + } else { + // w->mode == WMODE_FRAME_TRANS + win_get_region_noframe_local_without_corners(w, tmp); + pixman_region32_intersect(tmp, tmp, &w->bounding_shape); + pixman_region32_translate(tmp, w->g.x, w->g.y); + } + + pixman_region32_union(tmp, tmp, last_reg_ignore); + rc_region_unref(&last_reg_ignore); + last_reg_ignore = tmp; + } + + // (Un)redirect screen + // We could definitely unredirect the screen when there's no window to + // paint, but this is typically unnecessary, may cause flickering when + // fading is enabled, and could create inconsistency when the wallpaper + // is not correctly set. + if (ps->o.unredir_if_possible && is_highest) { + if (w->mode == WMODE_SOLID && !ps->o.force_win_blend && + win_is_fullscreen(ps, w) && !w->unredir_if_possible_excluded) { + unredir_possible = true; + } + } + + // Unredirect screen if some window is requesting compositor bypass, even + // if that window is not on the top. + if (ps->o.unredir_if_possible && win_is_bypassing_compositor(ps, w) && + !w->unredir_if_possible_excluded) { + // Here we deviate from EWMH a bit. EWMH says we must not + // unredirect the screen if the window requesting bypassing would + // look different after unredirecting. Instead we always follow + // the request. + unredir_possible = true; + } + + w->prev_trans = bottom; + if (bottom) { + w->stacking_rank = bottom->stacking_rank + 1; + } else { + w->stacking_rank = 0; + } + bottom = w; + + // If the screen is not redirected and the window has redir_ignore set, + // this window should not cause the screen to become redirected + if (!(ps->o.wintype_option[w->window_type].redir_ignore && !ps->redirected)) { + is_highest = false; + } + + skip_window: + reg_ignore_valid = reg_ignore_valid && w->reg_ignore_valid; + w->reg_ignore_valid = true; + + // Avoid setting w->to_paint if w is freed + if (w) { + w->to_paint = to_paint; + } + } + + rc_region_unref(&last_reg_ignore); + + // If possible, unredirect all windows and stop painting + if (ps->o.redirected_force != UNSET) { + unredir_possible = !ps->o.redirected_force; + } else if (ps->o.unredir_if_possible && is_highest && !ps->redirected) { + // If there's no window to paint, and the screen isn't redirected, + // don't redirect it. + unredir_possible = true; + } + if (unredir_possible) { + if (ps->redirected) { + if (!ps->o.unredir_if_possible_delay || ps->tmout_unredir_hit) { + unredirect(ps); + } else if (!ev_is_active(&ps->unredir_timer)) { + ev_timer_set( + &ps->unredir_timer, + (double)ps->o.unredir_if_possible_delay / 1000.0, 0); + ev_timer_start(ps->loop, &ps->unredir_timer); + } + } + } else { + ev_timer_stop(ps->loop, &ps->unredir_timer); + if (!ps->redirected) { + if (!redirect_start(ps)) { + return NULL; + } + } + } + + return bottom; +} + +void root_damaged(session_t *ps) { + if (ps->root_tile_paint.pixmap) { + free_root_tile(ps); + } + + if (!ps->redirected) { + return; + } + + if (ps->backend_data) { + if (ps->root_image) { + ps->backend_data->ops->release_image(ps->backend_data, ps->root_image); + } + auto pixmap = x_get_root_back_pixmap(ps->c, ps->root, ps->atoms); + if (pixmap != XCB_NONE) { + ps->root_image = ps->backend_data->ops->bind_pixmap( + ps->backend_data, pixmap, x_get_visual_info(ps->c, ps->vis), false); + ps->backend_data->ops->set_image_property( + ps->backend_data, IMAGE_PROPERTY_EFFECTIVE_SIZE, + ps->root_image, (int[]){ps->root_width, ps->root_height}); + } + } + + // Mark screen damaged + force_repaint(ps); +} + +/** + * Xlib error handler function. + */ +static int xerror(Display attr_unused *dpy, XErrorEvent *ev) { + if (!should_ignore(ps_g, ev->serial)) { + x_print_error(ev->serial, ev->request_code, ev->minor_code, ev->error_code); + } + return 0; +} + +/** + * XCB error handler function. + */ +void ev_xcb_error(session_t *ps, xcb_generic_error_t *err) { + if (!should_ignore(ps, err->sequence)) { + x_print_error(err->sequence, err->major_code, err->minor_code, err->error_code); + } +} + +/** + * Force a full-screen repaint. + */ +void force_repaint(session_t *ps) { + assert(pixman_region32_not_empty(&ps->screen_reg)); + queue_redraw(ps); + add_damage(ps, &ps->screen_reg); +} + +#ifdef CONFIG_DBUS +/** @name DBus hooks + */ +///@{ + +/** + * Set no_fading_openclose option. + * + * Don't affect fading already in progress + */ +void opts_set_no_fading_openclose(session_t *ps, bool newval) { + ps->o.no_fading_openclose = newval; +} + +//!@} +#endif + +/** + * Setup window properties, then register us with the compositor selection (_NET_WM_CM_S) + * + * @return 0 if success, 1 if compositor already running, -1 if error. + */ +static int register_cm(session_t *ps) { + assert(!ps->reg_win); + + ps->reg_win = x_new_id(ps->c); + auto e = xcb_request_check( + ps->c, xcb_create_window_checked(ps->c, XCB_COPY_FROM_PARENT, ps->reg_win, ps->root, + 0, 0, 1, 1, 0, XCB_NONE, ps->vis, 0, NULL)); + + if (e) { + log_fatal("Failed to create window."); + free(e); + return -1; + } + + const xcb_atom_t prop_atoms[] = { + ps->atoms->aWM_NAME, + ps->atoms->a_NET_WM_NAME, + ps->atoms->aWM_ICON_NAME, + }; + + const bool prop_is_utf8[] = {false, true, false}; + + // Set names and classes + for (size_t i = 0; i < ARR_SIZE(prop_atoms); i++) { + e = xcb_request_check( + ps->c, xcb_change_property_checked( + ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, prop_atoms[i], + prop_is_utf8[i] ? ps->atoms->aUTF8_STRING : XCB_ATOM_STRING, + 8, strlen("picom"), "picom")); + if (e) { + log_error_x_error(e, "Failed to set window property %d", + prop_atoms[i]); + free(e); + } + } + + const char picom_class[] = "picom\0picom"; + e = xcb_request_check( + ps->c, xcb_change_property_checked(ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, + ps->atoms->aWM_CLASS, XCB_ATOM_STRING, 8, + ARR_SIZE(picom_class), picom_class)); + if (e) { + log_error_x_error(e, "Failed to set the WM_CLASS property"); + free(e); + } + + // Set WM_CLIENT_MACHINE. As per EWMH, because we set _NET_WM_PID, we must also + // set WM_CLIENT_MACHINE. + { + const auto hostname_max = (unsigned long)sysconf(_SC_HOST_NAME_MAX); + char *hostname = malloc(hostname_max); + + if (gethostname(hostname, hostname_max) == 0) { + e = xcb_request_check( + ps->c, xcb_change_property_checked( + ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, + ps->atoms->aWM_CLIENT_MACHINE, XCB_ATOM_STRING, 8, + (uint32_t)strlen(hostname), hostname)); + if (e) { + log_error_x_error(e, "Failed to set the WM_CLIENT_MACHINE" + " property"); + free(e); + } + } else { + log_error_errno("Failed to get hostname"); + } + + free(hostname); + } + + // Set _NET_WM_PID + { + auto pid = getpid(); + xcb_change_property(ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, + ps->atoms->a_NET_WM_PID, XCB_ATOM_CARDINAL, 32, 1, &pid); + } + + // Set COMPTON_VERSION + e = xcb_request_check( + ps->c, xcb_change_property_checked( + ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, + get_atom(ps->atoms, "COMPTON_VERSION"), XCB_ATOM_STRING, 8, + (uint32_t)strlen(COMPTON_VERSION), COMPTON_VERSION)); + if (e) { + log_error_x_error(e, "Failed to set COMPTON_VERSION."); + free(e); + } + + // Acquire X Selection _NET_WM_CM_S? + if (!ps->o.no_x_selection) { + const char register_prop[] = "_NET_WM_CM_S"; + xcb_atom_t atom; + + char *buf = NULL; + if (asprintf(&buf, "%s%d", register_prop, ps->scr) < 0) { + log_fatal("Failed to allocate memory"); + return -1; + } + atom = get_atom(ps->atoms, buf); + free(buf); + + xcb_get_selection_owner_reply_t *reply = xcb_get_selection_owner_reply( + ps->c, xcb_get_selection_owner(ps->c, atom), NULL); + + if (reply && reply->owner != XCB_NONE) { + // Another compositor already running + free(reply); + return 1; + } + free(reply); + xcb_set_selection_owner(ps->c, ps->reg_win, atom, 0); + } + + return 0; +} + +/** + * Write PID to a file. + */ +static inline bool write_pid(session_t *ps) { + if (!ps->o.write_pid_path) { + return true; + } + + FILE *f = fopen(ps->o.write_pid_path, "w"); + if (unlikely(!f)) { + log_error("Failed to write PID to \"%s\".", ps->o.write_pid_path); + return false; + } + + fprintf(f, "%ld\n", (long)getpid()); + fclose(f); + + return true; +} + +/** + * Update refresh rate info with X Randr extension. + */ +void update_refresh_rate(session_t *ps) { + xcb_randr_get_screen_info_reply_t *randr_info = xcb_randr_get_screen_info_reply( + ps->c, xcb_randr_get_screen_info(ps->c, ps->root), NULL); + + if (!randr_info) + return; + ps->refresh_rate = randr_info->rate; + free(randr_info); + + if (ps->refresh_rate) + ps->refresh_intv = US_PER_SEC / ps->refresh_rate; + else + ps->refresh_intv = 0; +} + +/** + * Initialize refresh-rated based software optimization. + * + * @return true for success, false otherwise + */ +static bool swopti_init(session_t *ps) { + log_warn("--sw-opti is going to be deprecated. If you get real benefits from " + "using " + "this option, please open an issue to let us know."); + // Prepare refresh rate + // Check if user provides one + ps->refresh_rate = ps->o.refresh_rate; + if (ps->refresh_rate) + ps->refresh_intv = US_PER_SEC / ps->refresh_rate; + + // Auto-detect refresh rate otherwise + if (!ps->refresh_rate && ps->randr_exists) { + update_refresh_rate(ps); + } + + // Turn off vsync_sw if we can't get the refresh rate + if (!ps->refresh_rate) + return false; + + return true; +} + +/** + * Modify a struct timeval timeout value to render at a fixed pace. + * + * @param ps current session + * @param[in,out] ptv pointer to the timeout + */ +static double swopti_handle_timeout(session_t *ps) { + if (!ps->refresh_intv) + return 0; + + // Get the microsecond offset of the time when the we reach the timeout + // I don't think a 32-bit long could overflow here. + long offset = (get_time_timeval().tv_usec - ps->paint_tm_offset) % ps->refresh_intv; + // XXX this formula dones't work if refresh rate is not a whole number + if (offset < 0) + offset += ps->refresh_intv; + + // If the target time is sufficiently close to a refresh time, don't add + // an offset, to avoid certain blocking conditions. + if (offset < SWOPTI_TOLERANCE || offset > ps->refresh_intv - SWOPTI_TOLERANCE) + return 0; + + // Add an offset so we wait until the next refresh after timeout + return (double)(ps->refresh_intv - offset) / 1e6; +} + +/** + * Initialize X composite overlay window. + */ +static bool init_overlay(session_t *ps) { + xcb_composite_get_overlay_window_reply_t *reply = + xcb_composite_get_overlay_window_reply( + ps->c, xcb_composite_get_overlay_window(ps->c, ps->root), NULL); + if (reply) { + ps->overlay = reply->overlay_win; + free(reply); + } else { + ps->overlay = XCB_NONE; + } + if (ps->overlay != XCB_NONE) { + // Set window region of the overlay window, code stolen from + // compiz-0.8.8 + if (!XCB_AWAIT_VOID(xcb_shape_mask, ps->c, XCB_SHAPE_SO_SET, + XCB_SHAPE_SK_BOUNDING, ps->overlay, 0, 0, 0)) { + log_fatal("Failed to set the bounding shape of overlay, giving " + "up."); + return false; + } + if (!XCB_AWAIT_VOID(xcb_shape_rectangles, ps->c, XCB_SHAPE_SO_SET, + XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, + ps->overlay, 0, 0, 0, NULL)) { + log_fatal("Failed to set the input shape of overlay, giving up."); + return false; + } + + // Listen to Expose events on the overlay + xcb_change_window_attributes(ps->c, ps->overlay, XCB_CW_EVENT_MASK, + (const uint32_t[]){XCB_EVENT_MASK_EXPOSURE}); + + // Retrieve DamageNotify on root window if we are painting on an + // overlay + // root_damage = XDamageCreate(ps->dpy, root, XDamageReportNonEmpty); + + // Unmap the overlay, we will map it when needed in redirect_start + XCB_AWAIT_VOID(xcb_unmap_window, ps->c, ps->overlay); + } else { + log_error("Cannot get X Composite overlay window. Falling " + "back to painting on root window."); + } + log_debug("overlay = %#010x", ps->overlay); + + return true; +} + +static bool init_debug_window(session_t *ps) { + xcb_colormap_t colormap = x_new_id(ps->c); + ps->debug_window = x_new_id(ps->c); + + auto err = xcb_request_check( + ps->c, xcb_create_colormap_checked(ps->c, XCB_COLORMAP_ALLOC_NONE, colormap, + ps->root, ps->vis)); + if (err) { + goto err_out; + } + + err = xcb_request_check( + ps->c, xcb_create_window_checked(ps->c, (uint8_t)ps->depth, ps->debug_window, + ps->root, 0, 0, to_u16_checked(ps->root_width), + to_u16_checked(ps->root_height), 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, ps->vis, + XCB_CW_COLORMAP, (uint32_t[]){colormap, 0})); + if (err) { + goto err_out; + } + + err = xcb_request_check(ps->c, xcb_map_window(ps->c, ps->debug_window)); + if (err) { + goto err_out; + } + return true; + +err_out: + free(err); + return false; +} + +xcb_window_t session_get_target_window(session_t *ps) { + if (ps->o.debug_mode) { + return ps->debug_window; + } + return ps->overlay != XCB_NONE ? ps->overlay : ps->root; +} + +uint8_t session_redirection_mode(session_t *ps) { + if (ps->o.debug_mode) { + // If the backend is not rendering to the screen, we don't need to + // take over the screen. + assert(ps->o.experimental_backends); + return XCB_COMPOSITE_REDIRECT_AUTOMATIC; + } + if (ps->o.experimental_backends && !backend_list[ps->o.backend]->present) { + // if the backend doesn't render anything, we don't need to take over the + // screen. + return XCB_COMPOSITE_REDIRECT_AUTOMATIC; + } + return XCB_COMPOSITE_REDIRECT_MANUAL; +} + +/** + * Redirect all windows. + * + * @return whether the operation succeeded or not + */ +static bool redirect_start(session_t *ps) { + assert(!ps->redirected); + log_debug("Redirecting the screen."); + + // Map overlay window. Done firstly according to this: + // https://bugzilla.gnome.org/show_bug.cgi?id=597014 + if (ps->overlay != XCB_NONE) { + xcb_map_window(ps->c, ps->overlay); + } + + bool success = XCB_AWAIT_VOID(xcb_composite_redirect_subwindows, ps->c, ps->root, + session_redirection_mode(ps)); + if (!success) { + log_fatal("Another composite manager is already running " + "(and does not handle _NET_WM_CM_Sn correctly)"); + return false; + } + + x_sync(ps->c); + + if (!initialize_backend(ps)) { + return false; + } + + if (ps->o.experimental_backends) { + assert(ps->backend_data); + ps->ndamage = ps->backend_data->ops->max_buffer_age; + } else { + ps->ndamage = maximum_buffer_age(ps); + } + ps->damage_ring = ccalloc(ps->ndamage, region_t); + ps->damage = ps->damage_ring + ps->ndamage - 1; + + for (int i = 0; i < ps->ndamage; i++) { + pixman_region32_init(&ps->damage_ring[i]); + } + + // Must call XSync() here + x_sync(ps->c); + + ps->redirected = true; + ps->first_frame = true; + + // Re-detect driver since we now have a backend + ps->drivers = detect_driver(ps->c, ps->backend_data, ps->root); + apply_driver_workarounds(ps, ps->drivers); + + root_damaged(ps); + + // Repaint the whole screen + force_repaint(ps); + log_debug("Screen redirected."); + return true; +} + +/** + * Unredirect all windows. + */ +static void unredirect(session_t *ps) { + assert(ps->redirected); + log_debug("Unredirecting the screen."); + + destroy_backend(ps); + + xcb_composite_unredirect_subwindows(ps->c, ps->root, session_redirection_mode(ps)); + // Unmap overlay window + if (ps->overlay != XCB_NONE) { + xcb_unmap_window(ps->c, ps->overlay); + } + + // Free the damage ring + for (int i = 0; i < ps->ndamage; ++i) { + pixman_region32_fini(&ps->damage_ring[i]); + } + ps->ndamage = 0; + free(ps->damage_ring); + ps->damage_ring = ps->damage = NULL; + + // Must call XSync() here + x_sync(ps->c); + + ps->redirected = false; + log_debug("Screen unredirected."); +} + +// Handle queued events before we go to sleep +static void handle_queued_x_events(EV_P attr_unused, ev_prepare *w, int revents attr_unused) { + session_t *ps = session_ptr(w, event_check); + xcb_generic_event_t *ev; + while ((ev = xcb_poll_for_queued_event(ps->c))) { + ev_handle(ps, ev); + free(ev); + }; + // Flush because if we go into sleep when there is still + // requests in the outgoing buffer, they will not be sent + // for an indefinite amount of time. + // Use XFlush here too, we might still use some Xlib functions + // because OpenGL. + XFlush(ps->dpy); + xcb_flush(ps->c); + int err = xcb_connection_has_error(ps->c); + if (err) { + log_fatal("X11 server connection broke (error %d)", err); + exit(1); + } +} + +static void handle_new_windows(session_t *ps) { + list_foreach_safe(struct win, w, &ps->window_stack, stack_neighbour) { + if (w->is_new) { + auto new_w = fill_win(ps, w); + if (!new_w->managed) { + continue; + } + auto mw = (struct managed_win *)new_w; + if (mw->a.map_state == XCB_MAP_STATE_VIEWABLE) { + win_set_flags(mw, WIN_FLAGS_MAPPED); + + // This window might be damaged before we called fill_win + // and created the damage handle. And there is no way for + // us to find out. So just blindly mark it damaged + mw->ever_damaged = true; + } + } + } +} + +static void refresh_windows(session_t *ps) { + win_stack_foreach_managed(w, &ps->window_stack) { + win_process_update_flags(ps, w); + } +} + +static void refresh_images(session_t *ps) { + win_stack_foreach_managed(w, &ps->window_stack) { + win_process_image_flags(ps, w); + } +} + +/** + * Unredirection timeout callback. + */ +static void tmout_unredir_callback(EV_P attr_unused, ev_timer *w, int revents attr_unused) { + session_t *ps = session_ptr(w, unredir_timer); + ps->tmout_unredir_hit = true; + queue_redraw(ps); +} + +static void fade_timer_callback(EV_P attr_unused, ev_timer *w, int revents attr_unused) { + session_t *ps = session_ptr(w, fade_timer); + queue_redraw(ps); +} + +static void animation_timer_callback(EV_P attr_unused, ev_timer *w, int revents attr_unused) { + session_t *ps = session_ptr(w, animation_timer); + queue_redraw(ps); +} + +static void handle_pending_updates(EV_P_ struct session *ps) { + if (ps->pending_updates) { + log_debug("Delayed handling of events, entering critical section"); + auto e = xcb_request_check(ps->c, xcb_grab_server_checked(ps->c)); + if (e) { + log_fatal_x_error(e, "failed to grab x server"); + return quit(ps); + } + + ps->server_grabbed = true; + + // Catching up with X server + handle_queued_x_events(EV_A_ & ps->event_check, 0); + + // Call fill_win on new windows + handle_new_windows(ps); + + // Handle screen changes + // This HAS TO be called before refresh_windows, as handle_root_flags + // could call configure_root, which will release images and mark them + // stale. + handle_root_flags(ps); + + // Process window flags (window mapping) + refresh_windows(ps); + + { + auto r = xcb_get_input_focus_reply( + ps->c, xcb_get_input_focus(ps->c), NULL); + if (!ps->active_win || (r && r->focus != ps->active_win->base.id)) { + recheck_focus(ps); + } + free(r); + } + + // Process window flags (stale images) + refresh_images(ps); + + e = xcb_request_check(ps->c, xcb_ungrab_server_checked(ps->c)); + if (e) { + log_fatal_x_error(e, "failed to ungrab x server"); + return quit(ps); + } + + ps->server_grabbed = false; + ps->pending_updates = false; + log_debug("Exited critical section"); + } +} + +static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { + handle_pending_updates(EV_A_ ps); + + if (ps->first_frame) { + // If we are still rendering the first frame, if some of the windows are + // unmapped/destroyed during the above handle_pending_updates() call, they + // won't have pixmap before we rendered it, causing us to crash. + // But we will only render them if they are in fading. So we just skip + // fading for all windows here. + // + // Using foreach_safe here since skipping fading can cause window to be + // freed if it's destroyed. + win_stack_foreach_managed_safe(w, &ps->window_stack) { + auto _ attr_unused = win_skip_fading(ps, w); + } + } + + if (ps->o.benchmark) { + if (ps->o.benchmark_wid) { + auto w = find_managed_win(ps, ps->o.benchmark_wid); + if (!w) { + log_fatal("Couldn't find specified benchmark window."); + exit(1); + } + add_damage_from_win(ps, w); + } else { + force_repaint(ps); + } + } + + /* TODO(yshui) Have a stripped down version of paint_preprocess that is used when + * screen is not redirected. its sole purpose should be to decide whether the + * screen should be redirected. */ + bool fade_running = false; + bool animation_running = false; + bool was_redirected = ps->redirected; + auto bottom = paint_preprocess(ps, &fade_running, &animation_running); + ps->tmout_unredir_hit = false; + + if (!was_redirected && ps->redirected) { + // paint_preprocess redirected the screen, which might change the state of + // some of the windows (e.g. the window image might become stale). + // so we rerun _draw_callback to make sure the rendering decision we make + // is up-to-date, and all the new flags got handled. + // + // TODO(yshui) This is not ideal, we should try to avoid setting window + // flags in paint_preprocess. + log_debug("Re-run _draw_callback"); + return draw_callback_impl(EV_A_ ps, revents); + } + + // Start/stop fade timer depends on whether window are fading + if (!fade_running && ev_is_active(&ps->fade_timer)) { + ev_timer_stop(EV_A_ & ps->fade_timer); + } else if (fade_running && !ev_is_active(&ps->fade_timer)) { + ev_timer_set(&ps->fade_timer, fade_timeout(ps), 0); + ev_timer_start(EV_A_ & ps->fade_timer); + } + // Start/stop animation timer depends on whether windows are animating + if (!animation_running && ev_is_active(&ps->animation_timer)) { + ev_timer_stop(EV_A_ & ps->animation_timer); + } else if (animation_running && !ev_is_active(&ps->animation_timer)) { + ev_timer_set(&ps->animation_timer, 0, 0); + ev_timer_start(EV_A_ & ps->animation_timer); + } + + // If the screen is unredirected, free all_damage to stop painting + if (ps->redirected && ps->o.stoppaint_force != ON) { + static int paint = 0; + + log_trace("Render start, frame %d", paint); + if (ps->o.experimental_backends) { + paint_all_new(ps, bottom, false); + } else { + paint_all(ps, bottom, false); + } + log_trace("Render end"); + + ps->first_frame = false; + paint++; + if (ps->o.benchmark && paint >= ps->o.benchmark) { + exit(0); + } + } + + if (!fade_running) { + ps->fade_time = 0L; + } + if (!animation_running) { + ps->animation_time = 0L; + ps->root_desktop_switch_direction = 0; + } + + // TODO(yshui) Investigate how big the X critical section needs to be. There are + // suggestions that rendering should be in the critical section as well. + + ps->redraw_needed = false; +} + +static void draw_callback(EV_P_ ev_idle *w, int revents) { + // This function is not used if we are using --swopti + session_t *ps = session_ptr(w, draw_idle); + + draw_callback_impl(EV_A_ ps, revents); + + // Don't do painting non-stop unless we are in benchmark mode + if (!ps->o.benchmark) { + ev_idle_stop(EV_A_ & ps->draw_idle); + } +} + +static void delayed_draw_timer_callback(EV_P_ ev_timer *w, int revents) { + session_t *ps = session_ptr(w, delayed_draw_timer); + draw_callback_impl(EV_A_ ps, revents); + + // We might have stopped the ev_idle in delayed_draw_callback, + // so we restart it if we are in benchmark mode + if (ps->o.benchmark) + ev_idle_start(EV_A_ & ps->draw_idle); +} + +static void delayed_draw_callback(EV_P_ ev_idle *w, int revents) { + // This function is only used if we are using --swopti + session_t *ps = session_ptr(w, draw_idle); + assert(ps->redraw_needed); + assert(!ev_is_active(&ps->delayed_draw_timer)); + + double delay = swopti_handle_timeout(ps); + if (delay < 1e-6) { + if (!ps->o.benchmark) { + ev_idle_stop(EV_A_ & ps->draw_idle); + } + return draw_callback_impl(EV_A_ ps, revents); + } + + // This is a little bit hacky. When we get to this point in code, we need + // to update the screen , but we will only be updating after a delay, So + // we want to stop the ev_idle, so this callback doesn't get call repeatedly + // during the delay, we also want queue_redraw to not restart the ev_idle. + // So we stop ev_idle and leave ps->redraw_needed to be true. (effectively, + // ps->redraw_needed means if redraw is needed or if draw is in progress). + // + // We do this anyway even if we are in benchmark mode. That means we will + // have to restart draw_idle after the draw actually happened when we are in + // benchmark mode. + ev_idle_stop(EV_A_ & ps->draw_idle); + + ev_timer_set(&ps->delayed_draw_timer, delay, 0); + ev_timer_start(EV_A_ & ps->delayed_draw_timer); +} + +static void x_event_callback(EV_P attr_unused, ev_io *w, int revents attr_unused) { + session_t *ps = (session_t *)w; + xcb_generic_event_t *ev = xcb_poll_for_event(ps->c); + if (ev) { + ev_handle(ps, ev); + free(ev); + } +} + +/** + * Turn on the program reset flag. + * + * This will result in the compostior resetting itself after next paint. + */ +static void reset_enable(EV_P_ ev_signal *w attr_unused, int revents attr_unused) { + log_info("picom is resetting..."); + ev_break(EV_A_ EVBREAK_ALL); +} + +static void exit_enable(EV_P attr_unused, ev_signal *w, int revents attr_unused) { + session_t *ps = session_ptr(w, int_signal); + log_info("picom is quitting..."); + quit(ps); +} + +static void config_file_change_cb(void *_ps) { + auto ps = (struct session *)_ps; + reset_enable(ps->loop, NULL, 0); +} + +/** + * Initialize a session. + * + * @param argc number of commandline arguments + * @param argv commandline arguments + * @param dpy the X Display + * @param config_file the path to the config file + * @param all_xerros whether we should report all X errors + * @param fork whether we will fork after initialization + */ +static session_t *session_init(int argc, char **argv, Display *dpy, + const char *config_file, bool all_xerrors, bool fork) { + static const session_t s_def = { + .backend_data = NULL, + .dpy = NULL, + .scr = 0, + .c = NULL, + .vis = 0, + .depth = 0, + .root = XCB_NONE, + .root_height = 0, + .root_width = 0, + // .root_damage = XCB_NONE, + .overlay = XCB_NONE, + .root_tile_fill = false, + .root_tile_paint = PAINT_INIT, + .tgt_picture = XCB_NONE, + .tgt_buffer = PAINT_INIT, + .reg_win = XCB_NONE, +#ifdef CONFIG_OPENGL + .glx_prog_win = GLX_PROG_MAIN_INIT, +#endif + .redirected = false, + .alpha_picts = NULL, + .fade_time = 0L, + .animation_time = 0L, + .ignore_head = NULL, + .ignore_tail = NULL, + .quit = false, + + .expose_rects = NULL, + .size_expose = 0, + .n_expose = 0, + + .windows = NULL, + .active_win = NULL, + .active_leader = XCB_NONE, + + .black_picture = XCB_NONE, + .cshadow_picture = XCB_NONE, + .white_picture = XCB_NONE, + .gaussian_map = NULL, + + .refresh_rate = 0, + .refresh_intv = 0UL, + .paint_tm_offset = 0L, + +#ifdef CONFIG_VSYNC_DRM + .drm_fd = -1, +#endif + + .xfixes_event = 0, + .xfixes_error = 0, + .damage_event = 0, + .damage_error = 0, + .render_event = 0, + .render_error = 0, + .composite_event = 0, + .composite_error = 0, + .composite_opcode = 0, + .shape_exists = false, + .shape_event = 0, + .shape_error = 0, + .randr_exists = 0, + .randr_event = 0, + .randr_error = 0, + .glx_exists = false, + .glx_event = 0, + .glx_error = 0, + .xrfilter_convolution_exists = false, + + .atoms_wintypes = {0}, + .track_atom_lst = NULL, + +#ifdef CONFIG_DBUS + .dbus_data = NULL, +#endif + }; + + auto stderr_logger = stderr_logger_new(); + if (stderr_logger) { + // stderr logger might fail to create if we are already + // daemonized. + log_add_target_tls(stderr_logger); + } + + // Allocate a session and copy default values into it + session_t *ps = cmalloc(session_t); + *ps = s_def; + list_init_head(&ps->window_stack); + ps->loop = EV_DEFAULT; + pixman_region32_init(&ps->screen_reg); + + ps->ignore_tail = &ps->ignore_head; + + ps->o.show_all_xerrors = all_xerrors; + + // Use the same Display across reset, primarily for resource leak checking + ps->dpy = dpy; + ps->c = XGetXCBConnection(ps->dpy); + + const xcb_query_extension_reply_t *ext_info; + + ps->previous_xerror_handler = XSetErrorHandler(xerror); + + ps->scr = DefaultScreen(ps->dpy); + + auto screen = x_screen_of_display(ps->c, ps->scr); + ps->vis = screen->root_visual; + ps->depth = screen->root_depth; + ps->root = screen->root; + ps->root_width = screen->width_in_pixels; + ps->root_height = screen->height_in_pixels; + + // Start listening to events on root earlier to catch all possible + // root geometry changes + auto e = xcb_request_check( + ps->c, xcb_change_window_attributes_checked( + ps->c, ps->root, XCB_CW_EVENT_MASK, + (const uint32_t[]){XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | + XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY | + XCB_EVENT_MASK_PROPERTY_CHANGE})); + if (e) { + log_error_x_error(e, "Failed to setup root window event mask"); + } + + xcb_prefetch_extension_data(ps->c, &xcb_render_id); + xcb_prefetch_extension_data(ps->c, &xcb_composite_id); + xcb_prefetch_extension_data(ps->c, &xcb_damage_id); + xcb_prefetch_extension_data(ps->c, &xcb_shape_id); + xcb_prefetch_extension_data(ps->c, &xcb_xfixes_id); + xcb_prefetch_extension_data(ps->c, &xcb_randr_id); + xcb_prefetch_extension_data(ps->c, &xcb_xinerama_id); + xcb_prefetch_extension_data(ps->c, &xcb_present_id); + xcb_prefetch_extension_data(ps->c, &xcb_sync_id); + xcb_prefetch_extension_data(ps->c, &xcb_glx_id); + + ext_info = xcb_get_extension_data(ps->c, &xcb_render_id); + if (!ext_info || !ext_info->present) { + log_fatal("No render extension"); + exit(1); + } + ps->render_event = ext_info->first_event; + ps->render_error = ext_info->first_error; + + ext_info = xcb_get_extension_data(ps->c, &xcb_composite_id); + if (!ext_info || !ext_info->present) { + log_fatal("No composite extension"); + exit(1); + } + ps->composite_opcode = ext_info->major_opcode; + ps->composite_event = ext_info->first_event; + ps->composite_error = ext_info->first_error; + + { + xcb_composite_query_version_reply_t *reply = xcb_composite_query_version_reply( + ps->c, + xcb_composite_query_version(ps->c, XCB_COMPOSITE_MAJOR_VERSION, + XCB_COMPOSITE_MINOR_VERSION), + NULL); + + if (!reply || (reply->major_version == 0 && reply->minor_version < 2)) { + log_fatal("Your X server doesn't have Composite >= 0.2 support, " + "we cannot proceed."); + exit(1); + } + free(reply); + } + + ext_info = xcb_get_extension_data(ps->c, &xcb_damage_id); + if (!ext_info || !ext_info->present) { + log_fatal("No damage extension"); + exit(1); + } + ps->damage_event = ext_info->first_event; + ps->damage_error = ext_info->first_error; + xcb_discard_reply(ps->c, xcb_damage_query_version(ps->c, XCB_DAMAGE_MAJOR_VERSION, + XCB_DAMAGE_MINOR_VERSION) + .sequence); + + ext_info = xcb_get_extension_data(ps->c, &xcb_xfixes_id); + if (!ext_info || !ext_info->present) { + log_fatal("No XFixes extension"); + exit(1); + } + ps->xfixes_event = ext_info->first_event; + ps->xfixes_error = ext_info->first_error; + xcb_discard_reply(ps->c, xcb_xfixes_query_version(ps->c, XCB_XFIXES_MAJOR_VERSION, + XCB_XFIXES_MINOR_VERSION) + .sequence); + + ps->damaged_region = x_new_id(ps->c); + if (!XCB_AWAIT_VOID(xcb_xfixes_create_region, ps->c, ps->damaged_region, 0, NULL)) { + log_fatal("Failed to create a XFixes region"); + goto err; + } + + ext_info = xcb_get_extension_data(ps->c, &xcb_glx_id); + if (ext_info && ext_info->present) { + ps->glx_exists = true; + ps->glx_error = ext_info->first_error; + ps->glx_event = ext_info->first_event; + } + + // Parse configuration file + win_option_mask_t winopt_mask[NUM_WINTYPES] = {{0}}; + bool shadow_enabled = false, fading_enable = false, hasneg = false; + char *config_file_to_free = NULL; + config_file = config_file_to_free = parse_config( + &ps->o, config_file, &shadow_enabled, &fading_enable, &hasneg, winopt_mask); + + if (IS_ERR(config_file_to_free)) { + return NULL; + } + + // Parse all of the rest command line options + if (!get_cfg(&ps->o, argc, argv, shadow_enabled, fading_enable, hasneg, winopt_mask)) { + log_fatal("Failed to get configuration, usually mean you have specified " + "invalid options."); + return NULL; + } + + if (ps->o.logpath) { + auto l = file_logger_new(ps->o.logpath); + if (l) { + log_info("Switching to log file: %s", ps->o.logpath); + if (stderr_logger) { + log_remove_target_tls(stderr_logger); + stderr_logger = NULL; + } + log_add_target_tls(l); + stderr_logger = NULL; + } else { + log_error("Failed to setup log file %s, I will keep using stderr", + ps->o.logpath); + } + } + + if (strstr(argv[0], "compton")) { + log_warn("This compositor has been renamed to \"picom\", the \"compton\" " + "binary will not be installed in the future."); + } + + ps->atoms = init_atoms(ps->c); + ps->atoms_wintypes[WINTYPE_UNKNOWN] = 0; +#define SET_WM_TYPE_ATOM(x) \ + ps->atoms_wintypes[WINTYPE_##x] = ps->atoms->a_NET_WM_WINDOW_TYPE_##x + SET_WM_TYPE_ATOM(DESKTOP); + SET_WM_TYPE_ATOM(DOCK); + SET_WM_TYPE_ATOM(TOOLBAR); + SET_WM_TYPE_ATOM(MENU); + SET_WM_TYPE_ATOM(UTILITY); + SET_WM_TYPE_ATOM(SPLASH); + SET_WM_TYPE_ATOM(DIALOG); + SET_WM_TYPE_ATOM(NORMAL); + SET_WM_TYPE_ATOM(DROPDOWN_MENU); + SET_WM_TYPE_ATOM(POPUP_MENU); + SET_WM_TYPE_ATOM(TOOLTIP); + SET_WM_TYPE_ATOM(NOTIFICATION); + SET_WM_TYPE_ATOM(COMBO); + SET_WM_TYPE_ATOM(DND); +#undef SET_WM_TYPE_ATOM + + // Get needed atoms for c2 condition lists + if (!(c2_list_postprocess(ps, ps->o.unredir_if_possible_blacklist) && + c2_list_postprocess(ps, ps->o.paint_blacklist) && + c2_list_postprocess(ps, ps->o.shadow_blacklist) && + c2_list_postprocess(ps, ps->o.shadow_clip_list) && + c2_list_postprocess(ps, ps->o.fade_blacklist) && + c2_list_postprocess(ps, ps->o.blur_background_blacklist) && + c2_list_postprocess(ps, ps->o.invert_color_list) && + c2_list_postprocess(ps, ps->o.opacity_rules) && + c2_list_postprocess(ps, ps->o.rounded_corners_blacklist) && + c2_list_postprocess(ps, ps->o.focus_blacklist))) { + log_error("Post-processing of conditionals failed, some of your rules " + "might not work"); + } + + ps->gaussian_map = gaussian_kernel_autodetect_deviation(ps->o.shadow_radius); + sum_kernel_preprocess(ps->gaussian_map); + + rebuild_shadow_exclude_reg(ps); + + // Query X Shape + ext_info = xcb_get_extension_data(ps->c, &xcb_shape_id); + if (ext_info && ext_info->present) { + ps->shape_event = ext_info->first_event; + ps->shape_error = ext_info->first_error; + ps->shape_exists = true; + } + + ext_info = xcb_get_extension_data(ps->c, &xcb_randr_id); + if (ext_info && ext_info->present) { + ps->randr_exists = true; + ps->randr_event = ext_info->first_event; + ps->randr_error = ext_info->first_error; + } + + ext_info = xcb_get_extension_data(ps->c, &xcb_present_id); + if (ext_info && ext_info->present) { + auto r = xcb_present_query_version_reply( + ps->c, + xcb_present_query_version(ps->c, XCB_PRESENT_MAJOR_VERSION, + XCB_PRESENT_MINOR_VERSION), + NULL); + if (r) { + ps->present_exists = true; + free(r); + } + } + + // Query X Sync + ext_info = xcb_get_extension_data(ps->c, &xcb_sync_id); + if (ext_info && ext_info->present) { + ps->xsync_error = ext_info->first_error; + ps->xsync_event = ext_info->first_event; + // Need X Sync 3.1 for fences + auto r = xcb_sync_initialize_reply( + ps->c, + xcb_sync_initialize(ps->c, XCB_SYNC_MAJOR_VERSION, XCB_SYNC_MINOR_VERSION), + NULL); + if (r && (r->major_version > 3 || + (r->major_version == 3 && r->minor_version >= 1))) { + ps->xsync_exists = true; + free(r); + } + } + + ps->sync_fence = XCB_NONE; + if (ps->xsync_exists) { + ps->sync_fence = x_new_id(ps->c); + e = xcb_request_check( + ps->c, xcb_sync_create_fence(ps->c, ps->root, ps->sync_fence, 0)); + if (e) { + if (ps->o.xrender_sync_fence) { + log_error_x_error(e, "Failed to create a XSync fence. " + "xrender-sync-fence will be " + "disabled"); + ps->o.xrender_sync_fence = false; + } + ps->sync_fence = XCB_NONE; + free(e); + } + } else if (ps->o.xrender_sync_fence) { + log_error("XSync extension not found. No XSync fence sync is " + "possible. (xrender-sync-fence can't be enabled)"); + ps->o.xrender_sync_fence = false; + } + + // Query X RandR + if ((ps->o.sw_opti && !ps->o.refresh_rate) || ps->o.xinerama_shadow_crop) { + if (!ps->randr_exists) { + log_fatal("No XRandR extension. sw-opti, refresh-rate or " + "xinerama-shadow-crop " + "cannot be enabled."); + goto err; + } + } + + // Query X Xinerama extension + if (ps->o.xinerama_shadow_crop) { + ext_info = xcb_get_extension_data(ps->c, &xcb_xinerama_id); + ps->xinerama_exists = ext_info && ext_info->present; + } + + rebuild_screen_reg(ps); + + bool compositor_running = false; + if (session_redirection_mode(ps) == XCB_COMPOSITE_REDIRECT_MANUAL) { + // We are running in the manual redirection mode, meaning we are running + // as a proper compositor. So we need to register us as a compositor, etc. + + // We are also here when --diagnostics is set. We want to be here because + // that gives us more diagnostic information. + + // Create registration window + int ret = register_cm(ps); + if (ret == -1) { + exit(1); + } + + compositor_running = ret == 1; + if (compositor_running) { + // Don't take the overlay when there is another compositor + // running, so we don't disrupt it. + + // If we are printing diagnostic, we will continue a bit further + // to get more diagnostic information, otherwise we will exit. + if (!ps->o.print_diagnostics) { + log_fatal("Another composite manager is already running"); + exit(1); + } + } else { + if (!init_overlay(ps)) { + goto err; + } + } + } else { + // We are here if we don't really function as a compositor, so we are not + // taking over the screen, and we don't need to register as a compositor + + // If we are in debug mode, we need to create a window for rendering if + // the backend supports presenting. + + // The old backends doesn't have a automatic redirection mode + log_info("The compositor is started in automatic redirection mode."); + assert(ps->o.experimental_backends); + + if (backend_list[ps->o.backend]->present) { + // If the backend has `present`, we couldn't be in automatic + // redirection mode unless we are in debug mode. + assert(ps->o.debug_mode); + if (!init_debug_window(ps)) { + goto err; + } + } + } + + ps->drivers = detect_driver(ps->c, ps->backend_data, ps->root); + apply_driver_workarounds(ps, ps->drivers); + + // Initialize filters, must be preceded by OpenGL context creation + if (!ps->o.experimental_backends && !init_render(ps)) { + log_fatal("Failed to initialize the backend"); + exit(1); + } + + if (ps->o.print_diagnostics) { + print_diagnostics(ps, config_file, compositor_running); + free(config_file_to_free); + exit(0); + } + + ps->file_watch_handle = file_watch_init(ps->loop); + if (ps->file_watch_handle && config_file) { + file_watch_add(ps->file_watch_handle, config_file, config_file_change_cb, ps); + } + + free(config_file_to_free); + + if (bkend_use_glx(ps) && !ps->o.experimental_backends) { + auto gl_logger = gl_string_marker_logger_new(); + if (gl_logger) { + log_info("Enabling gl string marker"); + log_add_target_tls(gl_logger); + } + } + + if (ps->o.experimental_backends) { + if (ps->o.monitor_repaint && !backend_list[ps->o.backend]->fill) { + log_warn("--monitor-repaint is not supported by the backend, " + "disabling"); + ps->o.monitor_repaint = false; + } + } + + // Initialize software optimization + if (ps->o.sw_opti) + ps->o.sw_opti = swopti_init(ps); + + // Monitor screen changes if vsync_sw is enabled and we are using + // an auto-detected refresh rate, or when Xinerama features are enabled + if (ps->randr_exists && + ((ps->o.sw_opti && !ps->o.refresh_rate) || ps->o.xinerama_shadow_crop)) + xcb_randr_select_input(ps->c, ps->root, XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE); + + cxinerama_upd_scrs(ps); + + { + xcb_render_create_picture_value_list_t pa = { + .subwindowmode = IncludeInferiors, + }; + + ps->root_picture = x_create_picture_with_visual_and_pixmap( + ps->c, ps->vis, ps->root, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); + if (ps->overlay != XCB_NONE) { + ps->tgt_picture = x_create_picture_with_visual_and_pixmap( + ps->c, ps->vis, ps->overlay, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); + } else + ps->tgt_picture = ps->root_picture; + } + + ev_io_init(&ps->xiow, x_event_callback, ConnectionNumber(ps->dpy), EV_READ); + ev_io_start(ps->loop, &ps->xiow); + ev_init(&ps->unredir_timer, tmout_unredir_callback); + if (ps->o.sw_opti) + ev_idle_init(&ps->draw_idle, delayed_draw_callback); + else + ev_idle_init(&ps->draw_idle, draw_callback); + + ev_init(&ps->fade_timer, fade_timer_callback); + ev_init(&ps->animation_timer, animation_timer_callback); + ev_init(&ps->delayed_draw_timer, delayed_draw_timer_callback); + + // Set up SIGUSR1 signal handler to reset program + ev_signal_init(&ps->usr1_signal, reset_enable, SIGUSR1); + ev_signal_init(&ps->int_signal, exit_enable, SIGINT); + ev_signal_start(ps->loop, &ps->usr1_signal); + ev_signal_start(ps->loop, &ps->int_signal); + + // xcb can read multiple events from the socket when a request with reply is + // made. + // + // Use an ev_prepare to make sure we cannot accidentally forget to handle them + // before we go to sleep. + // + // If we don't drain the queue before goes to sleep (i.e. blocking on socket + // input), we will be sleeping with events available in queue. Which might + // cause us to block indefinitely because arrival of new events could be + // dependent on processing of existing events (e.g. if we don't process damage + // event and do damage subtract, new damage event won't be generated). + // + // So we make use of a ev_prepare handle, which is called right before libev + // goes into sleep, to handle all the queued X events. + ev_prepare_init(&ps->event_check, handle_queued_x_events); + // Make sure nothing can cause xcb to read from the X socket after events are + // handled and before we going to sleep. + ev_set_priority(&ps->event_check, EV_MINPRI); + ev_prepare_start(ps->loop, &ps->event_check); + + // Initialize DBus. We need to do this early, because add_win might call dbus + // functions + if (ps->o.dbus) { +#ifdef CONFIG_DBUS + cdbus_init(ps, DisplayString(ps->dpy)); + if (!ps->dbus_data) { + ps->o.dbus = false; + } +#else + log_fatal("DBus support not compiled in!"); + exit(1); +#endif + } + + e = xcb_request_check(ps->c, xcb_grab_server_checked(ps->c)); + if (e) { + log_fatal_x_error(e, "Failed to grab X server"); + free(e); + goto err; + } + + ps->server_grabbed = true; + + // We are going to pull latest information from X server now, events sent by X + // earlier is irrelavant at this point. + // A better solution is probably grabbing the server from the very start. But I + // think there still could be race condition that mandates discarding the events. + x_discard_events(ps->c); + + xcb_query_tree_reply_t *query_tree_reply = + xcb_query_tree_reply(ps->c, xcb_query_tree(ps->c, ps->root), NULL); + + e = xcb_request_check(ps->c, xcb_ungrab_server(ps->c)); + if (e) { + log_fatal_x_error(e, "Failed to ungrab server"); + free(e); + goto err; + } + + ps->server_grabbed = false; + + if (query_tree_reply) { + xcb_window_t *children; + int nchildren; + + children = xcb_query_tree_children(query_tree_reply); + nchildren = xcb_query_tree_children_length(query_tree_reply); + + for (int i = 0; i < nchildren; i++) { + add_win_above(ps, children[i], i ? children[i - 1] : XCB_NONE); + } + free(query_tree_reply); + } + + log_debug("Initial stack:"); + list_foreach(struct win, w, &ps->window_stack, stack_neighbour) { + log_debug("%#010x", w->id); + } + + ps->pending_updates = true; + + write_pid(ps); + + if (fork && stderr_logger) { + // Remove the stderr logger if we will fork + log_remove_target_tls(stderr_logger); + } + return ps; +err: + free(ps); + return NULL; +} + +/** + * Destroy a session. + * + * Does not close the X connection or free the <code>session_t</code> + * structure, though. + * + * @param ps session to destroy + */ +static void session_destroy(session_t *ps) { + if (ps->redirected) { + unredirect(ps); + } + +#ifdef CONFIG_OPENGL + free(ps->argb_fbconfig); + ps->argb_fbconfig = NULL; +#endif + + file_watch_destroy(ps->loop, ps->file_watch_handle); + ps->file_watch_handle = NULL; + + // Stop listening to events on root window + xcb_change_window_attributes(ps->c, ps->root, XCB_CW_EVENT_MASK, + (const uint32_t[]){0}); + +#ifdef CONFIG_DBUS + // Kill DBus connection + if (ps->o.dbus) { + assert(ps->dbus_data); + cdbus_destroy(ps); + } +#endif + + // Free window linked list + + list_foreach_safe(struct win, w, &ps->window_stack, stack_neighbour) { + if (!w->destroyed) { + win_ev_stop(ps, w); + HASH_DEL(ps->windows, w); + } + + if (w->managed) { + auto mw = (struct managed_win *)w; + free_win_res(ps, mw); + } + free(w); + } + list_init_head(&ps->window_stack); + + // Free blacklists + free_wincondlst(&ps->o.shadow_blacklist); + free_wincondlst(&ps->o.shadow_clip_list); + free_wincondlst(&ps->o.fade_blacklist); + free_wincondlst(&ps->o.focus_blacklist); + free_wincondlst(&ps->o.invert_color_list); + free_wincondlst(&ps->o.blur_background_blacklist); + free_wincondlst(&ps->o.opacity_rules); + free_wincondlst(&ps->o.paint_blacklist); + free_wincondlst(&ps->o.unredir_if_possible_blacklist); + free_wincondlst(&ps->o.rounded_corners_blacklist); + + // Free tracked atom list + { + latom_t *next = NULL; + for (latom_t *this = ps->track_atom_lst; this; this = next) { + next = this->next; + free(this); + } + + ps->track_atom_lst = NULL; + } + + // Free ignore linked list + { + ignore_t *next = NULL; + for (ignore_t *ign = ps->ignore_head; ign; ign = next) { + next = ign->next; + + free(ign); + } + + // Reset head and tail + ps->ignore_head = NULL; + ps->ignore_tail = &ps->ignore_head; + } + + // Free tgt_{buffer,picture} and root_picture + if (ps->tgt_buffer.pict == ps->tgt_picture) + ps->tgt_buffer.pict = XCB_NONE; + + if (ps->tgt_picture == ps->root_picture) + ps->tgt_picture = XCB_NONE; + else + free_picture(ps->c, &ps->tgt_picture); + + free_picture(ps->c, &ps->root_picture); + free_paint(ps, &ps->tgt_buffer); + + pixman_region32_fini(&ps->screen_reg); + free(ps->expose_rects); + + free(ps->o.write_pid_path); + free(ps->o.logpath); + for (int i = 0; i < ps->o.blur_kernel_count; ++i) { + free(ps->o.blur_kerns[i]); + } + free(ps->o.blur_kerns); + free(ps->o.glx_fshader_win_str); + free_xinerama_info(ps); + +#ifdef CONFIG_VSYNC_DRM + // Close file opened for DRM VSync + if (ps->drm_fd >= 0) { + close(ps->drm_fd); + ps->drm_fd = -1; + } +#endif + + // Release overlay window + if (ps->overlay) { + xcb_composite_release_overlay_window(ps->c, ps->overlay); + ps->overlay = XCB_NONE; + } + + if (ps->sync_fence != XCB_NONE) { + xcb_sync_destroy_fence(ps->c, ps->sync_fence); + ps->sync_fence = XCB_NONE; + } + + // Free reg_win + if (ps->reg_win != XCB_NONE) { + xcb_destroy_window(ps->c, ps->reg_win); + ps->reg_win = XCB_NONE; + } + + if (ps->debug_window != XCB_NONE) { + xcb_destroy_window(ps->c, ps->debug_window); + ps->debug_window = XCB_NONE; + } + + if (ps->damaged_region != XCB_NONE) { + xcb_xfixes_destroy_region(ps->c, ps->damaged_region); + ps->damaged_region = XCB_NONE; + } + + if (ps->o.experimental_backends) { + // backend is deinitialized in unredirect() + assert(ps->backend_data == NULL); + } else { + deinit_render(ps); + } + +#if CONFIG_OPENGL + if (glx_has_context(ps)) { + // GLX context created, but not for rendering + glx_destroy(ps); + } +#endif + + // Flush all events + x_sync(ps->c); + ev_io_stop(ps->loop, &ps->xiow); + free_conv(ps->gaussian_map); + destroy_atoms(ps->atoms); + +#ifdef DEBUG_XRC + // Report about resource leakage + xrc_report_xid(); +#endif + + XSetErrorHandler(ps->previous_xerror_handler); + + // Stop libev event handlers + ev_timer_stop(ps->loop, &ps->unredir_timer); + ev_timer_stop(ps->loop, &ps->fade_timer); + ev_timer_stop(ps->loop, &ps->animation_timer); + ev_idle_stop(ps->loop, &ps->draw_idle); + ev_prepare_stop(ps->loop, &ps->event_check); + ev_signal_stop(ps->loop, &ps->usr1_signal); + ev_signal_stop(ps->loop, &ps->int_signal); +} + +/** + * Do the actual work. + * + * @param ps current session + */ +static void session_run(session_t *ps) { + if (ps->o.sw_opti) + ps->paint_tm_offset = get_time_timeval().tv_usec; + + // In benchmark mode, we want draw_idle handler to always be active + if (ps->o.benchmark) { + ev_idle_start(ps->loop, &ps->draw_idle); + } else { + // Let's draw our first frame! + queue_redraw(ps); + } + ev_run(ps->loop, 0); +} + +/** + * The function that everybody knows. + */ +int main(int argc, char **argv) { + // Set locale so window names with special characters are interpreted + // correctly + setlocale(LC_ALL, ""); + + // Initialize logging system for early logging + log_init_tls(); + + { + auto stderr_logger = stderr_logger_new(); + if (stderr_logger) { + log_add_target_tls(stderr_logger); + } + } + + int exit_code; + char *config_file = NULL; + bool all_xerrors = false, need_fork = false; + if (get_early_config(argc, argv, &config_file, &all_xerrors, &need_fork, &exit_code)) { + return exit_code; + } + + int pfds[2]; + if (need_fork) { + if (pipe2(pfds, O_CLOEXEC)) { + perror("pipe2"); + return 1; + } + auto pid = fork(); + if (pid < 0) { + perror("fork"); + return 1; + } + if (pid > 0) { + // We are the parent + close(pfds[1]); + // We wait for the child to tell us it has finished initialization + // by sending us something via the pipe. + int tmp; + if (read(pfds[0], &tmp, sizeof tmp) <= 0) { + // Failed to read, the child has most likely died + // We can probably waitpid() here. + return 1; + } else { + // We are done + return 0; + } + } + // We are the child + close(pfds[0]); + } + + // Main loop + bool quit = false; + int ret_code = 0; + char *pid_file = NULL; + + do { + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { + log_fatal("Can't open display."); + ret_code = 1; + break; + } + XSetEventQueueOwner(dpy, XCBOwnsEventQueue); + + // Reinit logging system so we don't get leftovers from previous sessions + // or early logging. + log_deinit_tls(); + log_init_tls(); + + ps_g = session_init(argc, argv, dpy, config_file, all_xerrors, need_fork); + if (!ps_g) { + log_fatal("Failed to create new session."); + ret_code = 1; + break; + } + if (need_fork) { + // Finishing up daemonization + // Close files + if (fclose(stdout) || fclose(stderr) || fclose(stdin)) { + log_fatal("Failed to close standard input/output"); + ret_code = 1; + break; + } + // Make us the session and process group leader so we don't get + // killed when our parent die. + setsid(); + // Notify the parent that we are done. This might cause the parent + // to quit, so only do this after setsid() + int tmp = 1; + write(pfds[1], &tmp, sizeof tmp); + close(pfds[1]); + // We only do this once + need_fork = false; + } + session_run(ps_g); + quit = ps_g->quit; + if (quit && ps_g->o.write_pid_path) { + pid_file = strdup(ps_g->o.write_pid_path); + } + session_destroy(ps_g); + free(ps_g); + ps_g = NULL; + if (dpy) { + XCloseDisplay(dpy); + } + } while (!quit); + + free(config_file); + if (pid_file) { + log_trace("remove pid file %s", pid_file); + unlink(pid_file); + free(pid_file); + } + + log_deinit_tls(); + + return ret_code; +} diff --git a/src/picom.h b/src/picom.h new file mode 100644 index 0000000..25f7580 --- /dev/null +++ b/src/picom.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) + +// Throw everything in here. +// !!! DON'T !!! + +// === Includes === + +#include <locale.h> +#include <stdbool.h> +#include <stdlib.h> +#include <xcb/xproto.h> + +#include <X11/Xutil.h> +#include "backend/backend.h" +#include "c2.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "log.h" // XXX clean up +#include "region.h" +#include "render.h" +#include "types.h" +#include "utils.h" +#include "win.h" +#include "x.h" + +enum root_flags { + ROOT_FLAGS_SCREEN_CHANGE = 1, // Received RandR screen change notify, we + // use this to track refresh rate changes + ROOT_FLAGS_CONFIGURED = 2 // Received configure notify on the root window +}; + +// == Functions == +// TODO(yshui) move static inline functions that are only used in picom.c, into picom.c + +void add_damage(session_t *ps, const region_t *damage); + +uint32_t determine_evmask(session_t *ps, xcb_window_t wid, win_evmode_t mode); + +void circulate_win(session_t *ps, xcb_circulate_notify_event_t *ce); + +void update_refresh_rate(session_t *ps); + +void root_damaged(session_t *ps); + +void cxinerama_upd_scrs(session_t *ps); + +void queue_redraw(session_t *ps); + +void discard_ignore(session_t *ps, unsigned long sequence); + +void set_root_flags(session_t *ps, uint64_t flags); + +void quit(session_t *ps); + +xcb_window_t session_get_target_window(session_t *); + +uint8_t session_redirection_mode(session_t *ps); + +/** + * Set a <code>switch_t</code> array of all unset wintypes to true. + */ +static inline void wintype_arr_enable_unset(switch_t arr[]) { + wintype_t i; + + for (i = 0; i < NUM_WINTYPES; ++i) + if (UNSET == arr[i]) + arr[i] = ON; +} + +/** + * Check if a window ID exists in an array of window IDs. + * + * @param arr the array of window IDs + * @param count amount of elements in the array + * @param wid window ID to search for + */ +static inline bool array_wid_exists(const xcb_window_t *arr, int count, xcb_window_t wid) { + while (count--) { + if (arr[count] == wid) { + return true; + } + } + + return false; +} + +/** + * Destroy a condition list. + */ +static inline void free_wincondlst(c2_lptr_t **pcondlst) { + while ((*pcondlst = c2_free_lptr(*pcondlst))) + continue; +} + +#ifndef CONFIG_OPENGL +static inline void free_paint_glx(session_t *ps attr_unused, paint_t *p attr_unused) { +} +static inline void +free_win_res_glx(session_t *ps attr_unused, struct managed_win *w attr_unused) { +} +#endif + +/** + * Dump an drawable's info. + */ +static inline void dump_drawable(session_t *ps, xcb_drawable_t drawable) { + auto r = xcb_get_geometry_reply(ps->c, xcb_get_geometry(ps->c, drawable), NULL); + if (!r) { + log_trace("Drawable %#010x: Failed", drawable); + return; + } + log_trace("Drawable %#010x: x = %u, y = %u, wid = %u, hei = %d, b = %u, d = %u", + drawable, r->x, r->y, r->width, r->height, r->border_width, r->depth); + free(r); +} diff --git a/src/picom.modulemap b/src/picom.modulemap new file mode 100644 index 0000000..787c4ff --- /dev/null +++ b/src/picom.modulemap @@ -0,0 +1,214 @@ +// modulemap + +module compiler { + header "compiler.h" +} +module string_utils { + header "string_utils.h" +} +module dbus { + header "dbus.h" +} +module kernel { + header "kernel.h" +} +module utils { + // Has macros expands to calloc/malloc + header "utils.h" + export libc.stdlib +} +module region { + header "region.h" +} +module picom { + header "picom.h" +} +module types { + header "types.h" +} +module c2 { + header "c2.h" +} +module render { + header "render.h" +} +module options { + header "options.h" +} +module opengl { + header "opengl.h" +} +module diagnostic { + header "diagnostic.h" +} +module win_defs { + header "win_defs.h" +} +module win { + header "win.h" + export win_defs +} +module log { + header "log.h" + export compiler +} +module x { + header "x.h" +} +module vsync { + header "vsync.h" +} +module common { + header "common.h" +} +module config { + header "config.h" +} +module xrescheck { + header "xrescheck.h" +} +module cache { + header "cache.h" +} +module backend { + module gl { + module gl_common { + header "backend/gl/gl_common.h" + } + module glx { + header "backend/gl/glx.h" + export GL.glx + } + } + module backend { + header "backend/backend.h" + } + module backend_common { + header "backend/backend_common.h" + } +} +module xcb [system] { + module xcb { + header "/usr/include/xcb/xcb.h" + export * + } + module randr { + header "/usr/include/xcb/randr.h" + export * + } + module render { + header "/usr/include/xcb/render.h" + export * + } + module sync { + header "/usr/include/xcb/sync.h" + export * + } + module composite { + header "/usr/include/xcb/composite.h" + export * + } + module xfixes { + header "/usr/include/xcb/xfixes.h" + export * + } + module damage { + header "/usr/include/xcb/damage.h" + export * + } + module xproto { + header "/usr/include/xcb/xproto.h" + export * + } + module present { + header "/usr/include/xcb/present.h" + } + module util { + module render { + header "/usr/include/xcb/xcb_renderutil.h" + export * + } + } +} +module X11 [system] { + module Xlib { + header "/usr/include/X11/Xlib.h" + export * + } + module Xutil { + header "/usr/include/X11/Xutil.h" + export * + } +} +module GL [system] { + module glx { + header "/usr/include/GL/glx.h" + export * + } + module gl { + header "/usr/include/GL/gl.h" + export * + } +} +module libc [system] { + export * + module assert { + export * + textual header "/usr/include/assert.h" + } + module string { + export * + header "/usr/include/string.h" + } + module ctype { + export * + header "/usr/include/ctype.h" + } + module errno { + export * + header "/usr/include/errno.h" + } + module fenv { + export * + header "/usr/include/fenv.h" + } + module inttypes { + export * + header "/usr/include/inttypes.h" + } + module math { + export * + header "/usr/include/math.h" + } + module setjmp { + export * + header "/usr/include/setjmp.h" + } + module stdio { + export * + header "/usr/include/stdio.h" + } + + module stdlib [system] { + export * + header "/usr/include/stdlib.h" + } +} + +// glib specific header. In it's own module because it +// doesn't exist on some systems with unpatched glib 2.26+ +module "xlocale.h" [system] { + export * + header "/usr/include/xlocale.h" +} + +// System header that we have difficult with merging. +module "sys_types.h" [system] { + export * + header "/usr/include/sys/types.h" +} + +module "signal.h" [system] { + export * + header "/usr/include/signal.h" +} diff --git a/src/region.h b/src/region.h new file mode 100644 index 0000000..bda66e2 --- /dev/null +++ b/src/region.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> +#pragma once +#include <pixman.h> +#include <stdio.h> +#include <stdlib.h> +#include <xcb/xcb.h> + +#include "log.h" +#include "utils.h" + +typedef struct pixman_region32 pixman_region32_t; +typedef struct pixman_box32 pixman_box32_t; +typedef pixman_region32_t region_t; +typedef pixman_box32_t rect_t; + +RC_TYPE(region_t, rc_region, pixman_region32_init, pixman_region32_fini, static inline) + +static inline void dump_region(const region_t *x) { + if (log_get_level_tls() < LOG_LEVEL_TRACE) { + return; + } + int nrects; + const rect_t *rects = pixman_region32_rectangles((region_t *)x, &nrects); + log_trace("nrects: %d", nrects); + for (int i = 0; i < nrects; i++) + log_trace("(%d, %d) - (%d, %d)", rects[i].x1, rects[i].y1, rects[i].x2, + rects[i].y2); +} + +/// Convert one xcb rectangle to our rectangle type +static inline rect_t from_x_rect(const xcb_rectangle_t *rect) { + return (rect_t){ + .x1 = rect->x, + .y1 = rect->y, + .x2 = rect->x + rect->width, + .y2 = rect->y + rect->height, + }; +} + +/// Convert an array of xcb rectangles to our rectangle type +/// Returning an array that needs to be freed +static inline rect_t *from_x_rects(int nrects, const xcb_rectangle_t *rects) { + rect_t *ret = ccalloc(nrects, rect_t); + for (int i = 0; i < nrects; i++) { + ret[i] = from_x_rect(rects + i); + } + return ret; +} + +/** + * Resize a region. + */ +static inline void _resize_region(const region_t *region, region_t *output, int dx, + int dy) { + if (!region || !output) { + return; + } + if (!dx && !dy) { + if (region != output) { + pixman_region32_copy(output, (region_t *)region); + } + return; + } + // Loop through all rectangles + int nrects; + int nnewrects = 0; + const rect_t *rects = pixman_region32_rectangles((region_t *)region, &nrects); + auto newrects = ccalloc(nrects, rect_t); + for (int i = 0; i < nrects; i++) { + int x1 = rects[i].x1 - dx; + int y1 = rects[i].y1 - dy; + int x2 = rects[i].x2 + dx; + int y2 = rects[i].y2 + dy; + int wid = x2 - x1; + int hei = y2 - y1; + if (wid <= 0 || hei <= 0) { + continue; + } + newrects[nnewrects] = + (rect_t){.x1 = x1, .x2 = x2, .y1 = y1, .y2 = y2}; + ++nnewrects; + } + + pixman_region32_fini(output); + pixman_region32_init_rects(output, newrects, nnewrects); + + free(newrects); +} + +static inline region_t resize_region(const region_t *region, int dx, int dy) { + region_t ret; + pixman_region32_init(&ret); + _resize_region(region, &ret, dx, dy); + return ret; +} + +static inline void resize_region_in_place(region_t *region, int dx, int dy) { + return _resize_region(region, region, dx, dy); +} diff --git a/src/render.c b/src/render.c new file mode 100644 index 0000000..ac9b40e --- /dev/null +++ b/src/render.c @@ -0,0 +1,1500 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> + +#include <stdlib.h> +#include <string.h> +#include <xcb/composite.h> +#include <xcb/render.h> +#include <xcb/sync.h> +#include <xcb/xcb_image.h> +#include <xcb/xcb_renderutil.h> + +#include "common.h" +#include "options.h" + +#ifdef CONFIG_OPENGL +#include "backend/gl/glx.h" +#include "opengl.h" + +#ifndef GLX_BACK_BUFFER_AGE_EXT +#define GLX_BACK_BUFFER_AGE_EXT 0x20F4 +#endif + +#endif + +#include "compiler.h" +#include "config.h" +#include "kernel.h" +#include "log.h" +#include "region.h" +#include "types.h" +#include "utils.h" +#include "vsync.h" +#include "win.h" +#include "x.h" + +#include "backend/backend.h" +#include "backend/backend_common.h" +#include "render.h" + +#define XRFILTER_CONVOLUTION "convolution" +#define XRFILTER_GAUSSIAN "gaussian" +#define XRFILTER_BINOMIAL "binomial" + +/** + * Bind texture in paint_t if we are using GLX backend. + */ +static inline bool paint_bind_tex(session_t *ps, paint_t *ppaint, int wid, int hei, + bool repeat, int depth, xcb_visualid_t visual, bool force) { +#ifdef CONFIG_OPENGL + // XXX This is a mess. But this will go away after the backend refactor. + if (!ppaint->pixmap) + return false; + + struct glx_fbconfig_info *fbcfg; + if (!visual) { + assert(depth == 32); + if (!ps->argb_fbconfig) { + ps->argb_fbconfig = + glx_find_fbconfig(ps->dpy, ps->scr, + (struct xvisual_info){.red_size = 8, + .green_size = 8, + .blue_size = 8, + .alpha_size = 8, + .visual_depth = 32}); + } + if (!ps->argb_fbconfig) { + log_error("Failed to find appropriate FBConfig for 32 bit depth"); + return false; + } + fbcfg = ps->argb_fbconfig; + } else { + auto m = x_get_visual_info(ps->c, visual); + if (m.visual_depth < 0) { + return false; + } + + if (depth && depth != m.visual_depth) { + log_error("Mismatching visual depth: %d != %d", depth, m.visual_depth); + return false; + } + + if (!ppaint->fbcfg) { + ppaint->fbcfg = glx_find_fbconfig(ps->dpy, ps->scr, m); + } + if (!ppaint->fbcfg) { + log_error("Failed to find appropriate FBConfig for X pixmap"); + return false; + } + fbcfg = ppaint->fbcfg; + } + + if (force || !glx_tex_binded(ppaint->ptex, ppaint->pixmap)) + return glx_bind_pixmap(ps, &ppaint->ptex, ppaint->pixmap, wid, hei, + repeat, fbcfg); +#else + (void)ps; + (void)ppaint; + (void)wid; + (void)hei; + (void)repeat; + (void)depth; + (void)visual; + (void)force; +#endif + return true; +} + +/** + * Check if current backend uses XRender for rendering. + */ +static inline bool bkend_use_xrender(session_t *ps) { + return BKEND_XRENDER == ps->o.backend || BKEND_XR_GLX_HYBRID == ps->o.backend; +} + +int maximum_buffer_age(session_t *ps) { + if (bkend_use_glx(ps) && ps->o.use_damage) { + return CGLX_MAX_BUFFER_AGE; + } + return 1; +} + +static int get_buffer_age(session_t *ps) { +#ifdef CONFIG_OPENGL + if (bkend_use_glx(ps)) { + if (!glxext.has_GLX_EXT_buffer_age && ps->o.use_damage) { + log_warn("GLX_EXT_buffer_age not supported by your driver," + "`use-damage` has to be disabled"); + ps->o.use_damage = false; + } + if (ps->o.use_damage) { + unsigned int val; + glXQueryDrawable(ps->dpy, get_tgt_window(ps), + GLX_BACK_BUFFER_AGE_EXT, &val); + return (int)val ?: -1; + } + return -1; + } +#endif + return ps->o.use_damage ? 1 : -1; +} + +/** + * Reset filter on a <code>Picture</code>. + */ +static inline void xrfilter_reset(session_t *ps, xcb_render_picture_t p) { +#define FILTER "Nearest" + xcb_render_set_picture_filter(ps->c, p, strlen(FILTER), FILTER, 0, NULL); +#undef FILTER +} + +/// Set the input/output clip region of the target buffer (not the actual target!) +static inline void attr_nonnull(1, 2) set_tgt_clip(session_t *ps, region_t *reg) { + switch (ps->o.backend) { + case BKEND_XRENDER: + case BKEND_XR_GLX_HYBRID: + x_set_picture_clip_region(ps->c, ps->tgt_buffer.pict, 0, 0, reg); + break; +#ifdef CONFIG_OPENGL + case BKEND_GLX: glx_set_clip(ps, reg); break; +#endif + default: assert(false); + } +} + +/** + * Destroy a <code>Picture</code>. + */ +void free_picture(xcb_connection_t *c, xcb_render_picture_t *p) { + if (*p) { + xcb_render_free_picture(c, *p); + *p = XCB_NONE; + } +} + +/** + * Free paint_t. + */ +void free_paint(session_t *ps, paint_t *ppaint) { +#ifdef CONFIG_OPENGL + free_paint_glx(ps, ppaint); +#endif + free_picture(ps->c, &ppaint->pict); + if (ppaint->pixmap) + xcb_free_pixmap(ps->c, ppaint->pixmap); + ppaint->pixmap = XCB_NONE; +} + +uint32_t +make_circle(int cx, int cy, int radius, uint32_t max_ntraps, xcb_render_trapezoid_t traps[]) { + uint32_t n = 0, k = 0; + int y1, y2; + double w; + while (k < max_ntraps) { + y1 = (int)(-radius * cos(M_PI * k / max_ntraps)); + traps[n].top = (cy + y1) * 65536; + traps[n].left.p1.y = (cy + y1) * 65536; + traps[n].right.p1.y = (cy + y1) * 65536; + w = sqrt(radius * radius - y1 * y1) * 65536; + traps[n].left.p1.x = (int)((cx * 65536) - w); + traps[n].right.p1.x = (int)((cx * 65536) + w); + + do { + k++; + y2 = (int)(-radius * cos(M_PI * k / max_ntraps)); + } while (y1 == y2); + + traps[n].bottom = (cy + y2) * 65536; + traps[n].left.p2.y = (cy + y2) * 65536; + traps[n].right.p2.y = (cy + y2) * 65536; + w = sqrt(radius * radius - y2 * y2) * 65536; + traps[n].left.p2.x = (int)((cx * 65536) - w); + traps[n].right.p2.x = (int)((cx * 65536) + w); + n++; + } + return n; +} + +uint32_t make_rectangle(int x, int y, int wid, int hei, xcb_render_trapezoid_t traps[]) { + traps[0].top = y * 65536; + traps[0].left.p1.y = y * 65536; + traps[0].left.p1.x = x * 65536; + traps[0].left.p2.y = (y + hei) * 65536; + traps[0].left.p2.x = x * 65536; + traps[0].bottom = (y + hei) * 65536; + traps[0].right.p1.x = (x + wid) * 65536; + traps[0].right.p1.y = y * 65536; + traps[0].right.p2.x = (x + wid) * 65536; + traps[0].right.p2.y = (y + hei) * 65536; + return 1; +} + +uint32_t make_rounded_window_shape(xcb_render_trapezoid_t traps[], uint32_t max_ntraps, + int cr, int wid, int hei) { + uint32_t n = make_circle(cr, cr, cr, max_ntraps, traps); + n += make_circle(wid - cr, cr, cr, max_ntraps, traps + n); + n += make_circle(wid - cr, hei - cr, cr, max_ntraps, traps + n); + n += make_circle(cr, hei - cr, cr, max_ntraps, traps + n); + n += make_rectangle(0, cr, wid, hei - 2 * cr, traps + n); + n += make_rectangle(cr, 0, wid - 2 * cr, cr, traps + n); + n += make_rectangle(cr, hei - cr, wid - 2 * cr, cr, traps + n); + return n; +} + +void render(session_t *ps, int x, int y, int dx, int dy, int wid, int hei, int fullwid, + int fullhei, double opacity, bool argb, bool neg, int cr, + xcb_render_picture_t pict, glx_texture_t *ptex, const region_t *reg_paint, + const glx_prog_main_t *pprogram, clip_t *clip) { + switch (ps->o.backend) { + case BKEND_XRENDER: + case BKEND_XR_GLX_HYBRID: { + auto alpha_step = (int)(opacity * MAX_ALPHA); + xcb_render_picture_t alpha_pict = ps->alpha_picts[alpha_step]; + if (alpha_step != 0) { + if (cr) { + xcb_render_picture_t p_tmp = x_create_picture_with_standard( + ps->c, ps->root, fullwid, fullhei, + XCB_PICT_STANDARD_ARGB_32, 0, 0); + xcb_render_color_t trans = { + .red = 0, .blue = 0, .green = 0, .alpha = 0}; + const xcb_rectangle_t rect = { + .x = 0, + .y = 0, + .width = to_u16_checked(fullwid), + .height = to_u16_checked(fullhei)}; + xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, + p_tmp, trans, 1, &rect); + + uint32_t max_ntraps = to_u32_checked(cr); + xcb_render_trapezoid_t traps[4 * max_ntraps + 3]; + + uint32_t n = make_rounded_window_shape( + traps, max_ntraps, cr, fullwid, fullhei); + + xcb_render_trapezoids( + ps->c, XCB_RENDER_PICT_OP_OVER, alpha_pict, p_tmp, + x_get_pictfmt_for_standard(ps->c, XCB_PICT_STANDARD_A_8), + 0, 0, n, traps); + + xcb_render_composite( + ps->c, XCB_RENDER_PICT_OP_OVER, pict, p_tmp, + ps->tgt_buffer.pict, to_i16_checked(x), + to_i16_checked(y), to_i16_checked(x), to_i16_checked(y), + to_i16_checked(dx), to_i16_checked(dy), + to_u16_checked(wid), to_u16_checked(hei)); + + xcb_render_free_picture(ps->c, p_tmp); + + } else { + xcb_render_picture_t p_tmp = alpha_pict; + if (clip) { + p_tmp = x_create_picture_with_standard( + ps->c, ps->root, wid, hei, + XCB_PICT_STANDARD_ARGB_32, 0, 0); + + xcb_render_color_t black = { + .red = 255, .blue = 255, .green = 255, .alpha = 255}; + const xcb_rectangle_t rect = { + .x = 0, + .y = 0, + .width = to_u16_checked(wid), + .height = to_u16_checked(hei)}; + xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, + p_tmp, black, 1, &rect); + if (alpha_pict) { + xcb_render_composite( + ps->c, XCB_RENDER_PICT_OP_SRC, + alpha_pict, XCB_NONE, p_tmp, 0, 0, 0, + 0, 0, 0, to_u16_checked(wid), + to_u16_checked(hei)); + } + xcb_render_composite( + ps->c, XCB_RENDER_PICT_OP_OUT_REVERSE, + clip->pict, XCB_NONE, p_tmp, 0, 0, 0, 0, + to_i16_checked(clip->x), to_i16_checked(clip->y), + to_u16_checked(wid), to_u16_checked(hei)); + } + uint8_t op = ((!argb && !alpha_pict && !clip) + ? XCB_RENDER_PICT_OP_SRC + : XCB_RENDER_PICT_OP_OVER); + + xcb_render_composite( + ps->c, op, pict, p_tmp, ps->tgt_buffer.pict, + to_i16_checked(x), to_i16_checked(y), 0, 0, + to_i16_checked(dx), to_i16_checked(dy), + to_u16_checked(wid), to_u16_checked(hei)); + if (clip) { + xcb_render_free_picture(ps->c, p_tmp); + } + } + } + break; + } +#ifdef CONFIG_OPENGL + case BKEND_GLX: + glx_render(ps, ptex, x, y, dx, dy, wid, hei, ps->psglx->z, opacity, argb, + neg, reg_paint, pprogram); + ps->psglx->z += 1; + break; +#endif + default: assert(0); + } +#ifndef CONFIG_OPENGL + (void)neg; + (void)ptex; + (void)reg_paint; + (void)pprogram; +#endif +} + +static inline void +paint_region(session_t *ps, const struct managed_win *w, int x, int y, int wid, int hei, + double opacity, const region_t *reg_paint, xcb_render_picture_t pict) { + const int dx = (w ? w->g.x : 0) + x; + const int dy = (w ? w->g.y : 0) + y; + const int fullwid = w ? w->widthb : 0; + const int fullhei = w ? w->heightb : 0; + const bool argb = (w && (win_has_alpha(w) || ps->o.force_win_blend)); + const bool neg = (w && w->invert_color); + + render(ps, x, y, dx, dy, wid, hei, fullwid, fullhei, opacity, argb, neg, + w ? w->corner_radius : 0, pict, + (w ? w->paint.ptex : ps->root_tile_paint.ptex), reg_paint, +#ifdef CONFIG_OPENGL + w ? &ps->glx_prog_win : NULL +#else + NULL +#endif + , + XCB_NONE); +} + +/** + * Check whether a paint_t contains enough data. + */ +static inline bool paint_isvalid(session_t *ps, const paint_t *ppaint) { + // Don't check for presence of Pixmap here, because older X Composite doesn't + // provide it + if (!ppaint) + return false; + + if (bkend_use_xrender(ps) && !ppaint->pict) + return false; + +#ifdef CONFIG_OPENGL + if (BKEND_GLX == ps->o.backend && !glx_tex_binded(ppaint->ptex, XCB_NONE)) + return false; +#endif + + return true; +} + +/** + * Paint a window itself and dim it if asked. + */ +void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) { + // Fetch Pixmap + if (!w->paint.pixmap) { + w->paint.pixmap = x_new_id(ps->c); + set_ignore_cookie(ps, xcb_composite_name_window_pixmap(ps->c, w->base.id, + w->paint.pixmap)); + } + + xcb_drawable_t draw = w->paint.pixmap; + if (!draw) { + log_error("Failed to get pixmap from window %#010x (%s), window won't be " + "visible", + w->base.id, w->name); + return; + } + + // XRender: Build picture + if (bkend_use_xrender(ps) && !w->paint.pict) { + xcb_render_create_picture_value_list_t pa = { + .subwindowmode = XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS, + }; + + w->paint.pict = x_create_picture_with_pictfmt_and_pixmap( + ps->c, w->pictfmt, draw, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); + } + + // GLX: Build texture + // Let glx_bind_pixmap() determine pixmap size, because if the user + // is resizing windows, the width and height we get may not be up-to-date, + // causing the jittering issue M4he reported in #7. + if (!paint_bind_tex(ps, &w->paint, 0, 0, false, 0, w->a.visual, + (!ps->o.glx_no_rebind_pixmap && w->pixmap_damaged))) { + log_error("Failed to bind texture for window %#010x.", w->base.id); + } + w->pixmap_damaged = false; + + if (!paint_isvalid(ps, &w->paint)) { + log_error("Window %#010x is missing painting data.", w->base.id); + return; + } + + const int x = w->g.x; + const int y = w->g.y; + const uint16_t wid = to_u16_checked(w->widthb); + const uint16_t hei = to_u16_checked(w->heightb); + + xcb_render_picture_t pict = w->paint.pict; + + // Invert window color, if required + if (bkend_use_xrender(ps) && w->invert_color) { + xcb_render_picture_t newpict = x_create_picture_with_pictfmt( + ps->c, ps->root, wid, hei, w->pictfmt, 0, NULL); + if (newpict) { + // Apply clipping region to save some CPU + if (reg_paint) { + region_t reg; + pixman_region32_init(®); + pixman_region32_copy(®, (region_t *)reg_paint); + pixman_region32_translate(®, -x, -y); + // FIXME XFixesSetPictureClipRegion(ps->dpy, newpict, 0, + // 0, reg); + pixman_region32_fini(®); + } + + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, pict, XCB_NONE, + newpict, 0, 0, 0, 0, 0, 0, wid, hei); + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_DIFFERENCE, + ps->white_picture, XCB_NONE, newpict, 0, 0, + 0, 0, 0, 0, wid, hei); + // We use an extra PictOpInReverse operation to get correct + // pixel alpha. There could be a better solution. + if (win_has_alpha(w)) + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_IN_REVERSE, + pict, XCB_NONE, newpict, 0, 0, 0, 0, + 0, 0, wid, hei); + pict = newpict; + } + } + + if (w->frame_opacity == 1) { + paint_region(ps, w, 0, 0, wid, hei, w->opacity, reg_paint, pict); + } else { + // Painting parameters + const margin_t extents = win_calc_frame_extents(w); + const auto t = extents.top; + const auto l = extents.left; + const auto b = extents.bottom; + const auto r = extents.right; + +#define COMP_BDR(cx, cy, cwid, chei) \ + paint_region(ps, w, (cx), (cy), (cwid), (chei), w->frame_opacity * w->opacity, \ + reg_paint, pict) + + // Sanitize the margins, in case some broken WM makes + // top_width + bottom_width > height in some cases. + + do { + // top + int body_height = hei; + // ctop = checked top + // Make sure top margin is smaller than height + int ctop = min2(body_height, t); + if (ctop > 0) + COMP_BDR(0, 0, wid, ctop); + + body_height -= ctop; + if (body_height <= 0) + break; + + // bottom + // cbot = checked bottom + // Make sure bottom margin is not too large + int cbot = min2(body_height, b); + if (cbot > 0) + COMP_BDR(0, hei - cbot, wid, cbot); + + // Height of window exclude the margin + body_height -= cbot; + if (body_height <= 0) + break; + + // left + int body_width = wid; + int cleft = min2(body_width, l); + if (cleft > 0) + COMP_BDR(0, ctop, cleft, body_height); + + body_width -= cleft; + if (body_width <= 0) + break; + + // right + int cright = min2(body_width, r); + if (cright > 0) + COMP_BDR(wid - cright, ctop, cright, body_height); + + body_width -= cright; + if (body_width <= 0) + break; + + // body + paint_region(ps, w, cleft, ctop, body_width, body_height, + w->opacity, reg_paint, pict); + } while (0); + } + +#undef COMP_BDR + + if (pict != w->paint.pict) + free_picture(ps->c, &pict); + + // Dimming the window if needed + if (w->dim) { + double dim_opacity = ps->o.inactive_dim; + if (!ps->o.inactive_dim_fixed) + dim_opacity *= w->opacity; + + switch (ps->o.backend) { + case BKEND_XRENDER: + case BKEND_XR_GLX_HYBRID: { + auto cval = (uint16_t)(0xffff * dim_opacity); + + // Premultiply color + xcb_render_color_t color = { + .red = 0, + .green = 0, + .blue = 0, + .alpha = cval, + }; + + xcb_rectangle_t rect = { + .x = to_i16_checked(x), + .y = to_i16_checked(y), + .width = wid, + .height = hei, + }; + + xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_OVER, + ps->tgt_buffer.pict, color, 1, &rect); + } break; +#ifdef CONFIG_OPENGL + case BKEND_GLX: + glx_dim_dst(ps, x, y, wid, hei, (int)(ps->psglx->z - 0.7), + (float)dim_opacity, reg_paint); + break; +#endif + default: assert(false); + } + } +} + +extern const char *background_props_str[]; + +static bool get_root_tile(session_t *ps) { + /* + if (ps->o.paint_on_overlay) { + return ps->root_picture; + } */ + + assert(!ps->root_tile_paint.pixmap); + ps->root_tile_fill = false; + + bool fill = false; + xcb_pixmap_t pixmap = x_get_root_back_pixmap(ps->c, ps->root, ps->atoms); + + // Make sure the pixmap we got is valid + if (pixmap && !x_validate_pixmap(ps->c, pixmap)) + pixmap = XCB_NONE; + + // Create a pixmap if there isn't any + if (!pixmap) { + pixmap = x_create_pixmap(ps->c, (uint8_t)ps->depth, ps->root, 1, 1); + if (pixmap == XCB_NONE) { + log_error("Failed to create pixmaps for root tile."); + return false; + } + fill = true; + } + + // Create Picture + xcb_render_create_picture_value_list_t pa = { + .repeat = true, + }; + ps->root_tile_paint.pict = x_create_picture_with_visual_and_pixmap( + ps->c, ps->vis, pixmap, XCB_RENDER_CP_REPEAT, &pa); + + // Fill pixmap if needed + if (fill) { + xcb_render_color_t col; + xcb_rectangle_t rect; + + col.red = col.green = col.blue = 0x8080; + col.alpha = 0xffff; + + rect.x = rect.y = 0; + rect.width = rect.height = 1; + + xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, + ps->root_tile_paint.pict, col, 1, &rect); + } + + ps->root_tile_fill = fill; + ps->root_tile_paint.pixmap = pixmap; +#ifdef CONFIG_OPENGL + if (BKEND_GLX == ps->o.backend) + return paint_bind_tex(ps, &ps->root_tile_paint, 0, 0, true, 0, ps->vis, false); +#endif + + return true; +} + +/** + * Paint root window content. + */ +static void paint_root(session_t *ps, const region_t *reg_paint) { + // If there is no root tile pixmap, try getting one. + // If that fails, give up. + if (!ps->root_tile_paint.pixmap && !get_root_tile(ps)) + return; + + paint_region(ps, NULL, 0, 0, ps->root_width, ps->root_height, 1.0, reg_paint, + ps->root_tile_paint.pict); +} + +/** + * Generate shadow <code>Picture</code> for a window. + */ +static bool win_build_shadow(session_t *ps, struct managed_win *w, double opacity) { + const int width = w->widthb; + const int height = w->heightb; + // log_trace("(): building shadow for %s %d %d", w->name, width, height); + + 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(ps->c, ps->gaussian_map, opacity, width, height); + if (!shadow_image) { + log_error("failed to make shadow"); + return XCB_NONE; + } + + shadow_pixmap = + x_create_pixmap(ps->c, 8, ps->root, shadow_image->width, shadow_image->height); + shadow_pixmap_argb = + x_create_pixmap(ps->c, 32, ps->root, 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( + ps->c, XCB_PICT_STANDARD_A_8, shadow_pixmap, 0, NULL); + shadow_picture_argb = x_create_picture_with_standard_and_pixmap( + ps->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(ps->c); + xcb_create_gc(ps->c, gc, shadow_pixmap, 0, NULL); + + xcb_image_put(ps->c, shadow_pixmap, gc, shadow_image, 0, 0, 0); + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, ps->cshadow_picture, + shadow_picture, shadow_picture_argb, 0, 0, 0, 0, 0, 0, + shadow_image->width, shadow_image->height); + + assert(!w->shadow_paint.pixmap); + w->shadow_paint.pixmap = shadow_pixmap_argb; + assert(!w->shadow_paint.pict); + w->shadow_paint.pict = shadow_picture_argb; + + xcb_free_gc(ps->c, gc); + xcb_image_destroy(shadow_image); + xcb_free_pixmap(ps->c, shadow_pixmap); + xcb_render_free_picture(ps->c, shadow_picture); + + return true; + +shadow_picture_err: + if (shadow_image) + xcb_image_destroy(shadow_image); + if (shadow_pixmap) + xcb_free_pixmap(ps->c, shadow_pixmap); + if (shadow_pixmap_argb) + xcb_free_pixmap(ps->c, shadow_pixmap_argb); + if (shadow_picture) + xcb_render_free_picture(ps->c, shadow_picture); + if (shadow_picture_argb) + xcb_render_free_picture(ps->c, shadow_picture_argb); + if (gc) + xcb_free_gc(ps->c, gc); + + return false; +} + +/** + * Paint the shadow of a window. + */ +static inline void +win_paint_shadow(session_t *ps, struct managed_win *w, region_t *reg_paint) { + // Bind shadow pixmap to GLX texture if needed + paint_bind_tex(ps, &w->shadow_paint, 0, 0, false, 32, 0, false); + + if (!paint_isvalid(ps, &w->shadow_paint)) { + log_error("Window %#010x is missing shadow data.", w->base.id); + return; + } + + xcb_render_picture_t td = XCB_NONE; + bool should_clip = + (w->corner_radius > 0) && (!ps->o.wintype_option[w->window_type].full_shadow); + if (should_clip) { + if (ps->o.backend == BKEND_XRENDER || ps->o.backend == BKEND_XR_GLX_HYBRID) { + uint32_t max_ntraps = to_u32_checked(w->corner_radius); + xcb_render_trapezoid_t traps[4 * max_ntraps + 3]; + uint32_t n = make_rounded_window_shape( + traps, max_ntraps, w->corner_radius, w->widthb, w->heightb); + + td = x_create_picture_with_standard( + ps->c, ps->root, w->widthb, w->heightb, + XCB_PICT_STANDARD_ARGB_32, 0, 0); + xcb_render_color_t trans = { + .red = 0, .blue = 0, .green = 0, .alpha = 0}; + const xcb_rectangle_t rect = {.x = 0, + .y = 0, + .width = to_u16_checked(w->widthb), + .height = to_u16_checked(w->heightb)}; + xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, td, + trans, 1, &rect); + + auto solid = solid_picture(ps->c, ps->root, false, 1, 0, 0, 0); + xcb_render_trapezoids( + ps->c, XCB_RENDER_PICT_OP_OVER, solid, td, + x_get_pictfmt_for_standard(ps->c, XCB_PICT_STANDARD_A_8), 0, + 0, n, traps); + xcb_render_free_picture(ps->c, solid); + } else { + // Not implemented + } + } + + clip_t clip = { + .pict = td, + .x = -(w->shadow_dx), + .y = -(w->shadow_dy), + }; + render(ps, 0, 0, w->g.x + w->shadow_dx, w->g.y + w->shadow_dy, w->shadow_width, + w->shadow_height, w->widthb, w->heightb, w->shadow_opacity, true, false, 0, + w->shadow_paint.pict, w->shadow_paint.ptex, reg_paint, NULL, + should_clip ? &clip : NULL); + if (td) { + xcb_render_free_picture(ps->c, td); + } +} + +/** + * @brief Blur an area on a buffer. + * + * @param ps current session + * @param tgt_buffer a buffer as both source and destination + * @param x x pos + * @param y y pos + * @param wid width + * @param hei height + * @param blur_kerns blur kernels, ending with a NULL, guaranteed to have at + * least one kernel + * @param reg_clip a clipping region to be applied on intermediate buffers + * + * @return true if successful, false otherwise + */ +static bool +xr_blur_dst(session_t *ps, xcb_render_picture_t tgt_buffer, int16_t x, int16_t y, + uint16_t wid, uint16_t hei, struct x_convolution_kernel **blur_kerns, + int nkernels, const region_t *reg_clip, xcb_render_picture_t rounded) { + assert(blur_kerns); + assert(blur_kerns[0]); + + // Directly copying from tgt_buffer to it does not work, so we create a + // Picture in the middle. + xcb_render_picture_t tmp_picture = + x_create_picture_with_visual(ps->c, ps->root, wid, hei, ps->vis, 0, NULL); + + if (!tmp_picture) { + log_error("Failed to build intermediate Picture."); + return false; + } + + if (reg_clip && tmp_picture) + x_set_picture_clip_region(ps->c, tmp_picture, 0, 0, reg_clip); + + xcb_render_picture_t src_pict = tgt_buffer, dst_pict = tmp_picture; + for (int i = 0; i < nkernels; ++i) { + xcb_render_fixed_t *convolution_blur = blur_kerns[i]->kernel; + // `x / 65536.0` converts from X fixed point to double + int kwid = (int)((double)convolution_blur[0] / 65536.0), + khei = (int)((double)convolution_blur[1] / 65536.0); + bool rd_from_tgt = (tgt_buffer == src_pict); + + // 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( + ps->c, src_pict, strlen(XRFILTER_CONVOLUTION), XRFILTER_CONVOLUTION, + (uint32_t)(kwid * khei + 2), convolution_blur); + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, src_pict, XCB_NONE, + dst_pict, (rd_from_tgt ? x : 0), + (rd_from_tgt ? y : 0), 0, 0, (rd_from_tgt ? 0 : x), + (rd_from_tgt ? 0 : y), wid, hei); + xrfilter_reset(ps, src_pict); + + { + xcb_render_picture_t tmp = src_pict; + src_pict = dst_pict; + dst_pict = tmp; + } + } + + if (src_pict != tgt_buffer) + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_OVER, src_pict, rounded, + tgt_buffer, 0, 0, 0, 0, x, y, wid, hei); + + free_picture(ps->c, &tmp_picture); + + return true; +} + +/** + * Blur the background of a window. + */ +static inline void +win_blur_background(session_t *ps, struct managed_win *w, xcb_render_picture_t tgt_buffer, + const region_t *reg_paint) { + const int16_t x = w->g.x; + const int16_t y = w->g.y; + const auto wid = to_u16_checked(w->widthb); + const auto hei = to_u16_checked(w->heightb); + const int cr = w ? w->corner_radius : 0; + + double factor_center = 1.0; + // Adjust blur strength according to window opacity, to make it appear + // better during fading + if (!ps->o.blur_background_fixed) { + double pct = 1.0 - w->opacity * (1.0 - 1.0 / 9.0); + factor_center = pct * 8.0 / (1.1 - pct); + } + + switch (ps->o.backend) { + case BKEND_XRENDER: + case BKEND_XR_GLX_HYBRID: { + // Normalize blur kernels + for (int i = 0; i < ps->o.blur_kernel_count; i++) { + // Note: `x * 65536` converts double `x` to a X fixed point + // representation. `x / 65536` is the other way. + auto kern_src = ps->o.blur_kerns[i]; + auto kern_dst = ps->blur_kerns_cache[i]; + + assert(!kern_dst || (kern_src->w == kern_dst->kernel[0] / 65536 && + kern_src->h == kern_dst->kernel[1] / 65536)); + + // Skip for fixed factor_center if the cache exists already + if (ps->o.blur_background_fixed && kern_dst) { + continue; + } + + x_create_convolution_kernel(kern_src, factor_center, + &ps->blur_kerns_cache[i]); + } + + xcb_render_picture_t td = XCB_NONE; + if (cr) { + uint32_t max_ntraps = to_u32_checked(cr); + xcb_render_trapezoid_t traps[4 * max_ntraps + 3]; + uint32_t n = + make_rounded_window_shape(traps, max_ntraps, cr, wid, hei); + + td = x_create_picture_with_standard( + ps->c, ps->root, wid, hei, XCB_PICT_STANDARD_ARGB_32, 0, 0); + xcb_render_color_t trans = { + .red = 0, .blue = 0, .green = 0, .alpha = 0}; + const xcb_rectangle_t rect = {.x = 0, + .y = 0, + .width = to_u16_checked(wid), + .height = to_u16_checked(hei)}; + xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, td, + trans, 1, &rect); + + auto solid = solid_picture(ps->c, ps->root, false, 1, 0, 0, 0); + + xcb_render_trapezoids( + ps->c, XCB_RENDER_PICT_OP_OVER, solid, td, + x_get_pictfmt_for_standard(ps->c, XCB_PICT_STANDARD_A_8), 0, + 0, n, traps); + xcb_render_free_picture(ps->c, solid); + } + + // Minimize the region we try to blur, if the window itself is not + // opaque, only the frame is. + region_t reg_blur = win_get_bounding_shape_global_by_val(w); + if (w->mode == WMODE_FRAME_TRANS && !ps->o.force_win_blend) { + region_t reg_noframe; + pixman_region32_init(®_noframe); + win_get_region_noframe_local(w, ®_noframe); + pixman_region32_translate(®_noframe, w->g.x, w->g.y); + pixman_region32_subtract(®_blur, ®_blur, ®_noframe); + pixman_region32_fini(®_noframe); + } + + // Translate global coordinates to local ones + pixman_region32_translate(®_blur, -x, -y); + xr_blur_dst(ps, tgt_buffer, x, y, wid, hei, ps->blur_kerns_cache, + ps->o.blur_kernel_count, ®_blur, td); + if (td) { + xcb_render_free_picture(ps->c, td); + } + pixman_region32_clear(®_blur); + } break; +#ifdef CONFIG_OPENGL + case BKEND_GLX: + // TODO(compton) Handle frame opacity + glx_blur_dst(ps, x, y, wid, hei, (float)ps->psglx->z - 0.5f, + (float)factor_center, reg_paint, &w->glx_blur_cache); + break; +#endif + default: assert(0); + } +#ifndef CONFIG_OPENGL + (void)reg_paint; +#endif +} + +/// paint all windows +/// region = ?? +/// region_real = the damage region +void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { + if (ps->o.xrender_sync_fence || (ps->drivers & DRIVER_NVIDIA)) { + 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; + } + } + + region_t region; + pixman_region32_init(®ion); + int buffer_age = get_buffer_age(ps); + if (buffer_age == -1 || buffer_age > ps->ndamage || ignore_damage) { + pixman_region32_copy(®ion, &ps->screen_reg); + } else { + for (int i = 0; i < get_buffer_age(ps); i++) { + auto curr = ((ps->damage - ps->damage_ring) + i) % ps->ndamage; + pixman_region32_union(®ion, ®ion, &ps->damage_ring[curr]); + } + } + + if (!pixman_region32_not_empty(®ion)) { + return; + } + +#ifdef DEBUG_REPAINT + static struct timespec last_paint = {0}; +#endif + + if (ps->o.resize_damage > 0) { + resize_region_in_place(®ion, ps->o.resize_damage, ps->o.resize_damage); + } + + // Remove the damaged area out of screen + pixman_region32_intersect(®ion, ®ion, &ps->screen_reg); + + if (!paint_isvalid(ps, &ps->tgt_buffer)) { + if (!ps->tgt_buffer.pixmap) { + free_paint(ps, &ps->tgt_buffer); + ps->tgt_buffer.pixmap = + x_create_pixmap(ps->c, (uint8_t)ps->depth, ps->root, + ps->root_width, ps->root_height); + if (ps->tgt_buffer.pixmap == XCB_NONE) { + log_fatal("Failed to allocate a screen-sized pixmap for" + "painting"); + exit(1); + } + } + + if (BKEND_GLX != ps->o.backend) + ps->tgt_buffer.pict = x_create_picture_with_visual_and_pixmap( + ps->c, ps->vis, ps->tgt_buffer.pixmap, 0, 0); + } + + if (BKEND_XRENDER == ps->o.backend) { + x_set_picture_clip_region(ps->c, ps->tgt_picture, 0, 0, ®ion); + } + +#ifdef CONFIG_OPENGL + if (bkend_use_glx(ps)) { + ps->psglx->z = 0.0; + } +#endif + + region_t reg_tmp, *reg_paint; + pixman_region32_init(®_tmp); + if (t) { + // Calculate the region upon which the root window is to be + // painted based on the ignore region of the lowest window, if + // available + pixman_region32_subtract(®_tmp, ®ion, t->reg_ignore); + reg_paint = ®_tmp; + } else { + reg_paint = ®ion; + } + + // Region on screen we don't want any shadows on + region_t reg_shadow_clip; + pixman_region32_init(®_shadow_clip); + + set_tgt_clip(ps, reg_paint); + paint_root(ps, reg_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) { + region_t bshape_no_corners = + win_get_bounding_shape_global_without_corners_by_val(w); + region_t bshape_corners = win_get_bounding_shape_global_by_val(w); + // Painting shadow + if (w->shadow) { + // Lazy shadow building + if (!w->shadow_paint.pixmap) + if (!win_build_shadow(ps, w, 1)) + log_error("build shadow failed"); + + // Shadow doesn't need to be painted underneath the body + // of the windows above. Because no one can see it + pixman_region32_subtract(®_tmp, ®ion, w->reg_ignore); + + // Mask out the region we don't want shadow on + if (pixman_region32_not_empty(&ps->shadow_exclude_reg)) + pixman_region32_subtract(®_tmp, ®_tmp, + &ps->shadow_exclude_reg); + if (pixman_region32_not_empty(®_shadow_clip)) { + pixman_region32_subtract(®_tmp, ®_tmp, ®_shadow_clip); + } + + // Might be worth while to crop the region to shadow + // border + assert(w->shadow_width >= 0 && w->shadow_height >= 0); + pixman_region32_intersect_rect( + ®_tmp, ®_tmp, w->g.x + w->shadow_dx, w->g.y + w->shadow_dy, + (uint)w->shadow_width, (uint)w->shadow_height); + + // Mask out the body of the window from the shadow if + // needed Doing it here instead of in make_shadow() for + // saving GPU power and handling shaped windows (XXX + // unconfirmed) + if (!ps->o.wintype_option[w->window_type].full_shadow) + pixman_region32_subtract(®_tmp, ®_tmp, &bshape_no_corners); + + 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( + ®_tmp, ®_tmp, + &ps->xinerama_scr_regs[w->xinerama_scr]); + + // Detect if the region is empty before painting + if (pixman_region32_not_empty(®_tmp)) { + set_tgt_clip(ps, ®_tmp); + win_paint_shadow(ps, w, ®_tmp); + } + } + + // Only clip shadows above visible windows + if (w->opacity * MAX_ALPHA >= 1) { + if (w->clip_shadow_above) { + // Add window bounds to shadow-clip region + pixman_region32_union(®_shadow_clip, ®_shadow_clip, + &bshape_corners); + } else { + // Remove overlapping window bounds from shadow-clip + // region + pixman_region32_subtract( + ®_shadow_clip, ®_shadow_clip, &bshape_corners); + } + } + + // Calculate the paint region based on the reg_ignore of the current + // window and its bounding region. + // Remember, reg_ignore is the union of all windows above the current + // window. + pixman_region32_subtract(®_tmp, ®ion, w->reg_ignore); + pixman_region32_intersect(®_tmp, ®_tmp, &bshape_corners); + pixman_region32_fini(&bshape_corners); + pixman_region32_fini(&bshape_no_corners); + + if (pixman_region32_not_empty(®_tmp)) { + set_tgt_clip(ps, ®_tmp); + +#ifdef CONFIG_OPENGL + // If rounded corners backup the region first + if (w->corner_radius > 0 && ps->o.backend == BKEND_GLX) { + const int16_t x = w->g.x; + const int16_t y = w->g.y; + const auto wid = to_u16_checked(w->widthb); + const auto hei = to_u16_checked(w->heightb); + glx_bind_texture(ps, &w->glx_texture_bg, x, y, wid, hei); + } +#endif + + // Blur window background + if (w->blur_background && + (w->mode == WMODE_TRANS || + (ps->o.blur_background_frame && w->mode == WMODE_FRAME_TRANS) || + ps->o.force_win_blend)) { + win_blur_background(ps, w, ps->tgt_buffer.pict, ®_tmp); + } + + // Painting the window + paint_one(ps, w, ®_tmp); + +#ifdef CONFIG_OPENGL + // Rounded corners for XRender is implemented inside render() + // Round window corners + if (w->corner_radius > 0 && ps->o.backend == BKEND_GLX) { + const auto wid = to_u16_checked(w->widthb); + const auto hei = to_u16_checked(w->heightb); + glx_round_corners_dst(ps, w, w->glx_texture_bg, w->g.x, + w->g.y, wid, hei, + (float)ps->psglx->z - 0.5F, + (float)w->corner_radius, ®_tmp); + } +#endif + } + } + + // Free up all temporary regions + pixman_region32_fini(®_tmp); + pixman_region32_fini(®_shadow_clip); + + // 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); + + // Do this as early as possible + set_tgt_clip(ps, &ps->screen_reg); + + if (ps->o.vsync) { + // Make sure all previous requests are processed to achieve best + // effect + x_sync(ps->c); +#ifdef CONFIG_OPENGL + if (glx_has_context(ps)) { + if (ps->o.vsync_use_glfinish) + glFinish(); + else + glFlush(); + glXWaitX(); + } +#endif + } + + if (ps->vsync_wait) { + ps->vsync_wait(ps); + } + + auto rwidth = to_u16_checked(ps->root_width); + auto rheight = to_u16_checked(ps->root_height); + switch (ps->o.backend) { + case BKEND_XRENDER: + if (ps->o.monitor_repaint) { + // Copy the screen content to a new picture, and highlight the + // paint region. This is not very efficient, but since it's for + // debug only, we don't really care + + // First we create a new picture, and copy content from the buffer + // to it + auto pictfmt = x_get_pictform_for_visual(ps->c, ps->vis); + xcb_render_picture_t new_pict = x_create_picture_with_pictfmt( + ps->c, ps->root, rwidth, rheight, pictfmt, 0, NULL); + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, + ps->tgt_buffer.pict, XCB_NONE, new_pict, 0, + 0, 0, 0, 0, 0, rwidth, rheight); + + // Next, we set the region of paint and highlight it + x_set_picture_clip_region(ps->c, new_pict, 0, 0, ®ion); + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_OVER, ps->white_picture, + ps->alpha_picts[MAX_ALPHA / 2], new_pict, 0, + 0, 0, 0, 0, 0, rwidth, rheight); + + // Finally, clear clip regions of new_pict and the screen, and put + // the whole thing on screen + x_set_picture_clip_region(ps->c, new_pict, 0, 0, &ps->screen_reg); + x_set_picture_clip_region(ps->c, ps->tgt_picture, 0, 0, &ps->screen_reg); + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, new_pict, + XCB_NONE, ps->tgt_picture, 0, 0, 0, 0, 0, 0, + rwidth, rheight); + xcb_render_free_picture(ps->c, new_pict); + } else + xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, + ps->tgt_buffer.pict, XCB_NONE, ps->tgt_picture, + 0, 0, 0, 0, 0, 0, rwidth, rheight); + break; +#ifdef CONFIG_OPENGL + case BKEND_XR_GLX_HYBRID: + x_sync(ps->c); + if (ps->o.vsync_use_glfinish) + glFinish(); + else + glFlush(); + glXWaitX(); + assert(ps->tgt_buffer.pixmap); + paint_bind_tex(ps, &ps->tgt_buffer, ps->root_width, ps->root_height, + false, ps->depth, ps->vis, !ps->o.glx_no_rebind_pixmap); + if (ps->o.vsync_use_glfinish) + glFinish(); + else + glFlush(); + glXWaitX(); + glx_render(ps, ps->tgt_buffer.ptex, 0, 0, 0, 0, ps->root_width, + ps->root_height, 0, 1.0, false, false, ®ion, NULL); + fallthrough(); + case BKEND_GLX: glXSwapBuffers(ps->dpy, get_tgt_window(ps)); break; +#endif + default: assert(0); + } + + x_sync(ps->c); + +#ifdef CONFIG_OPENGL + if (glx_has_context(ps)) { + glFlush(); + glXWaitX(); + } +#endif + +#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 + + // Free the paint region + pixman_region32_fini(®ion); +} + +/** + * Query needed X Render / OpenGL filters to check for their existence. + */ +static bool xr_init_blur(session_t *ps) { + // Query filters + xcb_render_query_filters_reply_t *pf = xcb_render_query_filters_reply( + ps->c, xcb_render_query_filters(ps->c, get_tgt_window(ps)), NULL); + if (pf) { + xcb_str_iterator_t iter = xcb_render_query_filters_filters_iterator(pf); + for (; iter.rem; xcb_str_next(&iter)) { + int len = xcb_str_name_length(iter.data); + char *name = xcb_str_name(iter.data); + // Check for the convolution filter + if (strlen(XRFILTER_CONVOLUTION) == len && + !memcmp(XRFILTER_CONVOLUTION, name, strlen(XRFILTER_CONVOLUTION))) + ps->xrfilter_convolution_exists = true; + } + free(pf); + } + + // Turn features off if any required filter is not present + if (!ps->xrfilter_convolution_exists) { + log_error("Xrender convolution filter " + "unsupported by your X server. " + "Background blur is not possible."); + return false; + } + + return true; +} + +/** + * Pregenerate alpha pictures. + */ +static bool init_alpha_picts(session_t *ps) { + ps->alpha_picts = ccalloc(MAX_ALPHA + 1, xcb_render_picture_t); + + for (int i = 0; i <= MAX_ALPHA; ++i) { + double o = (double)i / MAX_ALPHA; + ps->alpha_picts[i] = solid_picture(ps->c, ps->root, false, o, 0, 0, 0); + if (ps->alpha_picts[i] == XCB_NONE) + return false; + } + return true; +} + +bool init_render(session_t *ps) { + if (ps->o.backend == BKEND_DUMMY) { + return false; + } + + // Initialize OpenGL as early as possible +#ifdef CONFIG_OPENGL + glxext_init(ps->dpy, ps->scr); +#endif + if (bkend_use_glx(ps)) { +#ifdef CONFIG_OPENGL + if (!glx_init(ps, true)) + return false; +#else + log_error("GLX backend support not compiled in."); + return false; +#endif + } + + // Initialize VSync + if (!vsync_init(ps)) { + return false; + } + + // Initialize window GL shader + if (BKEND_GLX == ps->o.backend && ps->o.glx_fshader_win_str) { +#ifdef CONFIG_OPENGL + if (!glx_load_prog_main(NULL, ps->o.glx_fshader_win_str, &ps->glx_prog_win)) + return false; +#else + log_error("GLSL supported not compiled in, can't load " + "shader."); + return false; +#endif + } + + if (!init_alpha_picts(ps)) { + log_error("Failed to init alpha pictures."); + return false; + } + + // Blur filter + if (ps->o.blur_method && ps->o.blur_method != BLUR_METHOD_KERNEL) { + log_warn("Old backends only support blur method \"kernel\". Your blur " + "setting will not be applied"); + ps->o.blur_method = BLUR_METHOD_NONE; + } + + if (ps->o.blur_method == BLUR_METHOD_KERNEL) { + ps->blur_kerns_cache = + ccalloc(ps->o.blur_kernel_count, struct x_convolution_kernel *); + + bool ret = false; + if (ps->o.backend == BKEND_GLX) { +#ifdef CONFIG_OPENGL + ret = glx_init_blur(ps); +#else + assert(false); +#endif + } else { + ret = xr_init_blur(ps); + } + if (!ret) { + return ret; + } + } + + ps->black_picture = solid_picture(ps->c, ps->root, true, 1, 0, 0, 0); + ps->white_picture = solid_picture(ps->c, ps->root, true, 1, 1, 1, 1); + + if (ps->black_picture == XCB_NONE || ps->white_picture == XCB_NONE) { + log_error("Failed to create solid xrender pictures."); + return false; + } + + // Generates another Picture for shadows if the color is modified by + // user + if (ps->o.shadow_red == 0 && ps->o.shadow_green == 0 && ps->o.shadow_blue == 0) { + ps->cshadow_picture = ps->black_picture; + } else { + ps->cshadow_picture = solid_picture(ps->c, ps->root, true, 1, ps->o.shadow_red, + ps->o.shadow_green, ps->o.shadow_blue); + if (ps->cshadow_picture == XCB_NONE) { + log_error("Failed to create shadow picture."); + return false; + } + } + + // Initialize our rounded corners fragment shader + if (ps->o.corner_radius > 0 && ps->o.backend == BKEND_GLX) { +#ifdef CONFIG_OPENGL + if (!glx_init_rounded_corners(ps)) { + log_error("Failed to init rounded corners shader."); + return false; + } +#else + assert(false); +#endif + } + return true; +} + +/** + * Free root tile related things. + */ +void free_root_tile(session_t *ps) { + free_picture(ps->c, &ps->root_tile_paint.pict); +#ifdef CONFIG_OPENGL + free_texture(ps, &ps->root_tile_paint.ptex); +#else + assert(!ps->root_tile_paint.ptex); +#endif + if (ps->root_tile_fill) { + xcb_free_pixmap(ps->c, ps->root_tile_paint.pixmap); + ps->root_tile_paint.pixmap = XCB_NONE; + } + ps->root_tile_paint.pixmap = XCB_NONE; + ps->root_tile_fill = false; +} + +void deinit_render(session_t *ps) { + // Free alpha_picts + for (int i = 0; i <= MAX_ALPHA; ++i) + free_picture(ps->c, &ps->alpha_picts[i]); + free(ps->alpha_picts); + ps->alpha_picts = NULL; + + // Free cshadow_picture and black_picture + if (ps->cshadow_picture == ps->black_picture) + ps->cshadow_picture = XCB_NONE; + else + free_picture(ps->c, &ps->cshadow_picture); + + free_picture(ps->c, &ps->black_picture); + free_picture(ps->c, &ps->white_picture); + + // Free other X resources + free_root_tile(ps); + +#ifdef CONFIG_OPENGL + free(ps->root_tile_paint.fbcfg); + if (bkend_use_glx(ps)) { + glx_destroy(ps); + } +#endif + + if (ps->o.blur_method != BLUR_METHOD_NONE) { + for (int i = 0; i < ps->o.blur_kernel_count; i++) { + free(ps->blur_kerns_cache[i]); + } + free(ps->blur_kerns_cache); + } +} + +// vim: set ts=8 sw=8 noet : diff --git a/src/render.h b/src/render.h new file mode 100644 index 0000000..95a46db --- /dev/null +++ b/src/render.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> +#pragma once + +#include <stdbool.h> +#include <xcb/render.h> +#include <xcb/xcb.h> +#ifdef CONFIG_OPENGL +#include "backend/gl/glx.h" +#endif +#include "region.h" + +typedef struct _glx_texture glx_texture_t; +typedef struct glx_prog_main glx_prog_main_t; +typedef struct session session_t; + +struct managed_win; + +typedef struct paint { + xcb_pixmap_t pixmap; + xcb_render_picture_t pict; + glx_texture_t *ptex; +#ifdef CONFIG_OPENGL + struct glx_fbconfig_info *fbcfg; +#endif +} paint_t; + +typedef struct clip { + xcb_render_picture_t pict; + int x; + int y; +} clip_t; + +void render(session_t *ps, int x, int y, int dx, int dy, int w, int h, int fullw, + int fullh, double opacity, bool argb, bool neg, int cr, + xcb_render_picture_t pict, glx_texture_t *ptex, const region_t *reg_paint, + const glx_prog_main_t *pprogram, clip_t *clip); +void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint); + +void paint_all(session_t *ps, struct managed_win *const t, bool ignore_damage); + +void free_picture(xcb_connection_t *c, xcb_render_picture_t *p); + +void free_paint(session_t *ps, paint_t *ppaint); +void free_root_tile(session_t *ps); + +bool init_render(session_t *ps); +void deinit_render(session_t *ps); + +int maximum_buffer_age(session_t *); diff --git a/src/string_utils.c b/src/string_utils.c new file mode 100644 index 0000000..65af0f2 --- /dev/null +++ b/src/string_utils.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> + +#include <string.h> + +#include <test.h> + +#include "compiler.h" +#include "string_utils.h" +#include "utils.h" + +#pragma GCC diagnostic push + +// gcc warns about legitimate strncpy in mstrjoin and mstrextend +// strncpy(str, src1, len1) intentional truncates the null byte from src1. +// strncpy(str+len1, src2, len2) uses bound depends on the source argument, +// but str is allocated with len1+len2+1, so this strncpy can't overflow +#pragma GCC diagnostic ignored "-Wpragmas" +#pragma GCC diagnostic ignored "-Wstringop-truncation" +#pragma GCC diagnostic ignored "-Wstringop-overflow" + +/** + * Allocate the space and join two strings. + */ +char *mstrjoin(const char *src1, const char *src2) { + auto len1 = strlen(src1); + auto len2 = strlen(src2); + auto len = len1 + len2 + 1; + auto str = ccalloc(len, char); + + strncpy(str, src1, len1); + strncpy(str + len1, src2, len2); + str[len - 1] = '\0'; + + return str; +} + +TEST_CASE(mstrjoin) { + char *str = mstrjoin("asdf", "qwer"); + TEST_STREQUAL(str, "asdfqwer"); + free(str); + + str = mstrjoin("", "qwer"); + TEST_STREQUAL(str, "qwer"); + free(str); + + str = mstrjoin("asdf", ""); + TEST_STREQUAL(str, "asdf"); + free(str); +} + +/** + * Concatenate a string on heap with another string. + */ +void mstrextend(char **psrc1, const char *src2) { + if (!*psrc1) { + *psrc1 = strdup(src2); + return; + } + + auto len1 = strlen(*psrc1); + auto len2 = strlen(src2); + auto len = len1 + len2 + 1; + *psrc1 = crealloc(*psrc1, len); + + strncpy(*psrc1 + len1, src2, len2); + (*psrc1)[len - 1] = '\0'; +} + +TEST_CASE(mstrextend) { + char *str1 = NULL; + mstrextend(&str1, "asdf"); + TEST_STREQUAL(str1, "asdf"); + + mstrextend(&str1, "asd"); + TEST_STREQUAL(str1, "asdfasd"); + + mstrextend(&str1, ""); + TEST_STREQUAL(str1, "asdfasd"); + free(str1); +} + +#pragma GCC diagnostic pop + +/// Parse a floating point number of form (+|-)?[0-9]*(\.[0-9]*) +double strtod_simple(const char *src, const char **end) { + double neg = 1; + if (*src == '-') { + neg = -1; + src++; + } else if (*src == '+') { + src++; + } + + double ret = 0; + while (*src >= '0' && *src <= '9') { + ret = ret * 10 + (*src - '0'); + src++; + } + + if (*src == '.') { + double frac = 0, mult = 0.1; + src++; + while (*src >= '0' && *src <= '9') { + frac += mult * (*src - '0'); + mult *= 0.1; + src++; + } + ret += frac; + } + + *end = src; + return ret * neg; +} + +TEST_CASE(strtod_simple) { + const char *end; + double result = strtod_simple("1.0", &end); + TEST_EQUAL(result, 1); + TEST_EQUAL(*end, '\0'); + + result = strtod_simple("-1.0", &end); + TEST_EQUAL(result, -1); + TEST_EQUAL(*end, '\0'); + + result = strtod_simple("+.5", &end); + TEST_EQUAL(result, 0.5); + TEST_EQUAL(*end, '\0'); +} diff --git a/src/string_utils.h b/src/string_utils.h new file mode 100644 index 0000000..38febde --- /dev/null +++ b/src/string_utils.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> +#pragma once +#include <ctype.h> +#include <stddef.h> + +#include "compiler.h" + +#define mstrncmp(s1, s2) strncmp((s1), (s2), strlen(s1)) + +char *mstrjoin(const char *src1, const char *src2); +char *mstrjoin3(const char *src1, const char *src2, const char *src3); +void mstrextend(char **psrc1, const char *src2); + +/// Parse a floating point number of form (+|-)?[0-9]*(\.[0-9]*) +double strtod_simple(const char *, const char **); + +static inline int uitostr(unsigned int n, char *buf) { + int ret = 0; + unsigned int tmp = n; + while (tmp > 0) { + tmp /= 10; + ret++; + } + + if (ret == 0) + ret = 1; + + int pos = ret; + while (pos--) { + buf[pos] = (char)(n % 10 + '0'); + n /= 10; + } + return ret; +} + +static inline const char *skip_space_const(const char *src) { + if (!src) + return NULL; + while (*src && isspace((unsigned char)*src)) + src++; + return src; +} + +static inline char *skip_space_mut(char *src) { + if (!src) + return NULL; + while (*src && isspace((unsigned char)*src)) + src++; + return src; +} + +#define skip_space(x) \ + _Generic((x), char * : skip_space_mut, const char * : skip_space_const)(x) diff --git a/src/types.h b/src/types.h new file mode 100644 index 0000000..c8d747b --- /dev/null +++ b/src/types.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> + +#pragma once + +/// Some common types + +#include <stdint.h> + +/// Enumeration type to represent switches. +typedef enum { + OFF = 0, // false + ON, // true + UNSET +} switch_t; + +/// A structure representing margins around a rectangle. +typedef struct { + int top; + int left; + int bottom; + int right; +} margin_t; + +struct color { + double red, green, blue, alpha; +}; + +typedef uint32_t opacity_t; + +#define MARGIN_INIT \ + { 0, 0, 0, 0 } diff --git a/src/uthash_extra.h b/src/uthash_extra.h new file mode 100644 index 0000000..cbc1056 --- /dev/null +++ b/src/uthash_extra.h @@ -0,0 +1,7 @@ +#pragma once + +#include <uthash.h> + +#define HASH_ITER2(head, el) \ + for (__typeof__(head) el = (head), __tmp = el != NULL ? el->hh.next : NULL; \ + el != NULL; el = __tmp, __tmp = el != NULL ? el->hh.next : NULL) diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..8a27f39 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,51 @@ +#include <stdio.h> +#include <string.h> +#include <sys/uio.h> + +#include "compiler.h" +#include "string_utils.h" +#include "utils.h" + +/// Report allocation failure without allocating memory +void report_allocation_failure(const char *func, const char *file, unsigned int line) { + // Since memory allocation failed, we try to print this error message without any + // memory allocation. Since logging framework allocates memory (and might even + // have not been initialized yet), so we can't use it. + char buf[11]; + int llen = uitostr(line, buf); + const char msg1[] = " has failed to allocate memory, "; + const char msg2[] = ". Aborting...\n"; + const struct iovec v[] = { + {.iov_base = (void *)func, .iov_len = strlen(func)}, + {.iov_base = "()", .iov_len = 2}, + {.iov_base = (void *)msg1, .iov_len = sizeof(msg1) - 1}, + {.iov_base = "at ", .iov_len = 3}, + {.iov_base = (void *)file, .iov_len = strlen(file)}, + {.iov_base = ":", .iov_len = 1}, + {.iov_base = buf, .iov_len = (size_t)llen}, + {.iov_base = (void *)msg2, .iov_len = sizeof(msg2) - 1}, + }; + + writev(STDERR_FILENO, v, ARR_SIZE(v)); + abort(); + + unreachable; +} + +/// +/// Calculates next closest power of two of 32bit integer n +/// ref: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 +/// +int next_power_of_two(int n) +{ + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + n++; + return n; +} + +// vim: set noet sw=8 ts=8 : diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..6bb8643 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> +#pragma once +#include <assert.h> +#include <ctype.h> +#include <limits.h> +#include <math.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <test.h> + +#include "compiler.h" +#include "types.h" + +#define ARR_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) + +#ifdef __FAST_MATH__ +#warning Use of -ffast-math can cause rendering error or artifacts, \ + therefore it is not recommended. +#endif + +#ifdef __clang__ +__attribute__((optnone)) +#else +__attribute__((optimize("-fno-fast-math"))) +#endif +static inline bool +safe_isnan(double a) { + return __builtin_isnan(a); +} + +/// Same as assert(false), but make sure we abort _even in release builds_. +/// Silence compiler warning caused by release builds making some code paths reachable. +#define BUG() \ + do { \ + assert(false); \ + abort(); \ + } while (0) +#define CHECK_EXPR(...) ((void)0) +/// Same as assert, but evaluates the expression even in release builds +#define CHECK(expr) \ + do { \ + auto _ = (expr); \ + /* make sure the original expression appears in the assertion message */ \ + assert((CHECK_EXPR(expr), _)); \ + (void)_; \ + } while (0) + +/// Asserts that var is within [lower, upper]. Silence compiler warning about expressions +/// being always true or false. +#define ASSERT_IN_RANGE(var, lower, upper) \ + do { \ + auto __tmp attr_unused = (var); \ + _Pragma("GCC diagnostic push"); \ + _Pragma("GCC diagnostic ignored \"-Wtype-limits\""); \ + assert(__tmp >= lower); \ + assert(__tmp <= upper); \ + _Pragma("GCC diagnostic pop"); \ + } while (0) + +/// Asserts that var >= lower. Silence compiler warning about expressions +/// being always true or false. +#define ASSERT_GEQ(var, lower) \ + do { \ + auto __tmp attr_unused = (var); \ + _Pragma("GCC diagnostic push"); \ + _Pragma("GCC diagnostic ignored \"-Wtype-limits\""); \ + assert(__tmp >= lower); \ + _Pragma("GCC diagnostic pop"); \ + } while (0) + +// Some macros for checked cast +// Note these macros are not complete, as in, they won't work for every integer types. But +// they are good enough for our use cases. + +#define to_int_checked(val) \ + ({ \ + int64_t tmp = (val); \ + ASSERT_IN_RANGE(tmp, INT_MIN, INT_MAX); \ + (int)tmp; \ + }) + +#define to_char_checked(val) \ + ({ \ + int64_t tmp = (val); \ + ASSERT_IN_RANGE(tmp, CHAR_MIN, CHAR_MAX); \ + (char)tmp; \ + }) + +#define to_u16_checked(val) \ + ({ \ + auto tmp = (val); \ + ASSERT_IN_RANGE(tmp, 0, UINT16_MAX); \ + (uint16_t) tmp; \ + }) + +#define to_i16_checked(val) \ + ({ \ + int64_t tmp = (val); \ + ASSERT_IN_RANGE(tmp, INT16_MIN, INT16_MAX); \ + (int16_t) tmp; \ + }) + +#define to_u32_checked(val) \ + ({ \ + auto tmp = (val); \ + int64_t max attr_unused = UINT32_MAX; /* silence clang tautological \ + comparison warning*/ \ + ASSERT_IN_RANGE(tmp, 0, max); \ + (uint32_t) tmp; \ + }) +/** + * Normalize an int value to a specific range. + * + * @param i int value to normalize + * @param min minimal value + * @param max maximum value + * @return normalized value + */ +static inline int attr_const normalize_i_range(int i, int min, int max) { + if (i > max) + return max; + if (i < min) + return min; + return i; +} + +/** + * Linearly interpolate from a range into another. + * + * @param a,b first range + * @param c,d second range + * @param value value to interpolate, should be in range [a,b] + * @return interpolated value in range [c,d] + */ +static inline int attr_const lerp_range(int a, int b, int c, int d, int value) { + ASSERT_IN_RANGE(value, a, b); + return (d-c)*(value-a)/(b-a) + c; +} + +#define min2(a, b) ((a) > (b) ? (b) : (a)) +#define max2(a, b) ((a) > (b) ? (a) : (b)) + +/// clamp `val` into interval [min, max] +#define clamp(val, min, max) max2(min2(val, max), min) + +/** + * Normalize a double value to a specific range. + * + * @param d double value to normalize + * @param min minimal value + * @param max maximum value + * @return normalized value + */ +static inline double attr_const normalize_d_range(double d, double min, double max) { + if (d > max) + return max; + if (d < min) + return min; + return d; +} + +/** + * Normalize a double value to 0.\ 0 - 1.\ 0. + * + * @param d double value to normalize + * @return normalized value + */ +static inline double attr_const normalize_d(double d) { + return normalize_d_range(d, 0.0, 1.0); +} + +/** + * Convert a hex RGB string to RGB + */ +static inline struct color hex_to_rgb(const char *hex) { + struct color rgb; + // Ignore the # in front of the string + const char *sane_hex = hex + 1; + int hex_color = (int)strtol(sane_hex, NULL, 16); + rgb.red = (float)(hex_color >> 16) / 256; + rgb.green = (float)((hex_color & 0x00ff00) >> 8) / 256; + rgb.blue = (float)(hex_color & 0x0000ff) / 256; + + return rgb; +} + +attr_noret void +report_allocation_failure(const char *func, const char *file, unsigned int line); + +/** + * @brief Quit if the passed-in pointer is empty. + */ +static inline void * +allocchk_(const char *func_name, const char *file, unsigned int line, void *ptr) { + if (unlikely(!ptr)) { + report_allocation_failure(func_name, file, line); + } + return ptr; +} + +/// @brief Wrapper of allocchk_(). +#define allocchk(ptr) allocchk_(__func__, __FILE__, __LINE__, ptr) + +/// @brief Wrapper of malloc(). +#define cmalloc(type) ((type *)allocchk(malloc(sizeof(type)))) + +/// @brief Wrapper of malloc() that takes a size +#define cvalloc(size) allocchk(malloc(size)) + +/// @brief Wrapper of calloc(). +#define ccalloc(nmemb, type) \ + ({ \ + auto tmp = (nmemb); \ + ASSERT_GEQ(tmp, 0); \ + ((type *)allocchk(calloc((size_t)tmp, sizeof(type)))); \ + }) + +/// @brief Wrapper of ealloc(). +#define crealloc(ptr, nmemb) \ + ({ \ + auto tmp = (nmemb); \ + ASSERT_GEQ(tmp, 0); \ + ((__typeof__(ptr))allocchk(realloc((ptr), (size_t)tmp * sizeof(*(ptr))))); \ + }) + +/// RC_TYPE generates a reference counted type from `type` +/// +/// parameters: +/// name = the generated type will be called `name`_t. +/// ctor = the constructor of `type`, will be called when +/// a value of `type` is created. should take one +/// argument of `type *`. +/// dtor = the destructor. will be called when all reference +/// is gone. has same signature as ctor +/// Q = function qualifier. this is the qualifier that +/// will be put before generated functions +// +/// functions generated: +/// `name`_new: create a new reference counted object of `type` +/// `name`_ref: increment the reference counter, return a +/// reference to the object +/// `name`_unref: decrement the reference counter. take a `type **` +/// because it needs to nullify the reference. +#define RC_TYPE(type, name, ctor, dtor, Q) \ + typedef struct { \ + type inner; \ + int ref_count; \ + } name##_internal_t; \ + typedef type name##_t; \ + Q type *name##_new(void) { \ + name##_internal_t *ret = cmalloc(name##_internal_t); \ + ctor((type *)ret); \ + ret->ref_count = 1; \ + return (type *)ret; \ + } \ + Q type *name##_ref(type *a) { \ + __auto_type b = (name##_internal_t *)a; \ + b->ref_count++; \ + return a; \ + } \ + Q void name##_unref(type **a) { \ + __auto_type b = (name##_internal_t *)*a; \ + if (!b) \ + return; \ + b->ref_count--; \ + if (!b->ref_count) { \ + dtor((type *)b); \ + free(b); \ + } \ + *a = NULL; \ + } + +/// Generate prototypes for functions generated by RC_TYPE +#define RC_TYPE_PROTO(type, name) \ + typedef type name##_t; \ + type *name##_new(void); \ + void name##_ref(type *a); \ + void name##_unref(type **a); + + +/// +/// Calculates next closest power of two of 32bit integer n +/// ref: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 +/// +int next_power_of_two(int n); + + +// vim: set noet sw=8 ts=8 : diff --git a/src/vsync.c b/src/vsync.c new file mode 100644 index 0000000..5980155 --- /dev/null +++ b/src/vsync.c @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> + +/// Function pointers to init VSync modes. + +#include "common.h" +#include "log.h" + +#ifdef CONFIG_OPENGL +#include "backend/gl/glx.h" +#include "opengl.h" +#endif + +#ifdef CONFIG_VSYNC_DRM +#include <drm.h> +#include <errno.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#endif + +#include "config.h" +#include "vsync.h" + +#ifdef CONFIG_VSYNC_DRM +/** + * Wait for next VSync, DRM method. + * + * Stolen from: + * https://github.com/MythTV/mythtv/blob/master/mythtv/libs/libmythtv/vsync.cpp + */ +static int vsync_drm_wait(session_t *ps) { + int ret = -1; + drm_wait_vblank_t vbl; + + vbl.request.type = _DRM_VBLANK_RELATIVE, vbl.request.sequence = 1; + + do { + ret = ioctl(ps->drm_fd, DRM_IOCTL_WAIT_VBLANK, &vbl); + vbl.request.type &= ~(uint)_DRM_VBLANK_RELATIVE; + } while (ret && errno == EINTR); + + if (ret) + log_error("VBlank ioctl did not work, unimplemented in this drmver?"); + + return ret; +} + +/** + * Initialize DRM VSync. + * + * @return true for success, false otherwise + */ +static bool vsync_drm_init(session_t *ps) { + // Should we always open card0? + if (ps->drm_fd < 0 && (ps->drm_fd = open("/dev/dri/card0", O_RDWR)) < 0) { + log_error("Failed to open device."); + return false; + } + + if (vsync_drm_wait(ps)) + return false; + + return true; +} +#endif + +#ifdef CONFIG_OPENGL +/** + * Initialize OpenGL VSync. + * + * Stolen from: + * http://git.tuxfamily.org/?p=ccm/cairocompmgr.git;a=commitdiff;h=efa4ceb97da501e8630ca7f12c99b1dce853c73e + * Possible original source: http://www.inb.uni-luebeck.de/~boehme/xvideo_sync.html + * + * @return true for success, false otherwise + */ +static bool vsync_opengl_init(session_t *ps) { + if (!ensure_glx_context(ps)) + return false; + + return glxext.has_GLX_SGI_video_sync; +} + +static bool vsync_opengl_oml_init(session_t *ps) { + if (!ensure_glx_context(ps)) + return false; + + return glxext.has_GLX_OML_sync_control; +} + +static inline bool vsync_opengl_swc_swap_interval(session_t *ps, int interval) { + if (glxext.has_GLX_MESA_swap_control) + return glXSwapIntervalMESA((uint)interval) == 0; + else if (glxext.has_GLX_SGI_swap_control) + return glXSwapIntervalSGI(interval) == 0; + else if (glxext.has_GLX_EXT_swap_control) { + GLXDrawable d = glXGetCurrentDrawable(); + if (d == None) { + // We don't have a context?? + return false; + } + glXSwapIntervalEXT(ps->dpy, glXGetCurrentDrawable(), interval); + return true; + } + return false; +} + +static bool vsync_opengl_swc_init(session_t *ps) { + if (!bkend_use_glx(ps)) { + log_error("OpenGL swap control requires the GLX backend."); + return false; + } + + if (!vsync_opengl_swc_swap_interval(ps, 1)) { + log_error("Failed to load a swap control extension."); + return false; + } + + return true; +} + +/** + * Wait for next VSync, OpenGL method. + */ +static int vsync_opengl_wait(session_t *ps attr_unused) { + unsigned vblank_count = 0; + + glXGetVideoSyncSGI(&vblank_count); + glXWaitVideoSyncSGI(2, (vblank_count + 1) % 2, &vblank_count); + return 0; +} + +/** + * Wait for next VSync, OpenGL OML method. + * + * https://mail.gnome.org/archives/clutter-list/2012-November/msg00031.html + */ +static int vsync_opengl_oml_wait(session_t *ps) { + int64_t ust = 0, msc = 0, sbc = 0; + + glXGetSyncValuesOML(ps->dpy, ps->reg_win, &ust, &msc, &sbc); + glXWaitForMscOML(ps->dpy, ps->reg_win, 0, 2, (msc + 1) % 2, &ust, &msc, &sbc); + return 0; +} +#endif + +/** + * Initialize current VSync method. + */ +bool vsync_init(session_t *ps) { +#ifdef CONFIG_OPENGL + if (bkend_use_glx(ps)) { + // Mesa turns on swap control by default, undo that + vsync_opengl_swc_swap_interval(ps, 0); + } +#endif +#ifdef CONFIG_VSYNC_DRM + log_warn("The DRM vsync method is deprecated, please don't enable it."); +#endif + + if (!ps->o.vsync) { + return true; + } + +#ifdef CONFIG_OPENGL + if (bkend_use_glx(ps)) { + if (!vsync_opengl_swc_init(ps)) { + return false; + } + ps->vsync_wait = NULL; // glXSwapBuffers will automatically wait + // for vsync, we don't need to do anything. + return true; + } +#endif + + // Oh no, we are not using glx backend. + // Throwing things at wall. +#ifdef CONFIG_OPENGL + if (vsync_opengl_oml_init(ps)) { + log_info("Using the opengl-oml vsync method"); + ps->vsync_wait = vsync_opengl_oml_wait; + return true; + } + + if (vsync_opengl_init(ps)) { + log_info("Using the opengl vsync method"); + ps->vsync_wait = vsync_opengl_wait; + return true; + } +#endif + +#ifdef CONFIG_VSYNC_DRM + if (vsync_drm_init(ps)) { + log_info("Using the drm vsync method"); + ps->vsync_wait = vsync_drm_wait; + return true; + } +#endif + + log_error("No supported vsync method found for this backend"); + return false; +} diff --git a/src/vsync.h b/src/vsync.h new file mode 100644 index 0000000..076bc26 --- /dev/null +++ b/src/vsync.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui <[email protected]> +#include <stdbool.h> + +typedef struct session session_t; + +bool vsync_init(session_t *ps); diff --git a/src/win.c b/src/win.c new file mode 100644 index 0000000..971bdc9 --- /dev/null +++ b/src/win.c @@ -0,0 +1,3116 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2011-2013, Christopher Jeffrey +// Copyright (c) 2013 Richard Grenville <[email protected]> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <inttypes.h> +#include <math.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <xcb/composite.h> +#include <xcb/damage.h> +#include <xcb/render.h> +#include <xcb/xcb.h> +#include <xcb/xcb_renderutil.h> +#include <xcb/xinerama.h> + +#include "atom.h" +#include "backend/backend.h" +#include "backend/backend_common.h" +#include "c2.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "list.h" +#include "log.h" +#include "picom.h" +#include "region.h" +#include "render.h" +#include "string_utils.h" +#include "types.h" +#include "uthash_extra.h" +#include "utils.h" +#include "x.h" + +#ifdef CONFIG_DBUS +#include "dbus.h" +#endif + +#ifdef CONFIG_OPENGL +// TODO(yshui) Get rid of this include +#include "opengl.h" +#endif + +#include "win.h" + +// TODO(yshui) Make more window states internal +struct managed_win_internal { + struct managed_win base; +}; + +#define OPAQUE (0xffffffff) +static const int WIN_GET_LEADER_MAX_RECURSION = 20; +static const int ROUNDED_PIXELS = 1; +static const double ROUNDED_PERCENT = 0.05; + +/** + * Retrieve the <code>WM_CLASS</code> of a window and update its + * <code>win</code> structure. + */ +static bool win_update_class(session_t *ps, struct managed_win *w); +static int win_update_role(session_t *ps, struct managed_win *w); +static void win_update_wintype(session_t *ps, struct managed_win *w); +static int win_update_name(session_t *ps, struct managed_win *w); +/** + * Reread opacity property of a window. + */ +static void win_update_opacity_prop(session_t *ps, struct managed_win *w); +static void win_update_opacity_target(session_t *ps, struct managed_win *w); +/** + * Retrieve frame extents from a window. + */ +static void +win_update_frame_extents(session_t *ps, struct managed_win *w, xcb_window_t client); +static void win_update_prop_shadow_raw(session_t *ps, struct managed_win *w); +static void win_update_prop_shadow(session_t *ps, struct managed_win *w); +/** + * Update leader of a window. + */ +static void win_update_leader(session_t *ps, struct managed_win *w); + +/// Generate a "no corners" region function, from a function that returns the +/// region via a region_t pointer argument. Corners of the window will be removed from +/// the returned region. +/// Function signature has to be (win *, region_t *) +#define gen_without_corners(fun) \ + void fun##_without_corners(const struct managed_win *w, region_t *res) { \ + fun(w, res); \ + win_region_remove_corners(w, res); \ + } + +/// Generate a "return by value" function, from a function that returns the +/// region via a region_t pointer argument. +/// Function signature has to be (win *) +#define gen_by_val(fun) \ + region_t fun##_by_val(const struct managed_win *w) { \ + region_t ret; \ + pixman_region32_init(&ret); \ + fun(w, &ret); \ + return ret; \ + } + +/** + * Clear leader cache of all windows. + */ +static inline void clear_cache_win_leaders(session_t *ps) { + win_stack_foreach_managed(w, &ps->window_stack) { + w->cache_leader = XCB_NONE; + } +} + +static xcb_window_t win_get_leader_raw(session_t *ps, struct managed_win *w, int recursions); + +/** + * Get the leader of a window. + * + * This function updates w->cache_leader if necessary. + */ +static inline xcb_window_t win_get_leader(session_t *ps, struct managed_win *w) { + return win_get_leader_raw(ps, w, 0); +} + +/** + * Whether the real content of the window is visible. + * + * A window is not considered "real" visible if it's fading out. Because in that case a + * cached version of the window is displayed. + */ +static inline bool attr_pure win_is_real_visible(const struct managed_win *w) { + return w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && + w->state != WSTATE_UNMAPPING; +} + +/** + * Update focused state of a window. + */ +static void win_update_focused(session_t *ps, struct managed_win *w) { + if (UNSET != w->focused_force) { + w->focused = w->focused_force; + } else { + w->focused = win_is_focused_raw(ps, w); + + // Use wintype_focus, and treat WM windows and override-redirected + // windows specially + if (ps->o.wintype_option[w->window_type].focus || + (ps->o.mark_wmwin_focused && w->wmwin) || + (ps->o.mark_ovredir_focused && w->base.id == w->client_win && !w->wmwin) || + (w->a.map_state == XCB_MAP_STATE_VIEWABLE && + c2_match(ps, w, ps->o.focus_blacklist, NULL))) { + w->focused = true; + } + + // If window grouping detection is enabled, mark the window active if + // its group is + if (ps->o.track_leader && ps->active_leader && + win_get_leader(ps, w) == ps->active_leader) { + w->focused = true; + } + } +} + +/** + * Run win_on_factor_change() on all windows with the same leader window. + * + * @param leader leader window ID + */ +static inline void group_on_factor_change(session_t *ps, xcb_window_t leader) { + if (!leader) { + return; + } + + HASH_ITER2(ps->windows, w) { + assert(!w->destroyed); + if (!w->managed) { + continue; + } + auto mw = (struct managed_win *)w; + if (win_get_leader(ps, mw) == leader) { + win_on_factor_change(ps, mw); + } + } +} + +static inline const char *win_get_name_if_managed(const struct win *w) { + if (!w->managed) { + return "(unmanaged)"; + } + auto mw = (struct managed_win *)w; + return mw->name; +} + +/** + * Return whether a window group is really focused. + * + * @param leader leader window ID + * @return true if the window group is focused, false otherwise + */ +static inline bool group_is_focused(session_t *ps, xcb_window_t leader) { + if (!leader) { + return false; + } + + HASH_ITER2(ps->windows, w) { + assert(!w->destroyed); + if (!w->managed) { + continue; + } + auto mw = (struct managed_win *)w; + if (win_get_leader(ps, mw) == leader && win_is_focused_raw(ps, mw)) { + return true; + } + } + + return false; +} + +/** + * Get a rectangular region a window occupies, excluding shadow. + */ +static void win_get_region_local(const struct managed_win *w, region_t *res) { + assert(w->widthb >= 0 && w->heightb >= 0); + pixman_region32_fini(res); + pixman_region32_init_rect(res, 0, 0, (uint)w->widthb, (uint)w->heightb); +} + +/** + * Get a rectangular region a window occupies, excluding frame and shadow. + */ +void win_get_region_noframe_local(const struct managed_win *w, region_t *res) { + const margin_t extents = win_calc_frame_extents(w); + + int x = extents.left; + int y = extents.top; + int width = max2(w->widthb - (extents.left + extents.right), 0); + int height = max2(w->heightb - (extents.top + extents.bottom), 0); + + pixman_region32_fini(res); + if (width > 0 && height > 0) { + pixman_region32_init_rect(res, x, y, (uint)width, (uint)height); + } else { + pixman_region32_init(res); + } +} + +gen_without_corners(win_get_region_noframe_local); + +void win_get_region_frame_local(const struct managed_win *w, region_t *res) { + const margin_t extents = win_calc_frame_extents(w); + auto outer_width = w->widthb; + auto outer_height = w->heightb; + + pixman_region32_fini(res); + pixman_region32_init_rects( + res, + (rect_t[]){ + // top + {.x1 = 0, .y1 = 0, .x2 = outer_width, .y2 = extents.top}, + // bottom + {.x1 = 0, .y1 = outer_height - extents.bottom, .x2 = outer_width, .y2 = outer_height}, + // left + {.x1 = 0, .y1 = 0, .x2 = extents.left, .y2 = outer_height}, + // right + {.x1 = outer_width - extents.right, .y1 = 0, .x2 = outer_width, .y2 = outer_height}, + }, + 4); + + // limit the frame region to inside the window + region_t reg_win; + pixman_region32_init_rects(®_win, (rect_t[]){{0, 0, outer_width, outer_height}}, 1); + pixman_region32_intersect(res, ®_win, res); + pixman_region32_fini(®_win); +} + +gen_by_val(win_get_region_frame_local); + +/** + * Add a window to damaged area. + * + * @param ps current session + * @param w struct _win element representing the window + */ +void add_damage_from_win(session_t *ps, const struct managed_win *w) { + // XXX there was a cached extents region, investigate + // if that's better + + // TODO(yshui) use the bounding shape when the window is shaped, otherwise the + // damage would be excessive + region_t extents; + pixman_region32_init(&extents); + win_extents(w, &extents); + add_damage(ps, &extents); + pixman_region32_fini(&extents); +} + +/// Release the images attached to this window +static inline void win_release_pixmap(backend_t *base, struct managed_win *w) { + log_debug("Releasing pixmap of window %#010x (%s)", w->base.id, w->name); + assert(w->win_image); + if (w->win_image) { + base->ops->release_image(base, w->win_image); + w->win_image = NULL; + // Bypassing win_set_flags, because `w` might have been destroyed + w->flags |= WIN_FLAGS_PIXMAP_NONE; + } +} +static inline void win_release_oldpixmap(backend_t *base, struct managed_win *w) { + log_debug("Releasing old_pixmap of window %#010x (%s)", w->base.id, w->name); + if (w->old_win_image) { + base->ops->release_image(base, w->old_win_image); + w->old_win_image = NULL; + } +} +static inline void win_release_shadow(backend_t *base, struct managed_win *w) { + log_debug("Releasing shadow of window %#010x (%s)", w->base.id, w->name); + assert(w->shadow_image); + if (w->shadow_image) { + base->ops->release_image(base, w->shadow_image); + w->shadow_image = NULL; + // Bypassing win_set_flags, because `w` might have been destroyed + w->flags |= WIN_FLAGS_SHADOW_NONE; + } +} + +static inline bool win_bind_pixmap(struct backend_base *b, struct managed_win *w) { + assert(!w->win_image); + auto pixmap = x_new_id(b->c); + auto e = xcb_request_check( + b->c, xcb_composite_name_window_pixmap_checked(b->c, w->base.id, pixmap)); + if (e) { + log_error("Failed to get named pixmap for window %#010x(%s)", w->base.id, + w->name); + free(e); + return false; + } + log_debug("New named pixmap for %#010x (%s) : %#010x", w->base.id, w->name, pixmap); + w->win_image = + b->ops->bind_pixmap(b, pixmap, x_get_visual_info(b->c, w->a.visual), true); + if (!w->win_image) { + log_error("Failed to bind pixmap"); + win_set_flags(w, WIN_FLAGS_IMAGE_ERROR); + return false; + } + + win_clear_flags(w, WIN_FLAGS_PIXMAP_NONE); + return true; +} + +bool win_bind_shadow(struct backend_base *b, struct managed_win *w, struct color c, + struct conv *kernel) { + assert(!w->shadow_image); + assert(w->shadow); + w->shadow_image = b->ops->render_shadow(b, w->widthb, w->heightb, kernel, c.red, + c.green, c.blue, c.alpha); + if (!w->shadow_image) { + log_error("Failed to bind shadow image, shadow will be disabled for " + "%#010x (%s)", + w->base.id, w->name); + win_set_flags(w, WIN_FLAGS_SHADOW_NONE); + w->shadow = false; + return false; + } + + log_debug("New shadow for %#010x (%s)", w->base.id, w->name); + win_clear_flags(w, WIN_FLAGS_SHADOW_NONE); + return true; +} + +void win_release_images(struct backend_base *backend, struct managed_win *w) { + // We don't want to decide what we should do if the image we want to release is + // stale (do we clear the stale flags or not?) + // But if we are not releasing any images anyway, we don't care about the stale + // flags. + + if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { + assert(!win_check_flags_all(w, WIN_FLAGS_PIXMAP_STALE)); + win_release_pixmap(backend, w); + win_release_oldpixmap(backend, w); + } + + if (!win_check_flags_all(w, WIN_FLAGS_SHADOW_NONE)) { + assert(!win_check_flags_all(w, WIN_FLAGS_SHADOW_STALE)); + win_release_shadow(backend, w); + } +} + +/// Returns true if the `prop` property is stale, as well as clears the stale flag. +static bool win_fetch_and_unset_property_stale(struct managed_win *w, xcb_atom_t prop); +/// Returns true if any of the properties are stale, as well as clear all the stale flags. +static void win_clear_all_properties_stale(struct managed_win *w); + +/// Fetch new window properties from the X server, and run appropriate updates. Might set +/// WIN_FLAGS_FACTOR_CHANGED +static void win_update_properties(session_t *ps, struct managed_win *w) { + if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_WINDOW_TYPE)) { + win_update_wintype(ps, w); + } + + if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_WINDOW_OPACITY)) { + win_update_opacity_prop(ps, w); + // we cannot receive OPACITY change when window has been destroyed + assert(w->state != WSTATE_DESTROYING); + win_update_opacity_target(ps, w); + } + + if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_FRAME_EXTENTS)) { + win_update_frame_extents(ps, w, w->client_win); + add_damage_from_win(ps, w); + } + + if (win_fetch_and_unset_property_stale(w, ps->atoms->aWM_NAME) || + win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_NAME)) { + if (win_update_name(ps, w) == 1) { + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } + } + + if (win_fetch_and_unset_property_stale(w, ps->atoms->aWM_CLASS)) { + if (win_update_class(ps, w)) { + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } + } + + if (win_fetch_and_unset_property_stale(w, ps->atoms->aWM_WINDOW_ROLE)) { + if (win_update_role(ps, w) == 1) { + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } + } + + if (win_fetch_and_unset_property_stale(w, ps->atoms->a_COMPTON_SHADOW)) { + win_update_prop_shadow(ps, w); + } + + if (win_fetch_and_unset_property_stale(w, ps->atoms->aWM_CLIENT_LEADER) || + win_fetch_and_unset_property_stale(w, ps->atoms->aWM_TRANSIENT_FOR)) { + win_update_leader(ps, w); + } + + win_clear_all_properties_stale(w); +} + +static void init_animation(session_t *ps, struct managed_win *w) { + enum open_window_animation animation = ps->o.animation_for_open_window; + + w->animation_transient = wid_has_prop(ps, w->client_win, ps->atoms->aWM_TRANSIENT_FOR); + + if (w->window_type != WINTYPE_TOOLTIP && w->animation_transient) + animation = ps->o.animation_for_transient_window; + + if (ps->o.wintype_option[w->window_type].animation < OPEN_WINDOW_ANIMATION_INVALID) + animation = ps->o.wintype_option[w->window_type].animation; + + if (ps->root_desktop_switch_direction != 0) { + if (ps->o.animation_for_workspace_switch_in == OPEN_WINDOW_ANIMATION_AUTO) + animation = OPEN_WINDOW_ANIMATION_SLIDE_IN; + else + animation = ps->o.animation_for_workspace_switch_in; + } + + switch (animation) { + case OPEN_WINDOW_ANIMATION_AUTO: + case OPEN_WINDOW_ANIMATION_NONE: { // No animation + w->animation_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_w = w->pending_g.width; + w->animation_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_FLYIN: { // Fly-in from a random point outside the screen + // Compute random point off screen + double angle = 2 * M_PI * ((double)rand() / RAND_MAX); + const double radius = + sqrt(ps->root_width * ps->root_width + ps->root_height * ps->root_height); + + // Set animation + w->animation_center_x = ps->root_width * 0.5 + radius * cos(angle); + w->animation_center_y = ps->root_height * 0.5 + radius * sin(angle); + w->animation_w = 0; + w->animation_h = 0; + break; + } + case OPEN_WINDOW_ANIMATION_ZOOM: { // Zoom-in the image, without changing its location + w->animation_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_w = 0; + w->animation_h = 0; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_UP: { // Slide up the image, without changing its location + w->animation_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_center_y = w->pending_g.y + w->pending_g.height; + w->animation_w = w->pending_g.width; + w->animation_h = 0; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_DOWN: { // Slide down the image, without changing its location + w->animation_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_center_y = w->pending_g.y; + w->animation_w = w->pending_g.width; + w->animation_h = 0; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_LEFT: { // Slide left the image, without changing its location + w->animation_center_x = w->pending_g.x + w->pending_g.width; + w->animation_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_w = 0; + w->animation_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_RIGHT: { // Slide right the image, without changing its location + w->animation_center_x = w->pending_g.x; + w->animation_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_w = 0; + w->animation_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_IN: { + w->animation_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_center_y = w->pending_g.y + w->pending_g.height * 0.5 - + ps->root_height * + ((ps->root_desktop_switch_direction < 0 && + ps->root_desktop_switch_direction >= -1) || + ps->root_desktop_switch_direction > 1?1:-1); + w->animation_w = w->pending_g.width; + w->animation_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_OUT: { + w->animation_dest_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_dest_center_y = w->pending_g.y + w->pending_g.height * 0.5 - + ps->root_height * + ((ps->root_desktop_switch_direction < 0 && + ps->root_desktop_switch_direction >= -1) || + ps->root_desktop_switch_direction > 1?-1:1); + w->animation_dest_w = w->pending_g.width; + w->animation_dest_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_INVALID: assert(false); break; + } +} + +static void init_animation_unmap(session_t *ps, struct managed_win *w) { + enum open_window_animation animation; + + if (ps->o.animation_for_unmap_window == OPEN_WINDOW_ANIMATION_AUTO) { + animation = ps->o.animation_for_open_window; + + if (w->window_type != WINTYPE_TOOLTIP && w->animation_transient) + animation = ps->o.animation_for_transient_window; + + if (ps->o.wintype_option[w->window_type].animation < OPEN_WINDOW_ANIMATION_INVALID) + animation = ps->o.wintype_option[w->window_type].animation; + + if (animation == OPEN_WINDOW_ANIMATION_SLIDE_UP) + animation = OPEN_WINDOW_ANIMATION_SLIDE_DOWN; + else if (animation == OPEN_WINDOW_ANIMATION_SLIDE_DOWN) + animation = OPEN_WINDOW_ANIMATION_SLIDE_UP; + else if (animation == OPEN_WINDOW_ANIMATION_SLIDE_LEFT) + animation = OPEN_WINDOW_ANIMATION_SLIDE_RIGHT; + else if (animation == OPEN_WINDOW_ANIMATION_SLIDE_RIGHT) + animation = OPEN_WINDOW_ANIMATION_SLIDE_LEFT; + else if (animation == OPEN_WINDOW_ANIMATION_SLIDE_IN) + animation = OPEN_WINDOW_ANIMATION_SLIDE_OUT; + else if (animation == OPEN_WINDOW_ANIMATION_SLIDE_OUT) + animation = OPEN_WINDOW_ANIMATION_SLIDE_IN; + + } else { + animation = ps->o.animation_for_unmap_window; + + if (ps->o.wintype_option[w->window_type].animation_unmap < OPEN_WINDOW_ANIMATION_INVALID) + animation = ps->o.wintype_option[w->window_type].animation_unmap; + } + + if (ps->root_desktop_switch_direction != 0) { + if (ps->o.animation_for_workspace_switch_out == OPEN_WINDOW_ANIMATION_AUTO) + animation = OPEN_WINDOW_ANIMATION_SLIDE_OUT; + else + animation = ps->o.animation_for_workspace_switch_out; + } + + switch (animation) { + case OPEN_WINDOW_ANIMATION_AUTO: + case OPEN_WINDOW_ANIMATION_NONE: { // No animation + w->animation_dest_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_dest_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_dest_w = w->pending_g.width; + w->animation_dest_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_FLYIN: { // Fly-out from a random point outside the screen + // Compute random point off screen + double angle = 2 * M_PI * ((double)rand() / RAND_MAX); + const double radius = + sqrt(ps->root_width * ps->root_width + ps->root_height * ps->root_height); + + // Set animation + w->animation_dest_center_x = ps->root_width * 0.5 + radius * cos(angle); + w->animation_dest_center_y = ps->root_height * 0.5 + radius * sin(angle); + w->animation_dest_w = 0; + w->animation_dest_h = 0; + break; + } + case OPEN_WINDOW_ANIMATION_ZOOM: { // Zoom-out the image, without changing its location + w->animation_dest_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_dest_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_dest_w = 0; + w->animation_dest_h = 0; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_UP: { // Slide up the image, without changing its location + w->animation_dest_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_dest_center_y = w->pending_g.y; + w->animation_dest_w = w->pending_g.width; + w->animation_dest_h = 0; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_DOWN: { // Slide down the image, without changing its location + w->animation_dest_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_dest_center_y = w->pending_g.y + w->pending_g.height; + w->animation_dest_w = w->pending_g.width; + w->animation_dest_h = 0; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_LEFT: { // Slide left the image, without changing its location + w->animation_dest_center_x = w->pending_g.x; + w->animation_dest_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_dest_w = 0; + w->animation_dest_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_RIGHT: { // Slide right the image, without changing its location + w->animation_dest_center_x = w->pending_g.x + w->pending_g.width; + w->animation_dest_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_dest_w = 0; + w->animation_dest_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_IN: { + w->animation_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_center_y = w->pending_g.y + w->pending_g.height * 0.5 - + ps->root_height * + ((ps->root_desktop_switch_direction < 0 && + ps->root_desktop_switch_direction >= -1) || + ps->root_desktop_switch_direction > 1?1:-1); + w->animation_w = w->pending_g.width; + w->animation_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_SLIDE_OUT: { + w->animation_dest_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_dest_center_y = w->pending_g.y + w->pending_g.height * 0.5 - + ps->root_height * + ((ps->root_desktop_switch_direction < 0 && + ps->root_desktop_switch_direction >= -1) || + ps->root_desktop_switch_direction > 1?-1:1); + w->animation_dest_w = w->pending_g.width; + w->animation_dest_h = w->pending_g.height; + break; + } + case OPEN_WINDOW_ANIMATION_INVALID: assert(false); break; + } +} + +/// Handle non-image flags. This phase might set IMAGES_STALE flags +void win_process_update_flags(session_t *ps, struct managed_win *w) { + // Whether the window was visible before we process the mapped flag. i.e. is the + // window just mapped. + bool was_visible = win_is_real_visible(w); + log_trace("Processing flags for window %#010x (%s), was visible: %d", w->base.id, + w->name, was_visible); + + if (win_check_flags_all(w, WIN_FLAGS_MAPPED)) { + map_win_start(ps, w); + win_clear_flags(w, WIN_FLAGS_MAPPED); + } + + if (!win_is_real_visible(w)) { + // Flags of invisible windows are processed when they are mapped + return; + } + + // Check client first, because later property updates need accurate client window + // information + if (win_check_flags_all(w, WIN_FLAGS_CLIENT_STALE)) { + win_recheck_client(ps, w); + win_clear_flags(w, WIN_FLAGS_CLIENT_STALE); + } + + bool damaged = false; + if (win_check_flags_any(w, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { + if (was_visible) { + // Mark the old extents of this window as damaged. The new extents + // will be marked damaged below, after the window extents are + // updated. + // + // If the window is just mapped, we don't need to mark the old + // extent as damaged. (It's possible that the window was in fading + // and is interrupted by being mapped. In that case, the fading + // window will be added to damage by map_win_start, so we don't + // need to do it here) + add_damage_from_win(ps, w); + } + + // Ignore animations all together if set to none on window type basis + if (ps->o.wintype_option[w->window_type].animation == 0) { + w->g = w->pending_g; + + // Update window geometry + } else if (ps->o.animations) { + if (!was_visible) { + // Set window-open animation + init_animation(ps, w); + + w->animation_dest_center_x = w->pending_g.x + w->pending_g.width * 0.5; + w->animation_dest_center_y = w->pending_g.y + w->pending_g.height * 0.5; + w->animation_dest_w = w->pending_g.width; + w->animation_dest_h = w->pending_g.height; + + w->g.x = (int16_t)round(w->animation_center_x - + w->animation_w * 0.5); + w->g.y = (int16_t)round(w->animation_center_y - + w->animation_h * 0.5); + w->g.width = (uint16_t)round(w->animation_w); + w->g.height = (uint16_t)round(w->animation_h); + + } else { + w->animation_dest_center_x = + w->pending_g.x + w->pending_g.width * 0.5; + w->animation_dest_center_y = + w->pending_g.y + w->pending_g.height * 0.5; + w->animation_dest_w = w->pending_g.width; + w->animation_dest_h = w->pending_g.height; + } + + w->g.border_width = w->pending_g.border_width; + + double x_dist = w->animation_dest_center_x - w->animation_center_x; + double y_dist = w->animation_dest_center_y - w->animation_center_y; + double w_dist = w->animation_dest_w - w->animation_w; + double h_dist = w->animation_dest_h - w->animation_h; + w->animation_inv_og_distance = + 1.0 / sqrt(x_dist * x_dist + y_dist * y_dist + + w_dist * w_dist + h_dist * h_dist); + + if (isinf(w->animation_inv_og_distance)) + w->animation_inv_og_distance = 0; + + // We only grab images if w->reg_ignore_valid is true as + // there's an ev_shape_notify() event fired quickly on new windows + // for e.g. in case of Firefox main menu and ev_shape_notify() + // sets the win_set_flags(w, WIN_FLAGS_SIZE_STALE); which + // brakes the new image captured and because this same event + // also sets w->reg_ignore_valid = false; too we check for it + if (w->reg_ignore_valid) { + if (w->old_win_image) { + ps->backend_data->ops->release_image(ps->backend_data, + w->old_win_image); + w->old_win_image = NULL; + } + + // We only grab + if (w->win_image) { + w->old_win_image = ps->backend_data->ops->clone_image( + ps->backend_data, w->win_image, &w->bounding_shape); + } + } + + w->animation_progress = 0.0; + + } else { + w->g = w->pending_g; + } + + if (win_check_flags_all(w, WIN_FLAGS_SIZE_STALE)) { + win_on_win_size_change(ps, w); + win_update_bounding_shape(ps, w); + damaged = true; + win_clear_flags(w, WIN_FLAGS_SIZE_STALE); + } + + if (win_check_flags_all(w, WIN_FLAGS_POSITION_STALE)) { + damaged = true; + win_clear_flags(w, WIN_FLAGS_POSITION_STALE); + } + + win_update_screen(ps->xinerama_nscrs, ps->xinerama_scr_regs, w); + } + + if (win_check_flags_all(w, WIN_FLAGS_PROPERTY_STALE)) { + win_update_properties(ps, w); + win_clear_flags(w, WIN_FLAGS_PROPERTY_STALE); + } + + // Factor change flags could be set by previous stages, so must be handled last + if (win_check_flags_all(w, WIN_FLAGS_FACTOR_CHANGED)) { + win_on_factor_change(ps, w); + win_clear_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } + + // Add damage, has to be done last so the window has the latest geometry + // information. + if (damaged) { + add_damage_from_win(ps, w); + } +} + +void win_process_image_flags(session_t *ps, struct managed_win *w) { + assert(!win_check_flags_all(w, WIN_FLAGS_MAPPED)); + + if (w->state == WSTATE_UNMAPPED || w->state == WSTATE_DESTROYING || + w->state == WSTATE_UNMAPPING) { + // Flags of invisible windows are processed when they are mapped + return; + } + + // Not a loop + while (win_check_flags_any(w, WIN_FLAGS_IMAGES_STALE) && + !win_check_flags_all(w, WIN_FLAGS_IMAGE_ERROR)) { + // Image needs to be updated, update it. + if (!ps->backend_data) { + // We are using legacy backend, nothing to do here. + break; + } + + if (win_check_flags_all(w, WIN_FLAGS_PIXMAP_STALE)) { + // Check to make sure the window is still mapped, otherwise we + // won't be able to rebind pixmap after releasing it, yet we might + // still need the pixmap for rendering. + assert(w->state != WSTATE_UNMAPPING && w->state != WSTATE_DESTROYING); + if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { + // Must release images first, otherwise breaks + // NVIDIA driver + win_release_pixmap(ps->backend_data, w); + } + win_bind_pixmap(ps->backend_data, w); + } + + if (win_check_flags_all(w, WIN_FLAGS_SHADOW_STALE)) { + if (!win_check_flags_all(w, WIN_FLAGS_SHADOW_NONE)) { + win_release_shadow(ps->backend_data, w); + } + if (w->shadow) { + win_bind_shadow(ps->backend_data, w, + (struct color){.red = ps->o.shadow_red, + .green = ps->o.shadow_green, + .blue = ps->o.shadow_blue, + .alpha = ps->o.shadow_opacity}, + ps->gaussian_map); + } + } + + // break here, loop always run only once + break; + } + + // Clear stale image flags + if (win_check_flags_any(w, WIN_FLAGS_IMAGES_STALE)) { + win_clear_flags(w, WIN_FLAGS_IMAGES_STALE); + } +} + +/** + * Check if a window has rounded corners. + * XXX This is really dumb + */ +static bool attr_pure win_has_rounded_corners(const struct managed_win *w) { + if (!w->bounding_shaped) { + return false; + } + + // Quit if border_size() returns XCB_NONE + if (!pixman_region32_not_empty((region_t *)&w->bounding_shape)) { + return false; + } + + // Determine the minimum width/height of a rectangle that could mark + // a window as having rounded corners + auto minwidth = + (uint16_t)max2(w->widthb * (1 - ROUNDED_PERCENT), w->widthb - ROUNDED_PIXELS); + auto minheight = + (uint16_t)max2(w->heightb * (1 - ROUNDED_PERCENT), w->heightb - ROUNDED_PIXELS); + + // Get the rectangles in the bounding region + int nrects = 0; + const rect_t *rects = + pixman_region32_rectangles((region_t *)&w->bounding_shape, &nrects); + + // Look for a rectangle large enough for this window be considered + // having rounded corners + for (int i = 0; i < nrects; ++i) { + if (rects[i].x2 - rects[i].x1 >= minwidth && + rects[i].y2 - rects[i].y1 >= minheight) { + return true; + } + } + return false; +} + +int win_update_name(session_t *ps, struct managed_win *w) { + char **strlst = NULL; + int nstr = 0; + + if (!w->client_win) { + return 0; + } + + if (!(wid_get_text_prop(ps, w->client_win, ps->atoms->a_NET_WM_NAME, &strlst, &nstr))) { + log_debug("(%#010x): _NET_WM_NAME unset, falling back to WM_NAME.", + w->client_win); + + if (!wid_get_text_prop(ps, w->client_win, ps->atoms->aWM_NAME, &strlst, &nstr)) { + log_debug("Unsetting window name for %#010x", w->client_win); + free(w->name); + w->name = NULL; + return -1; + } + } + + int ret = 0; + if (!w->name || strcmp(w->name, strlst[0]) != 0) { + ret = 1; + free(w->name); + w->name = strdup(strlst[0]); + } + + free(strlst); + + log_debug("(%#010x): client = %#010x, name = \"%s\", " + "ret = %d", + w->base.id, w->client_win, w->name, ret); + return ret; +} + +static int win_update_role(session_t *ps, struct managed_win *w) { + char **strlst = NULL; + int nstr = 0; + + if (!wid_get_text_prop(ps, w->client_win, ps->atoms->aWM_WINDOW_ROLE, &strlst, &nstr)) { + return -1; + } + + int ret = 0; + if (!w->role || strcmp(w->role, strlst[0]) != 0) { + ret = 1; + free(w->role); + w->role = strdup(strlst[0]); + } + + free(strlst); + + log_trace("(%#010x): client = %#010x, role = \"%s\", " + "ret = %d", + w->base.id, w->client_win, w->role, ret); + return ret; +} + +/** + * Check if a window is bounding-shaped. + */ +static inline bool win_bounding_shaped(const session_t *ps, xcb_window_t wid) { + if (ps->shape_exists) { + xcb_shape_query_extents_reply_t *reply; + Bool bounding_shaped; + + reply = xcb_shape_query_extents_reply( + ps->c, xcb_shape_query_extents(ps->c, wid), NULL); + bounding_shaped = reply && reply->bounding_shaped; + free(reply); + + return bounding_shaped; + } + + return false; +} + +static wintype_t wid_get_prop_wintype(session_t *ps, xcb_window_t wid) { + winprop_t prop = + x_get_prop(ps->c, wid, ps->atoms->a_NET_WM_WINDOW_TYPE, 32L, XCB_ATOM_ATOM, 32); + + for (unsigned i = 0; i < prop.nitems; ++i) { + for (wintype_t j = 1; j < NUM_WINTYPES; ++j) { + if (ps->atoms_wintypes[j] == (xcb_atom_t)prop.p32[i]) { + free_winprop(&prop); + return j; + } + } + } + + free_winprop(&prop); + + return WINTYPE_UNKNOWN; +} + +static bool +wid_get_opacity_prop(session_t *ps, xcb_window_t wid, opacity_t def, opacity_t *out) { + bool ret = false; + *out = def; + + winprop_t prop = x_get_prop(ps->c, wid, ps->atoms->a_NET_WM_WINDOW_OPACITY, 1L, + XCB_ATOM_CARDINAL, 32); + + if (prop.nitems) { + *out = *prop.c32; + ret = true; + } + + free_winprop(&prop); + + return ret; +} + +// XXX should distinguish between frame has alpha and window body has alpha +bool win_has_alpha(const struct managed_win *w) { + return w->pictfmt && w->pictfmt->type == XCB_RENDER_PICT_TYPE_DIRECT && + w->pictfmt->direct.alpha_mask; +} + +bool win_client_has_alpha(const struct managed_win *w) { + return w->client_pictfmt && w->client_pictfmt->type == XCB_RENDER_PICT_TYPE_DIRECT && + w->client_pictfmt->direct.alpha_mask; +} + +winmode_t win_calc_mode(const struct managed_win *w) { + if (w->opacity < 1.0) { + return WMODE_TRANS; + } + + if (win_has_alpha(w)) { + if (w->client_win == XCB_NONE) { + // This is a window not managed by the WM, and it has alpha, + // so it's transparent. No need to check WM frame. + return WMODE_TRANS; + } + // The WM window has alpha + if (win_client_has_alpha(w)) { + // The client window also has alpha, the entire window is + // transparent + return WMODE_TRANS; + } + if (win_has_frame(w)) { + // The client window doesn't have alpha, but we have a WM frame + // window, which has alpha. + return WMODE_FRAME_TRANS; + } + // Although the WM window has alpha, the frame window has 0 size, so + // consider the window solid + } + + if (w->frame_opacity != 1.0 && win_has_frame(w)) { + return WMODE_FRAME_TRANS; + } + + // log_trace("Window %#010x(%s) is solid", w->client_win, w->name); + return WMODE_SOLID; +} + +/** + * Calculate and return the opacity target of a window. + * + * The priority of opacity settings are: + * + * inactive_opacity_override (if set, and unfocused) > _NET_WM_WINDOW_OPACITY (if set) > + * opacity-rules (if matched) > window type default opacity > active/inactive opacity + * + * @param ps current session + * @param w struct _win object representing the window + * + * @return target opacity + */ +double win_calc_opacity_target(session_t *ps, const struct managed_win *w) { + double opacity = 1; + + if (w->state == WSTATE_UNMAPPED) { + // be consistent + return 0; + } + if (w->state == WSTATE_UNMAPPING || w->state == WSTATE_DESTROYING) { + if (ps->root_desktop_switch_direction) + return w->opacity; + + return 0; + } + // Try obeying opacity property and window type opacity firstly + if (w->has_opacity_prop) { + opacity = ((double)w->opacity_prop) / OPAQUE; + } else if (w->opacity_is_set) { + opacity = w->opacity_set; + } else if (!safe_isnan(ps->o.wintype_option[w->window_type].opacity)) { + opacity = ps->o.wintype_option[w->window_type].opacity; + } else { + // Respect active_opacity only when the window is physically focused + if (win_is_focused_raw(ps, w)) + opacity = ps->o.active_opacity; + else if (!w->focused) + // Respect inactive_opacity in some cases + opacity = ps->o.inactive_opacity; + } + + // respect inactive override + if (ps->o.inactive_opacity_override && !w->focused) { + opacity = ps->o.inactive_opacity; + } + + return opacity; +} + +/** + * Determine whether a window is to be dimmed. + */ +bool win_should_dim(session_t *ps, const struct managed_win *w) { + // Make sure we do nothing if the window is unmapped / being destroyed + if (w->state == WSTATE_UNMAPPED) { + return false; + } + + if (ps->o.inactive_dim > 0 && !(w->focused)) { + return true; + } else { + return false; + } +} + +/** + * Determine if a window should fade on opacity change. + */ +bool win_should_fade(session_t *ps, const struct managed_win *w) { + // To prevent it from being overwritten by last-paint value if the window is + if (w->fade_force != UNSET) { + return w->fade_force; + } + if (ps->o.no_fading_openclose && w->in_openclose) { + return false; + } + if (ps->o.no_fading_destroyed_argb && w->state == WSTATE_DESTROYING && + win_has_alpha(w) && w->client_win && w->client_win != w->base.id) { + // deprecated + return false; + } + if (w->fade_excluded) { + return false; + } + return ps->o.wintype_option[w->window_type].fade; +} + +/** + * Reread _COMPTON_SHADOW property from a window. + * + * The property must be set on the outermost window, usually the WM frame. + */ +void win_update_prop_shadow_raw(session_t *ps, struct managed_win *w) { + winprop_t prop = x_get_prop(ps->c, w->base.id, ps->atoms->a_COMPTON_SHADOW, 1, + XCB_ATOM_CARDINAL, 32); + + if (!prop.nitems) { + w->prop_shadow = -1; + } else { + w->prop_shadow = *prop.c32; + } + + free_winprop(&prop); +} + +static void win_set_shadow(session_t *ps, struct managed_win *w, bool shadow_new) { + if (w->shadow == shadow_new) { + return; + } + + log_debug("Updating shadow property of window %#010x (%s) to %d", w->base.id, + w->name, shadow_new); + + // We don't handle property updates of non-visible windows until they are mapped. + assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && + w->state != WSTATE_UNMAPPING); + + // Keep a copy of window extent before the shadow change. Will be used for + // calculation of damaged region + region_t extents; + pixman_region32_init(&extents); + win_extents(w, &extents); + + // Apply the shadow change + w->shadow = shadow_new; + + if (ps->redirected) { + // Add damage for shadow change + + // Window extents need update on shadow state change + // Shadow geometry currently doesn't change on shadow state change + // calc_shadow_geometry(ps, w); + + // Note: because the release and creation of the shadow images are + // delayed. When multiple shadow changes happen in a row, without + // rendering phase between them, there could be a stale shadow image + // attached to the window even if w->shadow was previously false. And vice + // versa. So we check the STALE flag before asserting the existence of the + // shadow image. + if (w->shadow) { + // Mark the new extents as damaged if the shadow is added + assert(!w->shadow_image || + win_check_flags_all(w, WIN_FLAGS_SHADOW_STALE) || + !ps->o.experimental_backends); + pixman_region32_clear(&extents); + win_extents(w, &extents); + add_damage_from_win(ps, w); + } else { + // Mark the old extents as damaged if the shadow is removed + assert(w->shadow_image || + win_check_flags_all(w, WIN_FLAGS_SHADOW_STALE) || + !ps->o.experimental_backends); + add_damage(ps, &extents); + } + + // Delayed update of shadow image + // By setting WIN_FLAGS_SHADOW_STALE, we ask win_process_flags to + // re-create or release the shaodw in based on whether w->shadow is set. + win_set_flags(w, WIN_FLAGS_SHADOW_STALE); + + // Only set pending_updates if we are redirected. Otherwise change of a + // shadow won't have influence on whether we should redirect. + ps->pending_updates = true; + } + + pixman_region32_fini(&extents); +} + +/** + * Determine if a window should have shadow, and update things depending + * on shadow state. + */ +static void win_determine_shadow(session_t *ps, struct managed_win *w) { + log_debug("Determining shadow of window %#010x (%s)", w->base.id, w->name); + bool shadow_new = w->shadow; + + if (w->shadow_force != UNSET) { + shadow_new = w->shadow_force; + } else if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { + shadow_new = true; + if (!ps->o.wintype_option[w->window_type].shadow) { + log_debug("Shadow disabled by wintypes"); + shadow_new = false; + } else if (c2_match(ps, w, ps->o.shadow_blacklist, NULL)) { + log_debug("Shadow disabled by shadow-exclude"); + shadow_new = false; + } else if (ps->o.shadow_ignore_shaped && w->bounding_shaped && + !w->rounded_corners) { + log_debug("Shadow disabled by shadow-ignore-shaped"); + shadow_new = false; + } else if (w->prop_shadow == 0) { + log_debug("Shadow disabled by shadow property"); + shadow_new = false; + } + } + + win_set_shadow(ps, w, shadow_new); +} + +/** + * Reread _COMPTON_SHADOW property from a window and update related + * things. + */ +void win_update_prop_shadow(session_t *ps, struct managed_win *w) { + long attr_shadow_old = w->prop_shadow; + + win_update_prop_shadow_raw(ps, w); + + if (w->prop_shadow != attr_shadow_old) { + win_determine_shadow(ps, w); + } +} + +static void win_determine_clip_shadow_above(session_t *ps, struct managed_win *w) { + bool should_crop = (ps->o.wintype_option[w->window_type].clip_shadow_above || + c2_match(ps, w, ps->o.shadow_clip_list, NULL)); + w->clip_shadow_above = should_crop; +} + +static void win_set_invert_color(session_t *ps, struct managed_win *w, bool invert_color_new) { + if (w->invert_color == invert_color_new) { + return; + } + + w->invert_color = invert_color_new; + + add_damage_from_win(ps, w); +} + +/** + * Determine if a window should have color inverted. + */ +static void win_determine_invert_color(session_t *ps, struct managed_win *w) { + bool invert_color_new = w->invert_color; + + if (UNSET != w->invert_color_force) { + invert_color_new = w->invert_color_force; + } else if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { + invert_color_new = c2_match(ps, w, ps->o.invert_color_list, NULL); + } + + win_set_invert_color(ps, w, invert_color_new); +} + +/** + * Set w->invert_color_force of a window. + */ +void win_set_invert_color_force(session_t *ps, struct managed_win *w, switch_t val) { + if (val != w->invert_color_force) { + w->invert_color_force = val; + win_determine_invert_color(ps, w); + queue_redraw(ps); + } +} + +/** + * Set w->fade_force of a window. + * + * Doesn't affect fading already in progress + */ +void win_set_fade_force(struct managed_win *w, switch_t val) { + w->fade_force = val; +} + +/** + * Set w->focused_force of a window. + */ +void win_set_focused_force(session_t *ps, struct managed_win *w, switch_t val) { + if (val != w->focused_force) { + w->focused_force = val; + win_on_factor_change(ps, w); + queue_redraw(ps); + } +} + +/** + * Set w->shadow_force of a window. + */ +void win_set_shadow_force(session_t *ps, struct managed_win *w, switch_t val) { + if (val != w->shadow_force) { + w->shadow_force = val; + win_determine_shadow(ps, w); + queue_redraw(ps); + } +} + +static void +win_set_blur_background(session_t *ps, struct managed_win *w, bool blur_background_new) { + if (w->blur_background == blur_background_new) + return; + + w->blur_background = blur_background_new; + + // This damage might not be absolutely necessary (e.g. when the window is opaque), + // but blur_background changes should be rare, so this should be fine. + add_damage_from_win(ps, w); +} + +/** + * Determine if a window should have background blurred. + */ +static void win_determine_blur_background(session_t *ps, struct managed_win *w) { + log_debug("Determining blur-background of window %#010x (%s)", w->base.id, w->name); + if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { + return; + } + + bool blur_background_new = ps->o.blur_method != BLUR_METHOD_NONE; + if (blur_background_new) { + if (!ps->o.wintype_option[w->window_type].blur_background) { + log_debug("Blur background disabled by wintypes"); + blur_background_new = false; + } else if (c2_match(ps, w, ps->o.blur_background_blacklist, NULL)) { + log_debug("Blur background disabled by blur-background-exclude"); + blur_background_new = false; + } + } + + win_set_blur_background(ps, w, blur_background_new); +} + +/** + * Determine if a window should have rounded corners. + */ +static void win_determine_rounded_corners(session_t *ps, struct managed_win *w) { + if (ps->o.corner_radius == 0) { + w->corner_radius = 0; + return; + } + + // Don't round full screen windows & excluded windows + if ((w && win_is_fullscreen(ps, w)) || + c2_match(ps, w, ps->o.rounded_corners_blacklist, NULL)) { + w->corner_radius = 0; + log_debug("Not rounding corners for window %#010x", w->base.id); + } else { + w->corner_radius = ps->o.corner_radius; + log_debug("Rounding corners for window %#010x", w->base.id); + // Initialize the border color to an invalid value + w->border_col[0] = w->border_col[1] = w->border_col[2] = + w->border_col[3] = -1.0F; + } +} + +/** + * Update window opacity according to opacity rules. + */ +void win_update_opacity_rule(session_t *ps, struct managed_win *w) { + if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { + return; + } + + double opacity = 1.0; + bool is_set = false; + void *val = NULL; + if (c2_match(ps, w, ps->o.opacity_rules, &val)) { + opacity = ((double)(long)val) / 100.0; + is_set = true; + } + + w->opacity_set = opacity; + w->opacity_is_set = is_set; +} + +/** + * Function to be called on window data changes. + * + * TODO(yshui) need better name + */ +void win_on_factor_change(session_t *ps, struct managed_win *w) { + log_debug("Window %#010x (%s) factor change", w->base.id, w->name); + // Focus needs to be updated first, as other rules might depend on the focused + // state of the window + win_update_focused(ps, w); + + win_determine_shadow(ps, w); + win_determine_clip_shadow_above(ps, w); + win_determine_invert_color(ps, w); + win_determine_blur_background(ps, w); + win_determine_rounded_corners(ps, w); + w->mode = win_calc_mode(w); + log_debug("Window mode changed to %d", w->mode); + win_update_opacity_rule(ps, w); + if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { + w->paint_excluded = c2_match(ps, w, ps->o.paint_blacklist, NULL); + } + if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { + w->unredir_if_possible_excluded = + c2_match(ps, w, ps->o.unredir_if_possible_blacklist, NULL); + } + + w->fade_excluded = c2_match(ps, w, ps->o.fade_blacklist, NULL); + + win_update_opacity_target(ps, w); + + w->reg_ignore_valid = false; +} + +/** + * Update cache data in struct _win that depends on window size. + */ +void win_on_win_size_change(session_t *ps, struct managed_win *w) { + w->widthb = w->g.width + w->g.border_width * 2; + w->heightb = w->g.height + w->g.border_width * 2; + w->shadow_dx = ps->o.shadow_offset_x; + w->shadow_dy = ps->o.shadow_offset_y; + w->shadow_width = w->widthb + ps->o.shadow_radius * 2; + w->shadow_height = w->heightb + ps->o.shadow_radius * 2; + + // We don't handle property updates of non-visible windows until they are mapped. + assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && + w->state != WSTATE_UNMAPPING); + + // Invalidate the shadow we built + if (w->state != WSTATE_DESTROYING) + win_set_flags(w, WIN_FLAGS_IMAGES_STALE); + + ps->pending_updates = true; + free_paint(ps, &w->shadow_paint); +} + +/** + * Update window type. + */ +void win_update_wintype(session_t *ps, struct managed_win *w) { + const wintype_t wtype_old = w->window_type; + + // Detect window type here + w->window_type = wid_get_prop_wintype(ps, w->client_win); + + // Conform to EWMH standard, if _NET_WM_WINDOW_TYPE is not present, take + // override-redirect windows or windows without WM_TRANSIENT_FOR as + // _NET_WM_WINDOW_TYPE_NORMAL, otherwise as _NET_WM_WINDOW_TYPE_DIALOG. + if (WINTYPE_UNKNOWN == w->window_type) { + if (w->a.override_redirect || + !wid_has_prop(ps, w->client_win, ps->atoms->aWM_TRANSIENT_FOR)) + w->window_type = WINTYPE_NORMAL; + else + w->window_type = WINTYPE_DIALOG; + } + + if (w->window_type != wtype_old) { + win_on_factor_change(ps, w); + } +} + +/** + * Mark a window as the client window of another. + * + * @param ps current session + * @param w struct _win of the parent window + * @param client window ID of the client window + */ +void win_mark_client(session_t *ps, struct managed_win *w, xcb_window_t client) { + w->client_win = client; + + // If the window isn't mapped yet, stop here, as the function will be + // called in map_win() + if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { + return; + } + + auto e = xcb_request_check( + ps->c, xcb_change_window_attributes( + ps->c, client, XCB_CW_EVENT_MASK, + (const uint32_t[]){determine_evmask(ps, client, WIN_EVMODE_CLIENT)})); + if (e) { + log_error("Failed to change event mask of window %#010x", client); + free(e); + } + + win_update_wintype(ps, w); + + // Get frame widths. The window is in damaged area already. + win_update_frame_extents(ps, w, client); + + // Get window group + if (ps->o.track_leader) { + win_update_leader(ps, w); + } + + // Get window name and class if we are tracking them + win_update_name(ps, w); + win_update_class(ps, w); + win_update_role(ps, w); + + // Update everything related to conditions + win_on_factor_change(ps, w); + + auto r = xcb_get_window_attributes_reply( + ps->c, xcb_get_window_attributes(ps->c, w->client_win), &e); + if (!r) { + log_error_x_error(e, "Failed to get client window attributes"); + return; + } + + w->client_pictfmt = x_get_pictform_for_visual(ps->c, r->visual); + free(r); +} + +/** + * Unmark current client window of a window. + * + * @param ps current session + * @param w struct _win of the parent window + */ +void win_unmark_client(session_t *ps, struct managed_win *w) { + xcb_window_t client = w->client_win; + log_debug("Detaching client window %#010x from frame %#010x (%s)", client, + w->base.id, w->name); + + w->client_win = XCB_NONE; + + // Recheck event mask + xcb_change_window_attributes( + ps->c, client, XCB_CW_EVENT_MASK, + (const uint32_t[]){determine_evmask(ps, client, WIN_EVMODE_UNKNOWN)}); +} + +/** + * Look for the client window of a particular window. + */ +static xcb_window_t find_client_win(session_t *ps, xcb_window_t w) { + if (wid_has_prop(ps, w, ps->atoms->aWM_STATE)) { + return w; + } + + xcb_query_tree_reply_t *reply = + xcb_query_tree_reply(ps->c, xcb_query_tree(ps->c, w), NULL); + if (!reply) { + return 0; + } + + xcb_window_t *children = xcb_query_tree_children(reply); + int nchildren = xcb_query_tree_children_length(reply); + int i; + xcb_window_t ret = 0; + + for (i = 0; i < nchildren; ++i) { + if ((ret = find_client_win(ps, children[i]))) { + break; + } + } + + free(reply); + + return ret; +} + +/** + * Recheck client window of a window. + * + * @param ps current session + * @param w struct _win of the parent window + */ +void win_recheck_client(session_t *ps, struct managed_win *w) { + assert(ps->server_grabbed); + // Initialize wmwin to false + w->wmwin = false; + + // Look for the client window + + // Always recursively look for a window with WM_STATE, as Fluxbox + // sets override-redirect flags on all frame windows. + xcb_window_t cw = find_client_win(ps, w->base.id); + if (cw) { + log_debug("(%#010x): client %#010x", w->base.id, cw); + } + // Set a window's client window to itself if we couldn't find a + // client window + if (!cw) { + cw = w->base.id; + w->wmwin = !w->a.override_redirect; + log_debug("(%#010x): client self (%s)", w->base.id, + (w->wmwin ? "wmwin" : "override-redirected")); + } + + // Unmark the old one + if (w->client_win && w->client_win != cw) { + win_unmark_client(ps, w); + } + + // Mark the new one + win_mark_client(ps, w, cw); +} + +/** + * Free all resources in a <code>struct _win</code>. + */ +void free_win_res(session_t *ps, struct managed_win *w) { + // No need to call backend release_image here because + // finish_unmap_win should've done that for us. + // XXX unless we are called by session_destroy + // assert(w->win_data == NULL); + free_win_res_glx(ps, w); + free_paint(ps, &w->paint); + free_paint(ps, &w->shadow_paint); + // Above should be done during unmapping + // Except when we are called by session_destroy + + pixman_region32_fini(&w->bounding_shape); + // BadDamage may be thrown if the window is destroyed + set_ignore_cookie(ps, xcb_damage_destroy(ps->c, w->damage)); + rc_region_unref(&w->reg_ignore); + free(w->name); + free(w->class_instance); + free(w->class_general); + free(w->role); + + free(w->stale_props); + w->stale_props = NULL; + w->stale_props_capacity = 0; +} + +/// Insert a new window after list_node `prev` +/// New window will be in unmapped state +static struct win *add_win(session_t *ps, xcb_window_t id, struct list_node *prev) { + log_debug("Adding window %#010x", id); + struct win *old_w = NULL; + HASH_FIND_INT(ps->windows, &id, old_w); + assert(old_w == NULL); + + auto new_w = cmalloc(struct win); + list_insert_after(prev, &new_w->stack_neighbour); + new_w->id = id; + new_w->managed = false; + new_w->is_new = true; + new_w->destroyed = false; + + HASH_ADD_INT(ps->windows, id, new_w); + ps->pending_updates = true; + return new_w; +} + +/// Insert a new win entry at the top of the stack +struct win *add_win_top(session_t *ps, xcb_window_t id) { + return add_win(ps, id, &ps->window_stack); +} + +/// Insert a new window above window with id `below`, if there is no window, add to top +/// New window will be in unmapped state +struct win *add_win_above(session_t *ps, xcb_window_t id, xcb_window_t below) { + struct win *w = NULL; + HASH_FIND_INT(ps->windows, &below, w); + if (!w) { + if (!list_is_empty(&ps->window_stack)) { + // `below` window is not found even if the window stack is not + // empty + return NULL; + } + return add_win_top(ps, id); + } else { + // we found something from the hash table, so if the stack is empty, + // we are in an inconsistent state. + assert(!list_is_empty(&ps->window_stack)); + return add_win(ps, id, w->stack_neighbour.prev); + } +} + +/// Query the Xorg for information about window `win` +/// `win` pointer might become invalid after this function returns +/// Returns the pointer to the window, might be different from `w` +struct win *fill_win(session_t *ps, struct win *w) { + static const struct managed_win win_def = { + // No need to initialize. (or, you can think that + // they are initialized right here). + // The following ones are updated during paint or paint preprocess + .shadow_opacity = 0.0, + .to_paint = false, + .frame_opacity = 1.0, + .dim = false, + .invert_color = false, + .blur_background = false, + .reg_ignore = NULL, + // The following ones are updated for other reasons + .pixmap_damaged = false, // updated by damage events + .state = WSTATE_UNMAPPED, // updated by window state changes + .in_openclose = true, // set to false after first map is done, + // true here because window is just created + .animation_velocity_x = 0.0, // updated by window geometry changes + .animation_velocity_y = 0.0, // updated by window geometry changes + .animation_velocity_w = 0.0, // updated by window geometry changes + .animation_velocity_h = 0.0, // updated by window geometry changes + .animation_progress = 1.0, // updated by window geometry changes + .animation_inv_og_distance = NAN, // updated by window geometry changes + .reg_ignore_valid = false, // set to true when damaged + .flags = WIN_FLAGS_IMAGES_NONE, // updated by property/attributes/etc + // change + .stale_props = NULL, + .stale_props_capacity = 0, + + // Runtime variables, updated by dbus + .fade_force = UNSET, + .shadow_force = UNSET, + .focused_force = UNSET, + .invert_color_force = UNSET, + + // Initialized in this function + .a = {0}, + .pictfmt = NULL, + .client_pictfmt = NULL, + .widthb = 0, + .heightb = 0, + .shadow_dx = 0, + .shadow_dy = 0, + .shadow_width = 0, + .shadow_height = 0, + .damage = XCB_NONE, + + // Not initialized until mapped, this variables + // have no meaning or have no use until the window + // is mapped + .win_image = NULL, + .old_win_image = NULL, + .shadow_image = NULL, + .prev_trans = NULL, + .shadow = false, + .clip_shadow_above = false, + .xinerama_scr = -1, + .mode = WMODE_TRANS, + .ever_damaged = false, + .client_win = XCB_NONE, + .leader = XCB_NONE, + .cache_leader = XCB_NONE, + .window_type = WINTYPE_UNKNOWN, + .wmwin = false, + .focused = false, + .opacity = 0, + .opacity_target = 0, + .has_opacity_prop = false, + .opacity_prop = OPAQUE, + .opacity_is_set = false, + .opacity_set = 1, + .frame_extents = MARGIN_INIT, // in win_mark_client + .bounding_shaped = false, + .bounding_shape = {0}, + .rounded_corners = false, + .paint_excluded = false, + .fade_excluded = false, + .unredir_if_possible_excluded = false, + .prop_shadow = -1, + // following 4 are set in win_mark_client + .name = NULL, + .class_instance = NULL, + .class_general = NULL, + .role = NULL, + + // Initialized during paint + .paint = PAINT_INIT, + .shadow_paint = PAINT_INIT, + + .corner_radius = 0, + }; + + assert(!w->destroyed); + assert(w->is_new); + + w->is_new = false; + + // Reject overlay window and already added windows + if (w->id == ps->overlay) { + return w; + } + + auto duplicated_win = find_managed_win(ps, w->id); + if (duplicated_win) { + log_debug("Window %#010x (recorded name: %s) added multiple times", w->id, + duplicated_win->name); + return &duplicated_win->base; + } + + log_debug("Managing window %#010x", w->id); + xcb_get_window_attributes_cookie_t acookie = xcb_get_window_attributes(ps->c, w->id); + xcb_get_window_attributes_reply_t *a = + xcb_get_window_attributes_reply(ps->c, acookie, NULL); + if (!a || a->map_state == XCB_MAP_STATE_UNVIEWABLE) { + // Failed to get window attributes or geometry probably means + // the window is gone already. Unviewable means the window is + // already reparented elsewhere. + // BTW, we don't care about Input Only windows, except for stacking + // proposes, so we need to keep track of them still. + free(a); + return w; + } + + if (a->_class == XCB_WINDOW_CLASS_INPUT_ONLY) { + // No need to manage this window, but we still keep it on the window stack + w->managed = false; + free(a); + return w; + } + + // Allocate and initialize the new win structure + auto new_internal = cmalloc(struct managed_win_internal); + auto new = (struct managed_win *)new_internal; + + // Fill structure + // We only need to initialize the part that are not initialized + // by map_win + *new = win_def; + new->base = *w; + new->base.managed = true; + new->a = *a; + pixman_region32_init(&new->bounding_shape); + + free(a); + + xcb_generic_error_t *e; + auto g = xcb_get_geometry_reply(ps->c, xcb_get_geometry(ps->c, w->id), &e); + if (!g) { + log_error_x_error(e, "Failed to get geometry of window %#010x", w->id); + free(e); + free(new); + return w; + } + new->pending_g = (struct win_geometry){ + .x = g->x, + .y = g->y, + .width = g->width, + .height = g->height, + .border_width = g->border_width, + }; + + free(g); + + // Create Damage for window (if not Input Only) + new->damage = x_new_id(ps->c); + e = xcb_request_check( + ps->c, xcb_damage_create_checked(ps->c, new->damage, w->id, + XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY)); + if (e) { + log_error_x_error(e, "Failed to create damage"); + free(e); + free(new); + return w; + } + + // Set window event mask + xcb_change_window_attributes( + ps->c, new->base.id, XCB_CW_EVENT_MASK, + (const uint32_t[]){determine_evmask(ps, new->base.id, WIN_EVMODE_FRAME)}); + + // Get notification when the shape of a window changes + if (ps->shape_exists) { + xcb_shape_select_input(ps->c, new->base.id, 1); + } + + new->pictfmt = x_get_pictform_for_visual(ps->c, new->a.visual); + new->client_pictfmt = NULL; + + list_replace(&w->stack_neighbour, &new->base.stack_neighbour); + struct win *replaced = NULL; + HASH_REPLACE_INT(ps->windows, id, &new->base, replaced); + assert(replaced == w); + free(w); + + // Set all the stale flags on this new window, so it's properties will get updated + // when it's mapped + win_set_flags(new, WIN_FLAGS_CLIENT_STALE | WIN_FLAGS_SIZE_STALE | + WIN_FLAGS_POSITION_STALE | WIN_FLAGS_PROPERTY_STALE | + WIN_FLAGS_FACTOR_CHANGED); + xcb_atom_t init_stale_props[] = { + ps->atoms->a_NET_WM_WINDOW_TYPE, ps->atoms->a_NET_WM_WINDOW_OPACITY, + ps->atoms->a_NET_FRAME_EXTENTS, ps->atoms->aWM_NAME, + ps->atoms->a_NET_WM_NAME, ps->atoms->aWM_CLASS, + ps->atoms->aWM_WINDOW_ROLE, ps->atoms->a_COMPTON_SHADOW, + ps->atoms->aWM_CLIENT_LEADER, ps->atoms->aWM_TRANSIENT_FOR, + }; + win_set_properties_stale(new, init_stale_props, ARR_SIZE(init_stale_props)); + +#ifdef CONFIG_DBUS + // Send D-Bus signal + if (ps->o.dbus) { + cdbus_ev_win_added(ps, &new->base); + } +#endif + return &new->base; +} + +/** + * Set leader of a window. + */ +static inline void win_set_leader(session_t *ps, struct managed_win *w, xcb_window_t nleader) { + // If the leader changes + if (w->leader != nleader) { + xcb_window_t cache_leader_old = win_get_leader(ps, w); + + w->leader = nleader; + + // Forcefully do this to deal with the case when a child window + // gets mapped before parent, or when the window is a waypoint + clear_cache_win_leaders(ps); + + // Update the old and new window group and active_leader if the window + // could affect their state. + xcb_window_t cache_leader = win_get_leader(ps, w); + if (win_is_focused_raw(ps, w) && cache_leader_old != cache_leader) { + ps->active_leader = cache_leader; + + group_on_factor_change(ps, cache_leader_old); + group_on_factor_change(ps, cache_leader); + } + + // Update everything related to conditions + win_on_factor_change(ps, w); + } +} + +/** + * Update leader of a window. + */ +void win_update_leader(session_t *ps, struct managed_win *w) { + xcb_window_t leader = XCB_NONE; + + // Read the leader properties + if (ps->o.detect_transient && !leader) { + leader = + wid_get_prop_window(ps->c, w->client_win, ps->atoms->aWM_TRANSIENT_FOR); + } + + if (ps->o.detect_client_leader && !leader) { + leader = + wid_get_prop_window(ps->c, w->client_win, ps->atoms->aWM_CLIENT_LEADER); + } + + win_set_leader(ps, w, leader); + + log_trace("(%#010x): client %#010x, leader %#010x, cache %#010x", w->base.id, + w->client_win, w->leader, win_get_leader(ps, w)); +} + +/** + * Internal function of win_get_leader(). + */ +static xcb_window_t win_get_leader_raw(session_t *ps, struct managed_win *w, int recursions) { + // Rebuild the cache if needed + if (!w->cache_leader && (w->client_win || w->leader)) { + // Leader defaults to client window + if (!(w->cache_leader = w->leader)) + w->cache_leader = w->client_win; + + // If the leader of this window isn't itself, look for its ancestors + if (w->cache_leader && w->cache_leader != w->client_win) { + auto wp = find_toplevel(ps, w->cache_leader); + if (wp) { + // Dead loop? + if (recursions > WIN_GET_LEADER_MAX_RECURSION) + return XCB_NONE; + + w->cache_leader = win_get_leader_raw(ps, wp, recursions + 1); + } + } + } + + return w->cache_leader; +} + +/** + * Retrieve the <code>WM_CLASS</code> of a window and update its + * <code>win</code> structure. + */ +bool win_update_class(session_t *ps, struct managed_win *w) { + char **strlst = NULL; + int nstr = 0; + + // Can't do anything if there's no client window + if (!w->client_win) + return false; + + // Free and reset old strings + free(w->class_instance); + free(w->class_general); + w->class_instance = NULL; + w->class_general = NULL; + + // Retrieve the property string list + if (!wid_get_text_prop(ps, w->client_win, ps->atoms->aWM_CLASS, &strlst, &nstr)) { + return false; + } + + // Copy the strings if successful + w->class_instance = strdup(strlst[0]); + + if (nstr > 1) { + w->class_general = strdup(strlst[1]); + } + + free(strlst); + + log_trace("(%#010x): client = %#010x, " + "instance = \"%s\", general = \"%s\"", + w->base.id, w->client_win, w->class_instance, w->class_general); + + return true; +} + +/** + * Handle window focus change. + */ +static void win_on_focus_change(session_t *ps, struct managed_win *w) { + // If window grouping detection is enabled + if (ps->o.track_leader) { + xcb_window_t leader = win_get_leader(ps, w); + + // If the window gets focused, replace the old active_leader + if (win_is_focused_raw(ps, w) && leader != ps->active_leader) { + xcb_window_t active_leader_old = ps->active_leader; + + ps->active_leader = leader; + + group_on_factor_change(ps, active_leader_old); + group_on_factor_change(ps, leader); + } + // If the group get unfocused, remove it from active_leader + else if (!win_is_focused_raw(ps, w) && leader && + leader == ps->active_leader && !group_is_focused(ps, leader)) { + ps->active_leader = XCB_NONE; + group_on_factor_change(ps, leader); + } + } + + // Update everything related to conditions + win_on_factor_change(ps, w); + +#ifdef CONFIG_DBUS + // Send D-Bus signal + if (ps->o.dbus) { + if (win_is_focused_raw(ps, w)) { + cdbus_ev_win_focusin(ps, &w->base); + } else { + cdbus_ev_win_focusout(ps, &w->base); + } + } +#endif +} + +/** + * Set real focused state of a window. + */ +void win_set_focused(session_t *ps, struct managed_win *w) { + // Unmapped windows will have their focused state reset on map + if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { + return; + } + + if (win_is_focused_raw(ps, w)) { + return; + } + + auto old_active_win = ps->active_win; + ps->active_win = w; + assert(win_is_focused_raw(ps, w)); + + if (old_active_win) { + win_on_focus_change(ps, old_active_win); + } + win_on_focus_change(ps, w); +} + +/** + * Get a rectangular region a window (and possibly its shadow) occupies. + * + * Note w->shadow and shadow geometry must be correct before calling this + * function. + */ +void win_extents(const struct managed_win *w, region_t *res) { + pixman_region32_clear(res); + pixman_region32_union_rect(res, res, w->g.x, w->g.y, (uint)w->widthb, (uint)w->heightb); + + if (w->shadow) { + assert(w->shadow_width >= 0 && w->shadow_height >= 0); + pixman_region32_union_rect(res, res, w->g.x + w->shadow_dx, + w->g.y + w->shadow_dy, (uint)w->shadow_width, + (uint)w->shadow_height); + } +} + +gen_by_val(win_extents); + +/** + * Update the out-dated bounding shape of a window. + * + * Mark the window shape as updated + */ +void win_update_bounding_shape(session_t *ps, struct managed_win *w) { + if (ps->shape_exists) { + w->bounding_shaped = win_bounding_shaped(ps, w->base.id); + } + + // We don't handle property updates of non-visible windows until they are mapped. + assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && + w->state != WSTATE_UNMAPPING); + + pixman_region32_clear(&w->bounding_shape); + // Start with the window rectangular region + win_get_region_local(w, &w->bounding_shape); + + // Only request for a bounding region if the window is shaped + // (while loop is used to avoid goto, not an actual loop) + while (w->bounding_shaped) { + /* + * if window doesn't exist anymore, this will generate an error + * as well as not generate a region. + */ + + xcb_shape_get_rectangles_reply_t *r = xcb_shape_get_rectangles_reply( + ps->c, + xcb_shape_get_rectangles(ps->c, w->base.id, XCB_SHAPE_SK_BOUNDING), NULL); + + if (!r) { + break; + } + + xcb_rectangle_t *xrects = xcb_shape_get_rectangles_rectangles(r); + int nrects = xcb_shape_get_rectangles_rectangles_length(r); + rect_t *rects = from_x_rects(nrects, xrects); + free(r); + + region_t br; + pixman_region32_init_rects(&br, rects, nrects); + free(rects); + + // Add border width because we are using a different origin. + // X thinks the top left of the inner window is the origin + // (for the bounding shape, althought xcb_get_geometry thinks + // the outer top left (outer means outside of the window + // border) is the origin), + // We think the top left of the border is the origin + pixman_region32_translate(&br, w->g.border_width, w->g.border_width); + + // Intersect the bounding region we got with the window rectangle, to + // make sure the bounding region is not bigger than the window + // rectangle + pixman_region32_intersect(&w->bounding_shape, &w->bounding_shape, &br); + pixman_region32_fini(&br); + break; + } + + if (w->bounding_shaped && ps->o.detect_rounded_corners) { + w->rounded_corners = win_has_rounded_corners(w); + } + + // Window shape changed, we should free old wpaint and shadow pict + // log_trace("free out dated pict"); + win_set_flags(w, WIN_FLAGS_IMAGES_STALE); + ps->pending_updates = true; + + free_paint(ps, &w->paint); + free_paint(ps, &w->shadow_paint); + + win_on_factor_change(ps, w); +} + +/** + * Reread opacity property of a window. + */ +void win_update_opacity_prop(session_t *ps, struct managed_win *w) { + // get frame opacity first + w->has_opacity_prop = wid_get_opacity_prop(ps, w->base.id, OPAQUE, &w->opacity_prop); + + if (w->has_opacity_prop) { + // opacity found + return; + } + + if (ps->o.detect_client_opacity && w->client_win && w->base.id == w->client_win) { + // checking client opacity not allowed + return; + } + + // get client opacity + w->has_opacity_prop = + wid_get_opacity_prop(ps, w->client_win, OPAQUE, &w->opacity_prop); +} + +/** + * Retrieve frame extents from a window. + */ +void win_update_frame_extents(session_t *ps, struct managed_win *w, xcb_window_t client) { + winprop_t prop = x_get_prop(ps->c, client, ps->atoms->a_NET_FRAME_EXTENTS, 4L, + XCB_ATOM_CARDINAL, 32); + + if (prop.nitems == 4) { + int extents[4]; + for (int i = 0; i < 4; i++) { + if (prop.c32[i] > (uint32_t)INT_MAX) { + log_warn("Your window manager sets a absurd " + "_NET_FRAME_EXTENTS value (%u), ignoring it.", + prop.c32[i]); + memset(extents, 0, sizeof(extents)); + break; + } + extents[i] = (int)prop.c32[i]; + } + + const bool changed = w->frame_extents.left != extents[0] || + w->frame_extents.right != extents[1] || + w->frame_extents.top != extents[2] || + w->frame_extents.bottom != extents[3]; + w->frame_extents.left = extents[0]; + w->frame_extents.right = extents[1]; + w->frame_extents.top = extents[2]; + w->frame_extents.bottom = extents[3]; + + // If frame_opacity != 1, then frame of this window + // is not included in reg_ignore of underneath windows + if (ps->o.frame_opacity == 1 && changed) { + w->reg_ignore_valid = false; + } + } + + log_trace("(%#010x): %d, %d, %d, %d", w->base.id, w->frame_extents.left, + w->frame_extents.right, w->frame_extents.top, w->frame_extents.bottom); + + free_winprop(&prop); +} + +bool win_is_region_ignore_valid(session_t *ps, const struct managed_win *w) { + win_stack_foreach_managed(i, &ps->window_stack) { + if (i == w) { + break; + } + if (!i->reg_ignore_valid) { + return false; + } + } + return true; +} + +/** + * Stop listening for events on a particular window. + */ +void win_ev_stop(session_t *ps, const struct win *w) { + xcb_change_window_attributes(ps->c, w->id, XCB_CW_EVENT_MASK, (const uint32_t[]){0}); + + if (!w->managed) { + return; + } + + auto mw = (struct managed_win *)w; + if (mw->client_win) { + xcb_change_window_attributes(ps->c, mw->client_win, XCB_CW_EVENT_MASK, + (const uint32_t[]){0}); + } + + if (ps->shape_exists) { + xcb_shape_select_input(ps->c, w->id, 0); + } +} + +/// Finish the unmapping of a window (e.g. after fading has finished). +/// Doesn't free `w` +static void unmap_win_finish(session_t *ps, struct managed_win *w) { + w->reg_ignore_valid = false; + w->state = WSTATE_UNMAPPED; + + // We are in unmap_win, this window definitely was viewable + if (ps->backend_data) { + // Only the pixmap needs to be freed and reacquired when mapping. + // Shadow image can be preserved. + if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { + win_release_pixmap(ps->backend_data, w); + win_release_oldpixmap(ps->backend_data, w); + } + } else { + assert(!w->win_image); + assert(!w->old_win_image); + assert(!w->shadow_image); + } + + // Force animation to completed position + w->animation_velocity_x = 0; + w->animation_velocity_y = 0; + w->animation_velocity_w = 0; + w->animation_velocity_h = 0; + w->animation_progress = 1.0; + + free_paint(ps, &w->paint); + free_paint(ps, &w->shadow_paint); + + // Try again at binding images when the window is mapped next time + win_clear_flags(w, WIN_FLAGS_IMAGE_ERROR); + + // Flag window so that it gets animated when it reapears + // in case it wasn't destroyed + win_set_flags(w, WIN_FLAGS_POSITION_STALE); + win_set_flags(w, WIN_FLAGS_SIZE_STALE); +} + +/// Finish the destruction of a window (e.g. after fading has finished). +/// Frees `w` +static void destroy_win_finish(session_t *ps, struct win *w) { + log_trace("Trying to finish destroying (%#010x)", w->id); + + auto next_w = win_stack_find_next_managed(ps, &w->stack_neighbour); + list_remove(&w->stack_neighbour); + + if (w->managed) { + auto mw = (struct managed_win *)w; + + if (mw->state != WSTATE_UNMAPPED) { + // Only UNMAPPED state has window resources freed, otherwise + // we need to call unmap_win_finish to free them. + // XXX actually we unmap_win_finish only frees the rendering + // resources, we still need to call free_win_res. will fix + // later. + unmap_win_finish(ps, mw); + } + + // Unmapping preserves the shadow image, so free it here + if (!win_check_flags_all(mw, WIN_FLAGS_SHADOW_NONE)) { + assert(mw->shadow_image != NULL); + win_release_shadow(ps->backend_data, mw); + } + + // Invalidate reg_ignore of windows below this one + // TODO(yshui) what if next_w is not mapped?? + /* TODO(yshui) seriously figure out how reg_ignore behaves. + * I think if `w` is unmapped, and destroyed after + * paint happened at least once, w->reg_ignore_valid would + * be true, and there is no need to invalid w->next->reg_ignore + * when w is destroyed. */ + if (next_w) { + rc_region_unref(&next_w->reg_ignore); + next_w->reg_ignore_valid = false; + } + + if (mw == ps->active_win) { + // Usually, the window cannot be the focused at destruction. + // FocusOut should be generated before the window is destroyed. We + // do this check just to be completely sure we don't have dangling + // references. + log_debug("window %#010x (%s) is destroyed while being focused", + w->id, mw->name); + ps->active_win = NULL; + } + + free_win_res(ps, mw); + + // Drop w from all prev_trans to avoid accessing freed memory in + // repair_win() + // TODO(yshui) there can only be one prev_trans pointing to w + win_stack_foreach_managed(w2, &ps->window_stack) { + if (mw == w2->prev_trans) { + w2->prev_trans = NULL; + } + } + } + + free(w); +} + +static void map_win_finish(struct managed_win *w) { + w->in_openclose = false; + w->state = WSTATE_MAPPED; +} + +/// Move window `w` so it's before `next` in the list +static inline void restack_win(session_t *ps, struct win *w, struct list_node *next) { + struct managed_win *mw = NULL; + if (w->managed) { + mw = (struct managed_win *)w; + } + + if (mw) { + // This invalidates all reg_ignore below the new stack position of `w` + mw->reg_ignore_valid = false; + rc_region_unref(&mw->reg_ignore); + + // This invalidates all reg_ignore below the old stack position of `w` + auto next_w = win_stack_find_next_managed(ps, &w->stack_neighbour); + if (next_w) { + next_w->reg_ignore_valid = false; + rc_region_unref(&next_w->reg_ignore); + } + } + + list_move_before(&w->stack_neighbour, next); + + // add damage for this window + if (mw) { + add_damage_from_win(ps, mw); + } + +#ifdef DEBUG_RESTACK + log_trace("Window stack modified. Current stack:"); + for (auto c = ps->list; c; c = c->next) { + const char *desc = ""; + if (c->state == WSTATE_DESTROYING) { + desc = "(D) "; + } + log_trace("%#010x \"%s\" %s", c->id, c->name, desc); + } +#endif +} + +/// Move window `w` so it's right above `below` +void restack_above(session_t *ps, struct win *w, xcb_window_t below) { + xcb_window_t old_below; + + if (!list_node_is_last(&ps->window_stack, &w->stack_neighbour)) { + old_below = list_next_entry(w, stack_neighbour)->id; + } else { + old_below = XCB_NONE; + } + log_debug("Restack %#010x (%s), old_below: %#010x, new_below: %#010x", w->id, + win_get_name_if_managed(w), old_below, below); + + if (old_below != below) { + struct list_node *new_next; + if (!below) { + new_next = &ps->window_stack; + } else { + struct win *tmp_w = NULL; + HASH_FIND_INT(ps->windows, &below, tmp_w); + + if (!tmp_w) { + log_error("Failed to found new below window %#010x.", below); + return; + } + + new_next = &tmp_w->stack_neighbour; + } + restack_win(ps, w, new_next); + } +} + +void restack_bottom(session_t *ps, struct win *w) { + restack_above(ps, w, 0); +} + +void restack_top(session_t *ps, struct win *w) { + log_debug("Restack %#010x (%s) to top", w->id, win_get_name_if_managed(w)); + if (&w->stack_neighbour == ps->window_stack.next) { + // already at top + return; + } + restack_win(ps, w, ps->window_stack.next); +} + +/// Start destroying a window. Windows cannot always be destroyed immediately +/// because of fading and such. +/// +/// @return whether the window has finished destroying and is freed +bool destroy_win_start(session_t *ps, struct win *w) { + auto mw = (struct managed_win *)w; + assert(w); + + log_debug("Destroying %#010x \"%s\", managed = %d", w->id, + (w->managed ? mw->name : NULL), w->managed); + + // Delete destroyed window from the hash table, even though the window might still + // be rendered for a while. We need to make sure future window with the same + // window id won't confuse us. Keep the window in the window stack if it's managed + // and mapped, since we might still need to render it (e.g. fading out). Window + // will be removed from the stack when it finishes destroying. + HASH_DEL(ps->windows, w); + + if (!w->managed || mw->state == WSTATE_UNMAPPED) { + // Window is already unmapped, or is an unmanged window, just destroy it + destroy_win_finish(ps, w); + return true; + } + + if (w->managed) { + // Clear IMAGES_STALE flags since the window is destroyed: Clear + // PIXMAP_STALE as there is no pixmap available anymore, so STALE doesn't + // make sense. + // XXX Clear SHADOW_STALE as setting/clearing flags on a destroyed window + // doesn't work leading to an inconsistent state where the shadow is + // refreshed but the flags are stuck in STALE. + // Do this before changing the window state to destroying + win_clear_flags(mw, WIN_FLAGS_IMAGES_STALE); + + // If size/shape/position information is stale, win_process_update_flags + // will update them and add the new window extents to damage. Since the + // window has been destroyed, we cannot get the complete information at + // this point, so we just add what we currently have to the damage. + if (win_check_flags_any(mw, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { + add_damage_from_win(ps, mw); + } + + // Clear some flags about stale window information. Because now the window + // is destroyed, we can't update them anyway. + win_clear_flags(mw, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE | + WIN_FLAGS_PROPERTY_STALE | + WIN_FLAGS_FACTOR_CHANGED | WIN_FLAGS_CLIENT_STALE); + + // Update state flags of a managed window + mw->state = WSTATE_DESTROYING; + mw->a.map_state = XCB_MAP_STATE_UNMAPPED; + mw->in_openclose = true; + } + + // don't need win_ev_stop because the window is gone anyway +#ifdef CONFIG_DBUS + // Send D-Bus signal + if (ps->o.dbus) { + cdbus_ev_win_destroyed(ps, w); + } +#endif + + if (!ps->redirected) { + // Skip transition if we are not rendering + return win_skip_fading(ps, mw); + } + + return false; +} + +void unmap_win_start(session_t *ps, struct managed_win *w) { + assert(w); + assert(w->base.managed); + assert(w->a._class != XCB_WINDOW_CLASS_INPUT_ONLY); + + log_debug("Unmapping %#010x \"%s\"", w->base.id, w->name); + + if (unlikely(w->state == WSTATE_DESTROYING)) { + log_warn("Trying to undestroy a window?"); + assert(false); + } + + bool was_damaged = w->ever_damaged; + w->ever_damaged = false; + + if (unlikely(w->state == WSTATE_UNMAPPING || w->state == WSTATE_UNMAPPED)) { + if (win_check_flags_all(w, WIN_FLAGS_MAPPED)) { + // Clear the pending map as this window is now unmapped + win_clear_flags(w, WIN_FLAGS_MAPPED); + } else { + log_warn("Trying to unmapping an already unmapped window %#010x " + "\"%s\"", + w->base.id, w->name); + assert(false); + } + return; + } + + // Note we don't update focused window here. This will either be + // triggered by subsequence Focus{In, Out} event, or by recheck_focus + + w->a.map_state = XCB_MAP_STATE_UNMAPPED; + w->state = WSTATE_UNMAPPING; + w->opacity_target_old = fmax(w->opacity_target, w->opacity_target_old); + w->opacity_target = win_calc_opacity_target(ps, w); + + if (ps->o.animations && + ps->o.animation_for_unmap_window != OPEN_WINDOW_ANIMATION_NONE && + ps->o.wintype_option[w->window_type].animation != 0) + { + init_animation_unmap(ps, w); + + double x_dist = w->animation_dest_center_x - w->animation_center_x; + double y_dist = w->animation_dest_center_y - w->animation_center_y; + double w_dist = w->animation_dest_w - w->animation_w; + double h_dist = w->animation_dest_h - w->animation_h; + w->animation_inv_og_distance = + 1.0 / sqrt(x_dist * x_dist + y_dist * y_dist + + w_dist * w_dist + h_dist * h_dist); + + if (isinf(w->animation_inv_og_distance)) + w->animation_inv_og_distance = 0; + + w->animation_progress = 0.0; + + if (w->old_win_image) { + ps->backend_data->ops->release_image(ps->backend_data, + w->old_win_image); + w->old_win_image = NULL; + } + } + +#ifdef CONFIG_DBUS + // Send D-Bus signal + if (ps->o.dbus) { + cdbus_ev_win_unmapped(ps, &w->base); + } +#endif + + if (!ps->redirected || !was_damaged) { + // If we are not redirected, we skip fading because we aren't rendering + // anything anyway. + // If the window wasn't ever damaged, it shouldn't be painted either. But + // a fading out window is always painted, so we have to skip fading here. + CHECK(!win_skip_fading(ps, w)); + } +} + +/** + * Execute fade callback of a window if fading finished. + * + * @return whether the window is destroyed and freed + */ +bool win_check_fade_finished(session_t *ps, struct managed_win *w) { + if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { + // No fading in progress + assert(w->opacity_target == w->opacity); + return false; + } + + if (w->opacity == w->opacity_target) { + switch (w->state) { + case WSTATE_UNMAPPING: unmap_win_finish(ps, w); return false; + case WSTATE_DESTROYING: destroy_win_finish(ps, &w->base); return true; + case WSTATE_MAPPING: map_win_finish(w); return false; + case WSTATE_FADING: w->state = WSTATE_MAPPED; break; + default: unreachable; + } + } + + return false; +} + +/// Skip the current in progress fading of window, +/// transition the window straight to its end state +/// +/// @return whether the window is destroyed and freed +bool win_skip_fading(session_t *ps, struct managed_win *w) { + if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { + assert(w->opacity_target == w->opacity); + return false; + } + log_debug("Skipping fading process of window %#010x (%s)", w->base.id, w->name); + w->opacity = w->opacity_target; + + if (w->animation_progress < 1) { + w->animation_progress = 1; + w->g.x = w->pending_g.x; + w->g.y = w->pending_g.y; + w->g.width = w->pending_g.width; + w->g.height = w->pending_g.height; + } + + return win_check_fade_finished(ps, w); +} + +/** + * Get the Xinerama screen a window is on. + * + * Return an index >= 0, or -1 if not found. + * + * TODO(yshui) move to x.c + * TODO(yshui) use xrandr + */ +void win_update_screen(int nscreens, region_t *screens, struct managed_win *w) { + w->xinerama_scr = -1; + + for (int i = 0; i < nscreens; i++) { + auto e = pixman_region32_extents(&screens[i]); + if (e->x1 <= w->g.x && e->y1 <= w->g.y && e->x2 >= w->g.x + w->widthb && + e->y2 >= w->g.y + w->heightb) { + w->xinerama_scr = i; + log_debug("Window %#010x (%s), %dx%d+%dx%d, is on screen %d " + "(%dx%d+%dx%d)", + w->base.id, w->name, w->g.x, w->g.y, w->widthb, w->heightb, + i, e->x1, e->y1, e->x2 - e->x1, e->y2 - e->y1); + return; + } + } + log_debug("Window %#010x (%s), %dx%d+%dx%d, is not contained by any screen", + w->base.id, w->name, w->g.x, w->g.y, w->g.width, w->g.height); +} + +/// Map an already registered window +void map_win_start(session_t *ps, struct managed_win *w) { + assert(ps->server_grabbed); + assert(w); + + // Don't care about window mapping if it's an InputOnly window + // Also, try avoiding mapping a window twice + if (w->a._class == XCB_WINDOW_CLASS_INPUT_ONLY) { + return; + } + + log_debug("Mapping (%#010x \"%s\")", w->base.id, w->name); + + assert(w->state != WSTATE_DESTROYING); + if (w->state != WSTATE_UNMAPPED && w->state != WSTATE_UNMAPPING) { + log_warn("Mapping an already mapped window"); + return; + } + + if (w->state == WSTATE_UNMAPPING) { + CHECK(!win_skip_fading(ps, w)); + // We skipped the unmapping process, the window was rendered, now it is + // not anymore. So we need to mark the then unmapping window as damaged. + // + // Solves problem when, for example, a window is unmapped then mapped in a + // different location + add_damage_from_win(ps, w); + assert(w); + } + + assert(w->state == WSTATE_UNMAPPED); + + // Rant: window size could change after we queried its geometry here and before + // we get its pixmap. Later, when we get back to the event processing loop, we + // will get the notification about size change from Xserver and try to refresh the + // pixmap, while the pixmap is actually already up-to-date (i.e. the notification + // is stale). There is basically no real way to prevent this, aside from grabbing + // the server. + + // XXX Can we assume map_state is always viewable? + w->a.map_state = XCB_MAP_STATE_VIEWABLE; + + // Update window mode here to check for ARGB windows + w->mode = win_calc_mode(w); + + log_debug("Window (%#010x) has type %s", w->base.id, WINTYPES[w->window_type]); + + // XXX We need to make sure that win_data is available + // iff `state` is MAPPED + w->state = WSTATE_MAPPING; + w->opacity_target_old = 0; + w->opacity_target = win_calc_opacity_target(ps, w); + + log_debug("Window %#010x has opacity %f, opacity target is %f", w->base.id, + w->opacity, w->opacity_target); + + // Cannot set w->ever_damaged = false here, since window mapping could be + // delayed, so a damage event might have already arrived before this function + // is called. But this should be unnecessary in the first place, since + // ever_damaged is set to false in unmap_win_finish anyway. + + // Sets the WIN_FLAGS_IMAGES_STALE flag so later in the critical section + // the window's image will be bound + + win_set_flags(w, WIN_FLAGS_PIXMAP_STALE); + +#ifdef CONFIG_DBUS + // Send D-Bus signal + if (ps->o.dbus) { + cdbus_ev_win_mapped(ps, &w->base); + } +#endif + + if (!ps->redirected) { + CHECK(!win_skip_fading(ps, w)); + } +} + +/** + * Update target window opacity depending on the current state. + */ +void win_update_opacity_target(session_t *ps, struct managed_win *w) { + auto opacity_target_old = w->opacity_target; + w->opacity_target = win_calc_opacity_target(ps, w); + + if (opacity_target_old == w->opacity_target) { + return; + } + + if (w->state == WSTATE_MAPPED) { + // Opacity target changed while MAPPED. Transition to FADING. + assert(w->opacity == opacity_target_old); + w->opacity_target_old = opacity_target_old; + w->state = WSTATE_FADING; + log_debug("Window %#010x (%s) opacity %f, opacity target %f, set " + "old target %f", + w->base.id, w->name, w->opacity, w->opacity_target, + w->opacity_target_old); + } else if (w->state == WSTATE_MAPPING) { + // Opacity target changed while fading in. + if (w->opacity >= w->opacity_target) { + // Already reached new target opacity. Transition to + // FADING. + map_win_finish(w); + w->opacity_target_old = fmax(opacity_target_old, w->opacity); + w->state = WSTATE_FADING; + log_debug("Window %#010x (%s) opacity %f already reached " + "new opacity target %f while mapping, set old " + "target %f", + w->base.id, w->name, w->opacity, w->opacity_target, + w->opacity_target_old); + } + } else if (w->state == WSTATE_FADING) { + // Opacity target changed while FADING. + if ((w->opacity < opacity_target_old && w->opacity > w->opacity_target) || + (w->opacity > opacity_target_old && w->opacity < w->opacity_target)) { + // Changed while fading in and will fade out or while + // fading out and will fade in. + w->opacity_target_old = opacity_target_old; + log_debug("Window %#010x (%s) opacity %f already reached " + "new opacity target %f while fading, set " + "old target %f", + w->base.id, w->name, w->opacity, w->opacity_target, + w->opacity_target_old); + } + } + + if (!ps->redirected) { + CHECK(!win_skip_fading(ps, w)); + } +} + +/** + * Find a managed window from window id in window linked list of the session. + */ +struct win *find_win(session_t *ps, xcb_window_t id) { + if (!id) { + return NULL; + } + + struct win *w = NULL; + HASH_FIND_INT(ps->windows, &id, w); + assert(w == NULL || !w->destroyed); + return w; +} + +/** + * Find a managed window from window id in window linked list of the session. + */ +struct managed_win *find_managed_win(session_t *ps, xcb_window_t id) { + struct win *w = find_win(ps, id); + if (!w || !w->managed) { + return NULL; + } + + auto mw = (struct managed_win *)w; + assert(mw->state != WSTATE_DESTROYING); + return mw; +} + +/** + * Find out the WM frame of a client window using existing data. + * + * @param id window ID + * @return struct win object of the found window, NULL if not found + */ +struct managed_win *find_toplevel(session_t *ps, xcb_window_t id) { + if (!id) { + return NULL; + } + + HASH_ITER2(ps->windows, w) { + assert(!w->destroyed); + if (!w->managed) { + continue; + } + + auto mw = (struct managed_win *)w; + if (mw->client_win == id) { + return mw; + } + } + + return NULL; +} + +/** + * Find a managed window that is, or is a parent of `wid`. + * + * @param ps current session + * @param wid window ID + * @return struct _win object of the found window, NULL if not found + */ +struct managed_win *find_managed_window_or_parent(session_t *ps, xcb_window_t wid) { + // TODO(yshui) this should probably be an "update tree", then find_toplevel. + // current approach is a bit more "racy", as the server state might be ahead of + // our state + struct win *w = NULL; + + // We traverse through its ancestors to find out the frame + // Using find_win here because if we found a unmanaged window we know about, we + // can stop early. + while (wid && wid != ps->root && !(w = find_win(ps, wid))) { + // xcb_query_tree probably fails if you run picom when X is somehow + // initializing (like add it in .xinitrc). In this case + // just leave it alone. + auto reply = xcb_query_tree_reply(ps->c, xcb_query_tree(ps->c, wid), NULL); + if (reply == NULL) { + break; + } + + wid = reply->parent; + free(reply); + } + + if (w == NULL || !w->managed) { + return NULL; + } + + return (struct managed_win *)w; +} + +/** + * Check if a rectangle includes the whole screen. + */ +static inline bool rect_is_fullscreen(const session_t *ps, int x, int y, int wid, int hei) { + return (x <= 0 && y <= 0 && (x + wid) >= ps->root_width && (y + hei) >= ps->root_height); +} + +/** + * Check if a window is fulscreen using EWMH + * + * TODO(yshui) cache this property + */ +static inline bool +win_is_fullscreen_xcb(xcb_connection_t *c, const struct atom *a, const xcb_window_t w) { + xcb_get_property_cookie_t prop = + xcb_get_property(c, 0, w, a->a_NET_WM_STATE, XCB_ATOM_ATOM, 0, 12); + xcb_get_property_reply_t *reply = xcb_get_property_reply(c, prop, NULL); + if (!reply) { + return false; + } + + if (reply->length) { + xcb_atom_t *val = xcb_get_property_value(reply); + for (uint32_t i = 0; i < reply->length; i++) { + if (val[i] != a->a_NET_WM_STATE_FULLSCREEN) { + continue; + } + free(reply); + return true; + } + } + free(reply); + return false; +} + +/// Set flags on a window. Some sanity checks are performed +void win_set_flags(struct managed_win *w, uint64_t flags) { + log_debug("Set flags %" PRIu64 " to window %#010x (%s)", flags, w->base.id, w->name); + if (unlikely(w->state == WSTATE_DESTROYING)) { + log_error("Flags set on a destroyed window %#010x (%s)", w->base.id, w->name); + return; + } + + w->flags |= flags; +} + +/// Clear flags on a window. Some sanity checks are performed +void win_clear_flags(struct managed_win *w, uint64_t flags) { + log_debug("Clear flags %" PRIu64 " from window %#010x (%s)", flags, w->base.id, + w->name); + if (unlikely(w->state == WSTATE_DESTROYING)) { + log_warn("Flags cleared on a destroyed window %#010x (%s)", w->base.id, + w->name); + return; + } + + w->flags = w->flags & (~flags); +} + +void win_set_properties_stale(struct managed_win *w, const xcb_atom_t *props, int nprops) { + const auto bits_per_element = sizeof(*w->stale_props) * 8; + size_t new_capacity = w->stale_props_capacity; + + // Calculate the new capacity of the properties array + for (int i = 0; i < nprops; i++) { + if (props[i] >= new_capacity * bits_per_element) { + new_capacity = props[i] / bits_per_element + 1; + } + } + + // Reallocate if necessary + if (new_capacity > w->stale_props_capacity) { + w->stale_props = + realloc(w->stale_props, new_capacity * sizeof(*w->stale_props)); + + // Clear the content of the newly allocated bytes + memset(w->stale_props + w->stale_props_capacity, 0, + (new_capacity - w->stale_props_capacity) * sizeof(*w->stale_props)); + w->stale_props_capacity = new_capacity; + } + + // Set the property bits + for (int i = 0; i < nprops; i++) { + w->stale_props[props[i] / bits_per_element] |= + 1UL << (props[i] % bits_per_element); + } + win_set_flags(w, WIN_FLAGS_PROPERTY_STALE); +} + +static void win_clear_all_properties_stale(struct managed_win *w) { + memset(w->stale_props, 0, w->stale_props_capacity * sizeof(*w->stale_props)); + win_clear_flags(w, WIN_FLAGS_PROPERTY_STALE); +} + +static bool win_fetch_and_unset_property_stale(struct managed_win *w, xcb_atom_t prop) { + const auto bits_per_element = sizeof(*w->stale_props) * 8; + if (prop >= w->stale_props_capacity * bits_per_element) { + return false; + } + + const auto mask = 1UL << (prop % bits_per_element); + bool ret = w->stale_props[prop / bits_per_element] & mask; + w->stale_props[prop / bits_per_element] &= ~mask; + return ret; +} + +bool win_check_flags_any(struct managed_win *w, uint64_t flags) { + return (w->flags & flags) != 0; +} + +bool win_check_flags_all(struct managed_win *w, uint64_t flags) { + return (w->flags & flags) == flags; +} + +/** + * Check if a window is a fullscreen window. + * + * It's not using w->border_size for performance measures. + */ +bool win_is_fullscreen(const session_t *ps, const struct managed_win *w) { + if (!ps->o.no_ewmh_fullscreen && + win_is_fullscreen_xcb(ps->c, ps->atoms, w->client_win)) { + return true; + } + return rect_is_fullscreen(ps, w->g.x, w->g.y, w->widthb, w->heightb) && + (!w->bounding_shaped || w->rounded_corners); +} + +/** + * Check if a window has BYPASS_COMPOSITOR property set + * + * TODO(yshui) cache this property + */ +bool win_is_bypassing_compositor(const session_t *ps, const struct managed_win *w) { + bool ret = false; + + auto prop = x_get_prop(ps->c, w->client_win, ps->atoms->a_NET_WM_BYPASS_COMPOSITOR, + 1L, XCB_ATOM_CARDINAL, 32); + + if (prop.nitems && *prop.c32 == 1) { + ret = true; + } + + free_winprop(&prop); + return ret; +} + +/** + * Check if a window is focused, without using any focus rules or forced focus settings + */ +bool win_is_focused_raw(const session_t *ps, const struct managed_win *w) { + return w->a.map_state == XCB_MAP_STATE_VIEWABLE && ps->active_win == w; +} + +// Find the managed window immediately below `i` in the window stack +struct managed_win * +win_stack_find_next_managed(const session_t *ps, const struct list_node *i) { + while (!list_node_is_last(&ps->window_stack, i)) { + auto next = list_entry(i->next, struct win, stack_neighbour); + if (next->managed) { + return (struct managed_win *)next; + } + i = &next->stack_neighbour; + } + return NULL; +} + +/// Return whether this window is mapped on the X server side +bool win_is_mapped_in_x(const struct managed_win *w) { + return w->state == WSTATE_MAPPING || w->state == WSTATE_FADING || + w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPING || + w->state == WSTATE_DESTROYING || (w->flags & WIN_FLAGS_MAPPED); +} diff --git a/src/win.h b/src/win.h new file mode 100644 index 0000000..d3a74f9 --- /dev/null +++ b/src/win.h @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2011-2013, Christopher Jeffrey +// Copyright (c) 2013 Richard Grenville <[email protected]> +#pragma once +#include <stdbool.h> +#include <xcb/damage.h> +#include <xcb/render.h> +#include <xcb/xcb.h> + +#include "uthash_extra.h" + +// FIXME shouldn't need this +#ifdef CONFIG_OPENGL +#include <GL/gl.h> +#endif + +#include "c2.h" +#include "compiler.h" +#include "list.h" +#include "region.h" +#include "render.h" +#include "types.h" +#include "utils.h" +#include "win_defs.h" +#include "x.h" + +struct backend_base; +typedef struct session session_t; +typedef struct _glx_texture glx_texture_t; + +#define win_stack_foreach_managed(w, win_stack) \ + list_foreach(struct managed_win, w, win_stack, base.stack_neighbour) if (w->base.managed) + +#define win_stack_foreach_managed_safe(w, win_stack) \ + list_foreach_safe(struct managed_win, w, win_stack, \ + base.stack_neighbour) if (w->base.managed) + +#ifdef CONFIG_OPENGL +// FIXME this type should be in opengl.h +// it is very unideal for it to be here +typedef struct { + /// Framebuffer used for blurring. + GLuint fbo; + /// Textures used for blurring. + GLuint textures[2]; + /// Width of the textures. + int width; + /// Height of the textures. + int height; +} glx_blur_cache_t; +#endif + +/// An entry in the window stack. May or may not correspond to a window we know about. +struct window_stack_entry { + struct list_node stack_neighbour; + /// The actual window correspond to this stack entry. NULL if we didn't know about + /// this window (e.g. an InputOnly window, or we haven't handled the window + /// creation yet) + struct win *win; + /// The window id. Might not be unique in the stack, because there might be + /// destroyed window still fading out in the stack. + xcb_window_t id; +}; + +/** + * About coordinate systems + * + * In general, X is the horizontal axis, Y is the vertical axis. + * X goes from left to right, Y goes downwards. + * + * Global: the origin is the top left corner of the Xorg screen. + * Local: the origin is the top left corner of the window, border is + * considered part of the window. + */ + +/// Structure representing a top-level managed window. +typedef struct win win; +struct win { + UT_hash_handle hh; + struct list_node stack_neighbour; + /// ID of the top-level frame window. + xcb_window_t id; + /// Whether the window is destroyed from Xorg's perspective + bool destroyed : 1; + /// True if we just received CreateNotify, and haven't queried X for any info + /// about the window + bool is_new : 1; + /// True if this window is managed, i.e. this struct is actually a `managed_win`. + /// Always false if `is_new` is true. + bool managed : 1; +}; + +struct win_geometry { + int16_t x; + int16_t y; + uint16_t width; + uint16_t height; + uint16_t border_width; +}; + +struct managed_win { + struct win base; + /// backend data attached to this window. Only available when + /// `state` is not UNMAPPED + void *win_image; + void *old_win_image; // Old window image for interpolating window contents during animations + void *shadow_image; + /// Pointer to the next higher window to paint. + struct managed_win *prev_trans; + /// Number of windows above this window + int stacking_rank; + // TODO(yshui) rethink reg_ignore + + // Core members + /// The "mapped state" of this window, doesn't necessary + /// match X mapped state, because of fading. + winstate_t state; + /// Window attributes. + xcb_get_window_attributes_reply_t a; + /// The geometry of the window body, excluding the window border region. + struct win_geometry g; + /// Updated geometry received in events + struct win_geometry pending_g; + /// Xinerama screen this window is on. + int xinerama_scr; + /// Window visual pict format + const xcb_render_pictforminfo_t *pictfmt; + /// Client window visual pict format + const xcb_render_pictforminfo_t *client_pictfmt; + /// Window painting mode. + winmode_t mode; + /// Whether the window has been damaged at least once. + bool ever_damaged; + /// Whether the window was damaged after last paint. + bool pixmap_damaged; + /// Damage of the window. + xcb_damage_damage_t damage; + /// Paint info of the window. + paint_t paint; + /// bitmap for properties which needs to be updated + uint64_t *stale_props; + /// number of uint64_ts that has been allocated for stale_props + uint64_t stale_props_capacity; + + /// Bounding shape of the window. In local coordinates. + /// See above about coordinate systems. + region_t bounding_shape; + /// Window flags. Definitions above. + uint64_t flags; + /// The region of screen that will be obscured when windows above is painted, + /// in global coordinates. + /// We use this to reduce the pixels that needed to be paint when painting + /// this window and anything underneath. Depends on window frame + /// opacity state, window geometry, window mapped/unmapped state, + /// window mode of the windows above. DOES NOT INCLUDE the body of THIS WINDOW. + /// NULL means reg_ignore has not been calculated for this window. + rc_region_t *reg_ignore; + /// Whether the reg_ignore of all windows beneath this window are valid + bool reg_ignore_valid; + /// Cached width/height of the window including border. + int widthb, heightb; + /// Whether the window is bounding-shaped. + bool bounding_shaped; + /// Whether the window just have rounded corners. + bool rounded_corners; + /// Whether this window is to be painted. + bool to_paint; + /// Whether the window is painting excluded. + bool paint_excluded; + /// Whether the window is unredirect-if-possible excluded. + bool unredir_if_possible_excluded; + /// Whether this window is in open/close state. + bool in_openclose; + /// Whether this window was transient when animated on open + bool animation_transient; + /// Current position and destination, for animation + double animation_center_x, animation_center_y; + double animation_dest_center_x, animation_dest_center_y; + double animation_w, animation_h; + double animation_dest_w, animation_dest_h; + /// Spring animation velocity + double animation_velocity_x, animation_velocity_y; + double animation_velocity_w, animation_velocity_h; + /// Track animation progress; goes from 0 to 1 + double animation_progress; + /// Inverse of the window distance at the start of animation, for + /// tracking animation progress + double animation_inv_og_distance; + + // Client window related members + /// ID of the top-level client window of the window. + xcb_window_t client_win; + /// Type of the window. + wintype_t window_type; + /// Whether it looks like a WM window. We consider a window WM window if + /// it does not have a decedent with WM_STATE and it is not override- + /// redirected itself. + bool wmwin; + /// Leader window ID of the window. + xcb_window_t leader; + /// Cached topmost window ID of the window. + xcb_window_t cache_leader; + + // Focus-related members + /// Whether the window is to be considered focused. + bool focused; + /// Override value of window focus state. Set by D-Bus method calls. + switch_t focused_force; + + // Blacklist related members + /// Name of the window. + char *name; + /// Window instance class of the window. + char *class_instance; + /// Window general class of the window. + char *class_general; + /// <code>WM_WINDOW_ROLE</code> value of the window. + char *role; + + // Opacity-related members + /// Current window opacity. + double opacity; + /// Target window opacity. + double opacity_target; + /// Previous window opacity. + double opacity_target_old; + /// true if window (or client window, for broken window managers + /// not transferring client window's _NET_WM_OPACITY value) has opacity prop + bool has_opacity_prop; + /// Cached value of opacity window attribute. + opacity_t opacity_prop; + /// true if opacity is set by some rules + bool opacity_is_set; + /// Last window opacity value set by the rules. + double opacity_set; + + /// Radius of rounded window corners + int corner_radius; + float border_col[4]; + + // Fading-related members + /// Override value of window fade state. Set by D-Bus method calls. + switch_t fade_force; + /// Whether fading is excluded by the rules. Calculated. + bool fade_excluded; + + // Frame-opacity-related members + /// Current window frame opacity. Affected by window opacity. + double frame_opacity; + /// Frame extents. Acquired from _NET_FRAME_EXTENTS. + margin_t frame_extents; + + // Shadow-related members + /// Whether a window has shadow. Calculated. + bool shadow; + /// Override value of window shadow state. Set by D-Bus method calls. + switch_t shadow_force; + /// Opacity of the shadow. Affected by window opacity and frame opacity. + double shadow_opacity; + /// X offset of shadow. Affected by commandline argument. + int shadow_dx; + /// Y offset of shadow. Affected by commandline argument. + int shadow_dy; + /// Width of shadow. Affected by window size and commandline argument. + int shadow_width; + /// Height of shadow. Affected by window size and commandline argument. + int shadow_height; + /// Picture to render shadow. Affected by window size. + paint_t shadow_paint; + /// The value of _COMPTON_SHADOW attribute of the window. Below 0 for + /// none. + long prop_shadow; + /// Do not paint shadow over this window. + bool clip_shadow_above; + + // Dim-related members + /// Whether the window is to be dimmed. + bool dim; + + /// Whether to invert window color. + bool invert_color; + /// Override value of window color inversion state. Set by D-Bus method + /// calls. + switch_t invert_color_force; + + /// Whether to blur window background. + bool blur_background; + +#ifdef CONFIG_OPENGL + /// Textures and FBO background blur use. + glx_blur_cache_t glx_blur_cache; + /// Background texture of the window + glx_texture_t *glx_texture_bg; +#endif +}; + +/// Process pending updates/images flags on a window. Has to be called in X critical +/// section +void win_process_update_flags(session_t *ps, struct managed_win *w); +void win_process_image_flags(session_t *ps, struct managed_win *w); +/// Bind a shadow to the window, with color `c` and shadow kernel `kernel` +bool win_bind_shadow(struct backend_base *b, struct managed_win *w, struct color c, + struct conv *kernel); + +/// Start the unmap of a window. We cannot unmap immediately since we might need to fade +/// the window out. +void unmap_win_start(struct session *, struct managed_win *); + +/// Start the mapping of a window. We cannot map immediately since we might need to fade +/// the window in. +void map_win_start(struct session *, struct managed_win *); + +/// Start the destroying of a window. Windows cannot always be destroyed immediately +/// because of fading and such. +bool must_use destroy_win_start(session_t *ps, struct win *w); + +/// Release images bound with a window, set the *_NONE flags on the window. Only to be +/// used when de-initializing the backend outside of win.c +void win_release_images(struct backend_base *base, struct managed_win *w); +winmode_t attr_pure win_calc_mode(const struct managed_win *w); +void win_set_shadow_force(session_t *ps, struct managed_win *w, switch_t val); +void win_set_fade_force(struct managed_win *w, switch_t val); +void win_set_focused_force(session_t *ps, struct managed_win *w, switch_t val); +void win_set_invert_color_force(session_t *ps, struct managed_win *w, switch_t val); +/** + * Set real focused state of a window. + */ +void win_set_focused(session_t *ps, struct managed_win *w); +bool attr_pure win_should_fade(session_t *ps, const struct managed_win *w); +void win_on_factor_change(session_t *ps, struct managed_win *w); +/** + * Update cache data in struct _win that depends on window size. + */ +void win_on_win_size_change(session_t *ps, struct managed_win *w); +void win_unmark_client(session_t *ps, struct managed_win *w); +void win_recheck_client(session_t *ps, struct managed_win *w); + +/** + * Calculate and return the opacity target of a window. + * + * The priority of opacity settings are: + * + * inactive_opacity_override (if set, and unfocused) > _NET_WM_WINDOW_OPACITY (if set) > + * opacity-rules (if matched) > window type default opacity > active/inactive opacity + * + * @param ps current session + * @param w struct _win object representing the window + * + * @return target opacity + */ +double attr_pure win_calc_opacity_target(session_t *ps, const struct managed_win *w); +bool attr_pure win_should_dim(session_t *ps, const struct managed_win *w); +void win_update_screen(int nscreens, region_t *screens, struct managed_win *w); +/** + * Retrieve the bounding shape of a window. + */ +// XXX was win_border_size +void win_update_bounding_shape(session_t *ps, struct managed_win *w); +/** + * Check if a window has BYPASS_COMPOSITOR property set + */ +bool win_is_bypassing_compositor(const session_t *ps, const struct managed_win *w); +/** + * Get a rectangular region in global coordinates a window (and possibly + * its shadow) occupies. + * + * Note w->shadow and shadow geometry must be correct before calling this + * function. + */ +void win_extents(const struct managed_win *w, region_t *res); +region_t win_extents_by_val(const struct managed_win *w); +/** + * Add a window to damaged area. + * + * @param ps current session + * @param w struct _win element representing the window + */ +void add_damage_from_win(session_t *ps, const struct managed_win *w); +/** + * Get a rectangular region a window occupies, excluding frame and shadow. + * + * Return region in global coordinates. + */ +void win_get_region_noframe_local(const struct managed_win *w, region_t *); +void win_get_region_noframe_local_without_corners(const struct managed_win *w, region_t *); + +/// Get the region for the frame of the window +void win_get_region_frame_local(const struct managed_win *w, region_t *res); +/// Get the region for the frame of the window, by value +region_t win_get_region_frame_local_by_val(const struct managed_win *w); +/// Insert a new window above window with id `below`, if there is no window, add to top +/// New window will be in unmapped state +struct win *add_win_above(session_t *ps, xcb_window_t id, xcb_window_t below); +/// Insert a new win entry at the top of the stack +struct win *add_win_top(session_t *ps, xcb_window_t id); +/// Query the Xorg for information about window `win` +/// `win` pointer might become invalid after this function returns +struct win *fill_win(session_t *ps, struct win *win); +/// Move window `w` to be right above `below` +void restack_above(session_t *ps, struct win *w, xcb_window_t below); +/// Move window `w` to the bottom of the stack +void restack_bottom(session_t *ps, struct win *w); +/// Move window `w` to the top of the stack +void restack_top(session_t *ps, struct win *w); + +/** + * Execute fade callback of a window if fading finished. + */ +bool must_use win_check_fade_finished(session_t *ps, struct managed_win *w); + +// Stop receiving events (except ConfigureNotify, XXX why?) from a window +void win_ev_stop(session_t *ps, const struct win *w); + +/// Skip the current in progress fading of window, +/// transition the window straight to its end state +/// +/// @return whether the window is destroyed and freed +bool must_use win_skip_fading(session_t *ps, struct managed_win *w); +/** + * Find a managed window from window id in window linked list of the session. + */ +struct managed_win *find_managed_win(session_t *ps, xcb_window_t id); +struct win *find_win(session_t *ps, xcb_window_t id); +struct managed_win *find_toplevel(session_t *ps, xcb_window_t id); +/** + * Find a managed window that is, or is a parent of `wid`. + * + * @param ps current session + * @param wid window ID + * @return struct _win object of the found window, NULL if not found + */ +struct managed_win *find_managed_window_or_parent(session_t *ps, xcb_window_t wid); + +/** + * Check if a window is a fullscreen window. + * + * It's not using w->border_size for performance measures. + */ +bool attr_pure win_is_fullscreen(const session_t *ps, const struct managed_win *w); + +/** + * Check if a window is focused, without using any focus rules or forced focus settings + */ +bool attr_pure win_is_focused_raw(const session_t *ps, const struct managed_win *w); + +/// check if window has ARGB visual +bool attr_pure win_has_alpha(const struct managed_win *w); + +/// check if reg_ignore_valid is true for all windows above us +bool attr_pure win_is_region_ignore_valid(session_t *ps, const struct managed_win *w); + +/// Whether a given window is mapped on the X server side +bool win_is_mapped_in_x(const struct managed_win *w); + +// Find the managed window immediately below `w` in the window stack +struct managed_win *attr_pure win_stack_find_next_managed(const session_t *ps, + const struct list_node *w); +/// Set flags on a window. Some sanity checks are performed +void win_set_flags(struct managed_win *w, uint64_t flags); +/// Clear flags on a window. Some sanity checks are performed +void win_clear_flags(struct managed_win *w, uint64_t flags); +/// Returns true if any of the flags in `flags` is set +bool win_check_flags_any(struct managed_win *w, uint64_t flags); +/// Returns true if all of the flags in `flags` are set +bool win_check_flags_all(struct managed_win *w, uint64_t flags); +/// Mark properties as stale for a window +void win_set_properties_stale(struct managed_win *w, const xcb_atom_t *prop, int nprops); + +static inline attr_unused void win_set_property_stale(struct managed_win *w, xcb_atom_t prop) { + return win_set_properties_stale(w, (xcb_atom_t[]){prop}, 1); +} + +/// Free all resources in a struct win +void free_win_res(session_t *ps, struct managed_win *w); + +static inline void win_region_remove_corners(const struct managed_win *w, region_t *res) { + region_t corners; + pixman_region32_init_rects( + &corners, + (rect_t[]){ + {.x1 = 0, .y1 = 0, .x2 = w->corner_radius, .y2 = w->corner_radius}, + {.x1 = 0, .y1 = w->heightb - w->corner_radius, .x2 = w->corner_radius, .y2 = w->heightb}, + {.x1 = w->widthb - w->corner_radius, .y1 = 0, .x2 = w->widthb, .y2 = w->corner_radius}, + {.x1 = w->widthb - w->corner_radius, + .y1 = w->heightb - w->corner_radius, + .x2 = w->widthb, + .y2 = w->heightb}, + }, + 4); + pixman_region32_subtract(res, res, &corners); + pixman_region32_fini(&corners); +} + +static inline region_t attr_unused win_get_bounding_shape_global_by_val(struct managed_win *w) { + region_t ret; + pixman_region32_init(&ret); + pixman_region32_copy(&ret, &w->bounding_shape); + pixman_region32_translate(&ret, w->g.x, w->g.y); + return ret; +} + +static inline region_t +win_get_bounding_shape_global_without_corners_by_val(struct managed_win *w) { + region_t ret; + pixman_region32_init(&ret); + pixman_region32_copy(&ret, &w->bounding_shape); + win_region_remove_corners(w, &ret); + pixman_region32_translate(&ret, w->g.x, w->g.y); + return ret; +} + +/** + * Calculate the extents of the frame of the given window based on EWMH + * _NET_FRAME_EXTENTS and the X window border width. + */ +static inline margin_t attr_pure attr_unused win_calc_frame_extents(const struct managed_win *w) { + margin_t result = w->frame_extents; + result.top = max2(result.top, w->g.border_width); + result.left = max2(result.left, w->g.border_width); + result.bottom = max2(result.bottom, w->g.border_width); + result.right = max2(result.right, w->g.border_width); + return result; +} + +/** + * Check whether a window has WM frames. + */ +static inline bool attr_pure attr_unused win_has_frame(const struct managed_win *w) { + return w->g.border_width || w->frame_extents.top || w->frame_extents.left || + w->frame_extents.right || w->frame_extents.bottom; +} diff --git a/src/win_defs.h b/src/win_defs.h new file mode 100644 index 0000000..e032bc7 --- /dev/null +++ b/src/win_defs.h @@ -0,0 +1,102 @@ +#pragma once +#include <stdint.h> + +typedef enum { + WINTYPE_UNKNOWN, + WINTYPE_DESKTOP, + WINTYPE_DOCK, + WINTYPE_TOOLBAR, + WINTYPE_MENU, + WINTYPE_UTILITY, + WINTYPE_SPLASH, + WINTYPE_DIALOG, + WINTYPE_NORMAL, + WINTYPE_DROPDOWN_MENU, + WINTYPE_POPUP_MENU, + WINTYPE_TOOLTIP, + WINTYPE_NOTIFICATION, + WINTYPE_COMBO, + WINTYPE_DND, + NUM_WINTYPES +} wintype_t; + +/// Enumeration type of window painting mode. +typedef enum { + WMODE_TRANS, // The window body is (potentially) transparent + WMODE_FRAME_TRANS, // The window body is opaque, but the frame is not + WMODE_SOLID, // The window is opaque including the frame +} winmode_t; + +/// Transition table: +/// (DESTROYED is when the win struct is destroyed and freed) +/// ('o' means in all other cases) +/// (Window is created in the UNMAPPED state) +/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// | |UNMAPPING|DESTROYING|MAPPING|FADING |UNMAPPED| MAPPED |DESTROYED| +/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// | UNMAPPING | o | Window |Window | - | Fading | - | - | +/// | | |destroyed |mapped | |finished| | | +/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// | DESTROYING | - | o | - | - | - | - | Fading | +/// | | | | | | | |finished | +/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// | MAPPING | Window | Window | o |Opacity| - | Fading | - | +/// | |unmapped |destroyed | |change | |finished| | +/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// | FADING | Window | Window | - | o | - | Fading | - | +/// | |unmapped |destroyed | | | |finished| | +/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// | UNMAPPED | - | - |Window | - | o | - | Window | +/// | | | |mapped | | | |destroyed| +/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// | MAPPED | Window | Window | - |Opacity| - | o | - | +/// | |unmapped |destroyed | |change | | | | +/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +typedef enum { + // The window is being faded out because it's unmapped. + WSTATE_UNMAPPING, + // The window is being faded out because it's destroyed, + WSTATE_DESTROYING, + // The window is being faded in + WSTATE_MAPPING, + // Window opacity is not at the target level + WSTATE_FADING, + // The window is mapped, no fading is in progress. + WSTATE_MAPPED, + // The window is unmapped, no fading is in progress. + WSTATE_UNMAPPED, +} winstate_t; + +enum win_flags { + // Note: *_NONE flags are mostly redudant and meant for detecting logical errors + // in the code + + /// pixmap is out of date, will be update in win_process_flags + WIN_FLAGS_PIXMAP_STALE = 1, + /// window does not have pixmap bound + WIN_FLAGS_PIXMAP_NONE = 2, + /// there was an error trying to bind the images + WIN_FLAGS_IMAGE_ERROR = 4, + /// shadow is out of date, will be updated in win_process_flags + WIN_FLAGS_SHADOW_STALE = 8, + /// shadow has not been generated + WIN_FLAGS_SHADOW_NONE = 16, + /// the client window needs to be updated + WIN_FLAGS_CLIENT_STALE = 32, + /// the window is mapped by X, we need to call map_win_start for it + WIN_FLAGS_MAPPED = 64, + /// this window has properties which needs to be updated + WIN_FLAGS_PROPERTY_STALE = 128, + // TODO(yshui) _maybe_ split SIZE_STALE into SIZE_STALE and SHAPE_STALE + /// this window has an unhandled size/shape change + WIN_FLAGS_SIZE_STALE = 256, + /// this window has an unhandled position (i.e. x and y) change + WIN_FLAGS_POSITION_STALE = 512, + /// need better name for this, is set when some aspects of the window changed + WIN_FLAGS_FACTOR_CHANGED = 1024, +}; + +static const uint64_t WIN_FLAGS_IMAGES_STALE = + WIN_FLAGS_PIXMAP_STALE | WIN_FLAGS_SHADOW_STALE; + +#define WIN_FLAGS_IMAGES_NONE (WIN_FLAGS_PIXMAP_NONE | WIN_FLAGS_SHADOW_NONE) @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> +#include <stdalign.h> +#include <stdbool.h> +#include <stdlib.h> + +#include <X11/Xutil.h> +#include <pixman.h> +#include <xcb/composite.h> +#include <xcb/damage.h> +#include <xcb/glx.h> +#include <xcb/render.h> +#include <xcb/sync.h> +#include <xcb/xcb.h> +#include <xcb/xcb_renderutil.h> +#include <xcb/xfixes.h> + +#include "atom.h" +#ifdef CONFIG_OPENGL +#include "backend/gl/glx.h" +#endif +#include "common.h" +#include "compiler.h" +#include "kernel.h" +#include "log.h" +#include "region.h" +#include "utils.h" +#include "x.h" + +/** + * Get a specific attribute of a window. + * + * Returns a blank structure if the returned type and format does not + * match the requested type and format. + * + * @param ps current session + * @param w window + * @param atom atom of attribute to fetch + * @param length length to read + * @param rtype atom of the requested type + * @param rformat requested format + * @return a <code>winprop_t</code> structure containing the attribute + * and number of items. A blank one on failure. + */ +winprop_t x_get_prop_with_offset(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom, + int offset, int length, xcb_atom_t rtype, int rformat) { + xcb_get_property_reply_t *r = xcb_get_property_reply( + c, + xcb_get_property(c, 0, w, atom, rtype, to_u32_checked(offset), + to_u32_checked(length)), + NULL); + + if (r && xcb_get_property_value_length(r) && + (rtype == XCB_GET_PROPERTY_TYPE_ANY || r->type == rtype) && + (!rformat || r->format == rformat) && + (r->format == 8 || r->format == 16 || r->format == 32)) { + auto len = xcb_get_property_value_length(r); + return (winprop_t){ + .ptr = xcb_get_property_value(r), + .nitems = (ulong)(len / (r->format / 8)), + .type = r->type, + .format = r->format, + .r = r, + }; + } + + free(r); + return (winprop_t){ + .ptr = NULL, .nitems = 0, .type = XCB_GET_PROPERTY_TYPE_ANY, .format = 0}; +} + +/// Get the type, format and size in bytes of a window's specific attribute. +winprop_info_t x_get_prop_info(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom) { + xcb_generic_error_t *e = NULL; + auto r = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_ANY, 0, 0), &e); + if (!r) { + log_debug_x_error(e, "Failed to get property info for window %#010x", w); + free(e); + return (winprop_info_t){ + .type = XCB_GET_PROPERTY_TYPE_ANY, .format = 0, .length = 0}; + } + + winprop_info_t winprop_info = { + .type = r->type, .format = r->format, .length = r->bytes_after}; + free(r); + + return winprop_info; +} + +/** + * Get the value of a type-<code>xcb_window_t</code> property of a window. + * + * @return the value if successful, 0 otherwise + */ +xcb_window_t wid_get_prop_window(xcb_connection_t *c, xcb_window_t wid, xcb_atom_t aprop) { + // Get the attribute + xcb_window_t p = XCB_NONE; + winprop_t prop = x_get_prop(c, wid, aprop, 1L, XCB_ATOM_WINDOW, 32); + + // Return it + if (prop.nitems) { + p = (xcb_window_t)*prop.p32; + } + + free_winprop(&prop); + + return p; +} + +/** + * Get the value of a text property of a window. + */ +bool wid_get_text_prop(session_t *ps, xcb_window_t wid, xcb_atom_t prop, char ***pstrlst, + int *pnstr) { + assert(ps->server_grabbed); + auto prop_info = x_get_prop_info(ps->c, wid, prop); + auto type = prop_info.type; + auto format = prop_info.format; + auto length = prop_info.length; + + if (type == XCB_ATOM_NONE) { + return false; + } + + if (type != XCB_ATOM_STRING && type != ps->atoms->aUTF8_STRING && + type != ps->atoms->aC_STRING) { + log_warn("Text property %d of window %#010x has unsupported type: %d", + prop, wid, type); + return false; + } + + if (format != 8) { + log_warn("Text property %d of window %#010x has unexpected format: %d", + prop, wid, format); + return false; + } + + xcb_generic_error_t *e = NULL; + auto word_count = (length + 4 - 1) / 4; + auto r = xcb_get_property_reply( + ps->c, xcb_get_property(ps->c, 0, wid, prop, type, 0, word_count), &e); + if (!r) { + log_debug_x_error(e, "Failed to get window property for %#010x", wid); + free(e); + return false; + } + + assert(length == (uint32_t)xcb_get_property_value_length(r)); + + void *data = xcb_get_property_value(r); + unsigned int nstr = 0; + uint32_t current_offset = 0; + while (current_offset < length) { + current_offset += + (uint32_t)strnlen(data + current_offset, length - current_offset) + 1; + nstr += 1; + } + + if (nstr == 0) { + // The property is set to an empty string, in that case, we return one + // string + char **strlst = malloc(sizeof(char *)); + strlst[0] = ""; + *pnstr = 1; + *pstrlst = strlst; + free(r); + return true; + } + + // Allocate the pointers and the strings together + void *buf = NULL; + if (posix_memalign(&buf, alignof(char *), length + sizeof(char *) * nstr + 1) != 0) { + abort(); + } + + char *strlst = buf + sizeof(char *) * nstr; + memcpy(strlst, xcb_get_property_value(r), length); + strlst[length] = '\0'; // X strings aren't guaranteed to be null terminated + + char **ret = buf; + current_offset = 0; + nstr = 0; + while (current_offset < length) { + ret[nstr] = strlst + current_offset; + current_offset += (uint32_t)strlen(strlst + current_offset) + 1; + nstr += 1; + } + + *pnstr = to_int_checked(nstr); + *pstrlst = ret; + free(r); + return true; +} + +// A cache of pict formats. We assume they don't change during the lifetime +// of this program +static thread_local xcb_render_query_pict_formats_reply_t *g_pictfmts = NULL; + +static inline void x_get_server_pictfmts(xcb_connection_t *c) { + if (g_pictfmts) { + return; + } + xcb_generic_error_t *e = NULL; + // Get window picture format + g_pictfmts = + xcb_render_query_pict_formats_reply(c, xcb_render_query_pict_formats(c), &e); + if (e || !g_pictfmts) { + log_fatal("failed to get pict formats\n"); + abort(); + } +} + +const xcb_render_pictforminfo_t * +x_get_pictform_for_visual(xcb_connection_t *c, xcb_visualid_t visual) { + x_get_server_pictfmts(c); + + xcb_render_pictvisual_t *pv = xcb_render_util_find_visual_format(g_pictfmts, visual); + for (xcb_render_pictforminfo_iterator_t i = + xcb_render_query_pict_formats_formats_iterator(g_pictfmts); + i.rem; xcb_render_pictforminfo_next(&i)) { + if (i.data->id == pv->format) { + return i.data; + } + } + return NULL; +} + +static xcb_visualid_t attr_pure x_get_visual_for_pictfmt(xcb_render_query_pict_formats_reply_t *r, + xcb_render_pictformat_t fmt) { + for (auto screen = xcb_render_query_pict_formats_screens_iterator(r); screen.rem; + xcb_render_pictscreen_next(&screen)) { + for (auto depth = xcb_render_pictscreen_depths_iterator(screen.data); + depth.rem; xcb_render_pictdepth_next(&depth)) { + for (auto pv = xcb_render_pictdepth_visuals_iterator(depth.data); + pv.rem; xcb_render_pictvisual_next(&pv)) { + if (pv.data->format == fmt) { + return pv.data->visual; + } + } + } + } + return XCB_NONE; +} + +xcb_visualid_t x_get_visual_for_standard(xcb_connection_t *c, xcb_pict_standard_t std) { + x_get_server_pictfmts(c); + + auto pictfmt = xcb_render_util_find_standard_format(g_pictfmts, std); + + return x_get_visual_for_pictfmt(g_pictfmts, pictfmt->id); +} + +xcb_render_pictformat_t +x_get_pictfmt_for_standard(xcb_connection_t *c, xcb_pict_standard_t std) { + x_get_server_pictfmts(c); + + auto pictfmt = xcb_render_util_find_standard_format(g_pictfmts, std); + + return pictfmt->id; +} + +int x_get_visual_depth(xcb_connection_t *c, xcb_visualid_t visual) { + auto setup = xcb_get_setup(c); + for (auto screen = xcb_setup_roots_iterator(setup); screen.rem; + xcb_screen_next(&screen)) { + for (auto depth = xcb_screen_allowed_depths_iterator(screen.data); + depth.rem; xcb_depth_next(&depth)) { + const int len = xcb_depth_visuals_length(depth.data); + const xcb_visualtype_t *visuals = xcb_depth_visuals(depth.data); + for (int i = 0; i < len; i++) { + if (visual == visuals[i].visual_id) { + return depth.data->depth; + } + } + } + } + return -1; +} + +xcb_render_picture_t +x_create_picture_with_pictfmt_and_pixmap(xcb_connection_t *c, + const xcb_render_pictforminfo_t *pictfmt, + xcb_pixmap_t pixmap, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) { + void *buf = NULL; + if (attr) { + xcb_render_create_picture_value_list_serialize(&buf, valuemask, attr); + if (!buf) { + log_error("failed to serialize picture attributes"); + return XCB_NONE; + } + } + + xcb_render_picture_t tmp_picture = x_new_id(c); + xcb_generic_error_t *e = + xcb_request_check(c, xcb_render_create_picture_checked( + c, tmp_picture, pixmap, pictfmt->id, valuemask, buf)); + free(buf); + if (e) { + log_error_x_error(e, "failed to create picture"); + return XCB_NONE; + } + return tmp_picture; +} + +xcb_render_picture_t +x_create_picture_with_visual_and_pixmap(xcb_connection_t *c, xcb_visualid_t visual, + xcb_pixmap_t pixmap, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) { + const xcb_render_pictforminfo_t *pictfmt = x_get_pictform_for_visual(c, visual); + return x_create_picture_with_pictfmt_and_pixmap(c, pictfmt, pixmap, valuemask, attr); +} + +xcb_render_picture_t +x_create_picture_with_standard_and_pixmap(xcb_connection_t *c, xcb_pict_standard_t standard, + xcb_pixmap_t pixmap, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) { + x_get_server_pictfmts(c); + + auto pictfmt = xcb_render_util_find_standard_format(g_pictfmts, standard); + assert(pictfmt); + return x_create_picture_with_pictfmt_and_pixmap(c, pictfmt, pixmap, valuemask, attr); +} + +xcb_render_picture_t +x_create_picture_with_standard(xcb_connection_t *c, xcb_drawable_t d, int w, int h, + xcb_pict_standard_t standard, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) { + x_get_server_pictfmts(c); + + auto pictfmt = xcb_render_util_find_standard_format(g_pictfmts, standard); + assert(pictfmt); + return x_create_picture_with_pictfmt(c, d, w, h, pictfmt, valuemask, attr); +} + +/** + * Create an picture. + */ +xcb_render_picture_t +x_create_picture_with_pictfmt(xcb_connection_t *c, xcb_drawable_t d, int w, int h, + const xcb_render_pictforminfo_t *pictfmt, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) { + uint8_t depth = pictfmt->depth; + + xcb_pixmap_t tmp_pixmap = x_create_pixmap(c, depth, d, w, h); + if (!tmp_pixmap) { + return XCB_NONE; + } + + xcb_render_picture_t picture = x_create_picture_with_pictfmt_and_pixmap( + c, pictfmt, tmp_pixmap, valuemask, attr); + + xcb_free_pixmap(c, tmp_pixmap); + + return picture; +} + +xcb_render_picture_t +x_create_picture_with_visual(xcb_connection_t *c, xcb_drawable_t d, int w, int h, + xcb_visualid_t visual, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) { + auto pictfmt = x_get_pictform_for_visual(c, visual); + return x_create_picture_with_pictfmt(c, d, w, h, pictfmt, valuemask, attr); +} + +bool x_fetch_region(xcb_connection_t *c, xcb_xfixes_region_t r, pixman_region32_t *res) { + xcb_generic_error_t *e = NULL; + xcb_xfixes_fetch_region_reply_t *xr = + xcb_xfixes_fetch_region_reply(c, xcb_xfixes_fetch_region(c, r), &e); + if (!xr) { + log_error_x_error(e, "Failed to fetch rectangles"); + return false; + } + + int nrect = xcb_xfixes_fetch_region_rectangles_length(xr); + auto b = ccalloc(nrect, pixman_box32_t); + xcb_rectangle_t *xrect = xcb_xfixes_fetch_region_rectangles(xr); + for (int i = 0; i < nrect; i++) { + b[i] = (pixman_box32_t){.x1 = xrect[i].x, + .y1 = xrect[i].y, + .x2 = xrect[i].x + xrect[i].width, + .y2 = xrect[i].y + xrect[i].height}; + } + bool ret = pixman_region32_init_rects(res, b, nrect); + free(b); + free(xr); + return ret; +} + +void x_set_picture_clip_region(xcb_connection_t *c, xcb_render_picture_t pict, + int16_t clip_x_origin, int16_t clip_y_origin, + const region_t *reg) { + int nrects; + const rect_t *rects = pixman_region32_rectangles((region_t *)reg, &nrects); + auto xrects = ccalloc(nrects, xcb_rectangle_t); + for (int i = 0; i < nrects; i++) { + xrects[i] = (xcb_rectangle_t){ + .x = to_i16_checked(rects[i].x1), + .y = to_i16_checked(rects[i].y1), + .width = to_u16_checked(rects[i].x2 - rects[i].x1), + .height = to_u16_checked(rects[i].y2 - rects[i].y1), + }; + } + + xcb_generic_error_t *e = xcb_request_check( + c, xcb_render_set_picture_clip_rectangles_checked( + c, pict, clip_x_origin, clip_y_origin, to_u32_checked(nrects), xrects)); + if (e) { + log_error_x_error(e, "Failed to set clip region"); + free(e); + } + free(xrects); +} + +void x_clear_picture_clip_region(xcb_connection_t *c, xcb_render_picture_t pict) { + xcb_render_change_picture_value_list_t v = {.clipmask = XCB_NONE}; + xcb_generic_error_t *e = xcb_request_check( + c, xcb_render_change_picture(c, pict, XCB_RENDER_CP_CLIP_MASK, &v)); + if (e) { + log_error_x_error(e, "failed to clear clip region"); + free(e); + } +} + +enum { + XSyncBadCounter = 0, + XSyncBadAlarm = 1, + XSyncBadFence = 2, +}; + +/** + * Convert a X11 error to string + * + * @return a pointer to a string. this pointer shouldn NOT be freed, same buffer is used + * for multiple calls to this function, + */ +static const char * +_x_strerror(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_code) { + session_t *const ps = ps_g; + + int o = 0; + const char *name = "Unknown"; + +#define CASESTRRET(s) \ + case s: \ + name = #s; \ + break + +#define CASESTRRET2(s) \ + case XCB_##s: name = #s; break + + // TODO(yshui) separate error code out from session_t + o = error_code - ps->xfixes_error; + switch (o) { CASESTRRET2(XFIXES_BAD_REGION); } + + o = error_code - ps->damage_error; + switch (o) { CASESTRRET2(DAMAGE_BAD_DAMAGE); } + + o = error_code - ps->render_error; + switch (o) { + CASESTRRET2(RENDER_PICT_FORMAT); + CASESTRRET2(RENDER_PICTURE); + CASESTRRET2(RENDER_PICT_OP); + CASESTRRET2(RENDER_GLYPH_SET); + CASESTRRET2(RENDER_GLYPH); + } + + if (ps->glx_exists) { + o = error_code - ps->glx_error; + switch (o) { + CASESTRRET2(GLX_BAD_CONTEXT); + CASESTRRET2(GLX_BAD_CONTEXT_STATE); + CASESTRRET2(GLX_BAD_DRAWABLE); + CASESTRRET2(GLX_BAD_PIXMAP); + CASESTRRET2(GLX_BAD_CONTEXT_TAG); + CASESTRRET2(GLX_BAD_CURRENT_WINDOW); + CASESTRRET2(GLX_BAD_RENDER_REQUEST); + CASESTRRET2(GLX_BAD_LARGE_REQUEST); + CASESTRRET2(GLX_UNSUPPORTED_PRIVATE_REQUEST); + CASESTRRET2(GLX_BAD_FB_CONFIG); + CASESTRRET2(GLX_BAD_PBUFFER); + CASESTRRET2(GLX_BAD_CURRENT_DRAWABLE); + CASESTRRET2(GLX_BAD_WINDOW); + CASESTRRET2(GLX_GLX_BAD_PROFILE_ARB); + } + } + + if (ps->xsync_exists) { + o = error_code - ps->xsync_error; + switch (o) { + CASESTRRET(XSyncBadCounter); + CASESTRRET(XSyncBadAlarm); + CASESTRRET(XSyncBadFence); + } + } + + switch (error_code) { + CASESTRRET2(ACCESS); + CASESTRRET2(ALLOC); + CASESTRRET2(ATOM); + CASESTRRET2(COLORMAP); + CASESTRRET2(CURSOR); + CASESTRRET2(DRAWABLE); + CASESTRRET2(FONT); + CASESTRRET2(G_CONTEXT); + CASESTRRET2(ID_CHOICE); + CASESTRRET2(IMPLEMENTATION); + CASESTRRET2(LENGTH); + CASESTRRET2(MATCH); + CASESTRRET2(NAME); + CASESTRRET2(PIXMAP); + CASESTRRET2(REQUEST); + CASESTRRET2(VALUE); + CASESTRRET2(WINDOW); + } + +#undef CASESTRRET +#undef CASESTRRET2 + + thread_local static char buffer[256]; + snprintf(buffer, sizeof(buffer), "X error %d %s request %d minor %d serial %lu", + error_code, name, major, minor, serial); + return buffer; +} + +/** + * Log a X11 error + */ +void x_print_error(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_code) { + log_debug("%s", _x_strerror(serial, major, minor, error_code)); +} + +/* + * Convert a xcb_generic_error_t to a string that describes the error + * + * @return a pointer to a string. this pointer shouldn NOT be freed, same buffer is used + * for multiple calls to this function, + */ +const char *x_strerror(xcb_generic_error_t *e) { + if (!e) { + return "No error"; + } + return _x_strerror(e->full_sequence, e->major_code, e->minor_code, e->error_code); +} + +/** + * Create a pixmap and check that creation succeeded. + */ +xcb_pixmap_t x_create_pixmap(xcb_connection_t *c, uint8_t depth, xcb_drawable_t drawable, + int width, int height) { + xcb_pixmap_t pix = x_new_id(c); + xcb_void_cookie_t cookie = xcb_create_pixmap_checked( + c, depth, pix, drawable, to_u16_checked(width), to_u16_checked(height)); + xcb_generic_error_t *err = xcb_request_check(c, cookie); + if (err == NULL) { + return pix; + } + + log_error_x_error(err, "Failed to create pixmap"); + free(err); + return XCB_NONE; +} + +/** + * Validate a pixmap. + * + * Detect whether the pixmap is valid with XGetGeometry. Well, maybe there + * are better ways. + */ +bool x_validate_pixmap(xcb_connection_t *c, xcb_pixmap_t pixmap) { + if (pixmap == XCB_NONE) { + return false; + } + + auto r = xcb_get_geometry_reply(c, xcb_get_geometry(c, pixmap), NULL); + if (!r) { + return false; + } + + bool ret = r->width && r->height; + free(r); + return ret; +} +/// Names of root window properties that could point to a pixmap of +/// background. +static const char *background_props_str[] = { + "_XROOTPMAP_ID", + "_XSETROOT_ID", + 0, +}; + +xcb_pixmap_t +x_get_root_back_pixmap(xcb_connection_t *c, xcb_window_t root, struct atom *atoms) { + xcb_pixmap_t pixmap = XCB_NONE; + + // Get the values of background attributes + for (int p = 0; background_props_str[p]; p++) { + xcb_atom_t prop_atom = get_atom(atoms, background_props_str[p]); + winprop_t prop = x_get_prop(c, root, prop_atom, 1, XCB_ATOM_PIXMAP, 32); + if (prop.nitems) { + pixmap = (xcb_pixmap_t)*prop.p32; + free_winprop(&prop); + break; + } + free_winprop(&prop); + } + + return pixmap; +} + +bool x_is_root_back_pixmap_atom(struct atom *atoms, xcb_atom_t atom) { + for (int p = 0; background_props_str[p]; p++) { + xcb_atom_t prop_atom = get_atom(atoms, background_props_str[p]); + if (prop_atom == atom) { + return true; + } + } + return false; +} + +/** + * Synchronizes a X Render drawable to ensure all pending painting requests + * are completed. + */ +bool x_fence_sync(xcb_connection_t *c, xcb_sync_fence_t f) { + // TODO(richardgv): If everybody just follows the rules stated in X Sync + // prototype, we need only one fence per screen, but let's stay a bit + // cautious right now + + auto e = xcb_request_check(c, xcb_sync_trigger_fence_checked(c, f)); + if (e) { + log_error_x_error(e, "Failed to trigger the fence"); + goto err; + } + + e = xcb_request_check(c, xcb_sync_await_fence_checked(c, 1, &f)); + if (e) { + log_error_x_error(e, "Failed to await on a fence"); + goto err; + } + + e = xcb_request_check(c, xcb_sync_reset_fence_checked(c, f)); + if (e) { + log_error_x_error(e, "Failed to reset the fence"); + goto err; + } + return true; + +err: + free(e); + return false; +} + +// xcb-render specific macros +#define XFIXED_TO_DOUBLE(value) (((double)(value)) / 65536) +#define DOUBLE_TO_XFIXED(value) ((xcb_render_fixed_t)(((double)(value)) * 65536)) + +/** + * Convert a struct conv to a X picture convolution filter, normalizing the kernel + * in the process. Allow the caller to specify the element at the center of the kernel, + * for compatibility with legacy code. + * + * @param[in] kernel the convolution kernel + * @param[in] center the element to put at the center of the matrix + * @param[inout] ret pointer to an array of `size`, if `size` is too small, more space + * will be allocated, and `*ret` will be updated + * @param[inout] size size of the array pointed to by `ret`, in number of elements + * @return number of elements filled into `*ret` + */ +void x_create_convolution_kernel(const conv *kernel, double center, + struct x_convolution_kernel **ret) { + assert(ret); + if (!*ret || (*ret)->capacity < kernel->w * kernel->h + 2) { + free(*ret); + *ret = + cvalloc(sizeof(struct x_convolution_kernel) + + (size_t)(kernel->w * kernel->h + 2) * sizeof(xcb_render_fixed_t)); + (*ret)->capacity = kernel->w * kernel->h + 2; + } + + (*ret)->size = kernel->w * kernel->h + 2; + + auto buf = (*ret)->kernel; + buf[0] = DOUBLE_TO_XFIXED(kernel->w); + buf[1] = DOUBLE_TO_XFIXED(kernel->h); + + double sum = center; + for (int i = 0; i < kernel->w * kernel->h; i++) { + if (i == kernel->w * kernel->h / 2) { + continue; + } + sum += kernel->data[i]; + } + + // Note for floating points a / b != a * (1 / b), but this shouldn't have any real + // impact on the result + double factor = sum != 0 ? 1.0 / sum : 1; + for (int i = 0; i < kernel->w * kernel->h; i++) { + buf[i + 2] = DOUBLE_TO_XFIXED(kernel->data[i] * factor); + } + + buf[kernel->h / 2 * kernel->w + kernel->w / 2 + 2] = + DOUBLE_TO_XFIXED(center * factor); +} + +/// Generate a search criteria for fbconfig from a X visual. +/// Returns {-1, -1, -1, -1, -1, 0} on failure +struct xvisual_info x_get_visual_info(xcb_connection_t *c, xcb_visualid_t visual) { + auto pictfmt = x_get_pictform_for_visual(c, visual); + auto depth = x_get_visual_depth(c, visual); + if (!pictfmt || depth == -1) { + log_error("Invalid visual %#03x", visual); + return (struct xvisual_info){-1, -1, -1, -1, -1, 0}; + } + if (pictfmt->type != XCB_RENDER_PICT_TYPE_DIRECT) { + log_error("We cannot handle non-DirectColor visuals. Report an " + "issue if you see this error message."); + return (struct xvisual_info){-1, -1, -1, -1, -1, 0}; + } + + int red_size = popcntul(pictfmt->direct.red_mask), + blue_size = popcntul(pictfmt->direct.blue_mask), + green_size = popcntul(pictfmt->direct.green_mask), + alpha_size = popcntul(pictfmt->direct.alpha_mask); + + return (struct xvisual_info){ + .red_size = red_size, + .green_size = green_size, + .blue_size = blue_size, + .alpha_size = alpha_size, + .visual_depth = depth, + .visual = visual, + }; +} + +xcb_screen_t *x_screen_of_display(xcb_connection_t *c, int screen) { + xcb_screen_iterator_t iter; + + iter = xcb_setup_roots_iterator(xcb_get_setup(c)); + for (; iter.rem; --screen, xcb_screen_next(&iter)) { + if (screen == 0) { + return iter.data; + } + } + + return NULL; +} @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2018 Yuxuan Shui <[email protected]> +#pragma once +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <xcb/render.h> +#include <xcb/sync.h> +#include <xcb/xcb.h> +#include <xcb/xcb_renderutil.h> +#include <xcb/xfixes.h> + +#include "compiler.h" +#include "kernel.h" +#include "log.h" +#include "region.h" + +typedef struct session session_t; +struct atom; + +/// Structure representing Window property value. +typedef struct winprop { + union { + void *ptr; + int8_t *p8; + int16_t *p16; + int32_t *p32; + uint32_t *c32; // 32bit cardinal + }; + unsigned long nitems; + xcb_atom_t type; + int format; + + xcb_get_property_reply_t *r; +} winprop_t; + +typedef struct winprop_info { + xcb_atom_t type; + uint8_t format; + uint32_t length; +} winprop_info_t; + +struct xvisual_info { + /// Bit depth of the red component + int red_size; + /// Bit depth of the green component + int green_size; + /// Bit depth of the blue component + int blue_size; + /// Bit depth of the alpha component + int alpha_size; + /// The depth of X visual + int visual_depth; + + xcb_visualid_t visual; +}; + +#define XCB_AWAIT_VOID(func, c, ...) \ + ({ \ + bool __success = true; \ + __auto_type __e = xcb_request_check(c, func##_checked(c, __VA_ARGS__)); \ + if (__e) { \ + x_print_error(__e->sequence, __e->major_code, __e->minor_code, \ + __e->error_code); \ + free(__e); \ + __success = false; \ + } \ + __success; \ + }) + +#define XCB_AWAIT(func, c, ...) \ + ({ \ + xcb_generic_error_t *__e = NULL; \ + __auto_type __r = func##_reply(c, func(c, __VA_ARGS__), &__e); \ + if (__e) { \ + x_print_error(__e->sequence, __e->major_code, __e->minor_code, \ + __e->error_code); \ + free(__e); \ + } \ + __r; \ + }) + +#define log_debug_x_error(e, fmt, ...) \ + LOG(DEBUG, fmt " (%s)", ##__VA_ARGS__, x_strerror(e)) +#define log_error_x_error(e, fmt, ...) \ + LOG(ERROR, fmt " (%s)", ##__VA_ARGS__, x_strerror(e)) +#define log_fatal_x_error(e, fmt, ...) \ + LOG(FATAL, fmt " (%s)", ##__VA_ARGS__, x_strerror(e)) + +/// Wraps x_new_id. abort the program if x_new_id returns error +static inline uint32_t x_new_id(xcb_connection_t *c) { + auto ret = xcb_generate_id(c); + if (ret == (uint32_t)-1) { + log_fatal("We seems to have run of XIDs. This is either a bug in the X " + "server, or a resource leakage in the compositor. Please open " + "an issue about this problem. The compositor will die."); + abort(); + } + return ret; +} + +/** + * Send a request to X server and get the reply to make sure all previous + * requests are processed, and their replies received + * + * xcb_get_input_focus is used here because it is the same request used by + * libX11 + */ +static inline void x_sync(xcb_connection_t *c) { + free(xcb_get_input_focus_reply(c, xcb_get_input_focus(c), NULL)); +} + +/** + * Get a specific attribute of a window. + * + * Returns a blank structure if the returned type and format does not + * match the requested type and format. + * + * @param ps current session + * @param w window + * @param atom atom of attribute to fetch + * @param length length to read + * @param rtype atom of the requested type + * @param rformat requested format + * @return a <code>winprop_t</code> structure containing the attribute + * and number of items. A blank one on failure. + */ +winprop_t x_get_prop_with_offset(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom, + int offset, int length, xcb_atom_t rtype, int rformat); + +/** + * Wrapper of wid_get_prop_adv(). + */ +static inline winprop_t x_get_prop(xcb_connection_t *c, xcb_window_t wid, xcb_atom_t atom, + int length, xcb_atom_t rtype, int rformat) { + return x_get_prop_with_offset(c, wid, atom, 0L, length, rtype, rformat); +} + +/// Get the type, format and size in bytes of a window's specific attribute. +winprop_info_t x_get_prop_info(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom); + +/// Discard all X events in queue or in flight. Should only be used when the server is +/// grabbed +static inline void x_discard_events(xcb_connection_t *c) { + xcb_generic_event_t *e; + while ((e = xcb_poll_for_event(c))) { + free(e); + } +} + +/** + * Get the value of a type-<code>xcb_window_t</code> property of a window. + * + * @return the value if successful, 0 otherwise + */ +xcb_window_t wid_get_prop_window(xcb_connection_t *c, xcb_window_t wid, xcb_atom_t aprop); + +/** + * Get the value of a text property of a window. + * + * @param[out] pstrlst Out parameter for an array of strings, caller needs to free this + * array + * @param[out] pnstr Number of strings in the array + */ +bool wid_get_text_prop(session_t *ps, xcb_window_t wid, xcb_atom_t prop, char ***pstrlst, + int *pnstr); + +const xcb_render_pictforminfo_t * +x_get_pictform_for_visual(xcb_connection_t *, xcb_visualid_t); +int x_get_visual_depth(xcb_connection_t *, xcb_visualid_t); + +xcb_render_picture_t +x_create_picture_with_pictfmt_and_pixmap(xcb_connection_t *, + const xcb_render_pictforminfo_t *pictfmt, + xcb_pixmap_t pixmap, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) + attr_nonnull(1, 2); + +xcb_render_picture_t +x_create_picture_with_visual_and_pixmap(xcb_connection_t *, xcb_visualid_t visual, + xcb_pixmap_t pixmap, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) + attr_nonnull(1); + +xcb_render_picture_t +x_create_picture_with_standard_and_pixmap(xcb_connection_t *, xcb_pict_standard_t standard, + xcb_pixmap_t pixmap, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) + attr_nonnull(1); + +xcb_render_picture_t +x_create_picture_with_standard(xcb_connection_t *c, xcb_drawable_t d, int w, int h, + xcb_pict_standard_t standard, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) + attr_nonnull(1); + +/** + * Create an picture. + */ +xcb_render_picture_t +x_create_picture_with_pictfmt(xcb_connection_t *, xcb_drawable_t, int w, int h, + const xcb_render_pictforminfo_t *pictfmt, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) + attr_nonnull(1, 5); + +xcb_render_picture_t +x_create_picture_with_visual(xcb_connection_t *, xcb_drawable_t, int w, int h, + xcb_visualid_t visual, uint32_t valuemask, + const xcb_render_create_picture_value_list_t *attr) + attr_nonnull(1); + +/// Fetch a X region and store it in a pixman region +bool x_fetch_region(xcb_connection_t *, xcb_xfixes_region_t r, region_t *res); + +void x_set_picture_clip_region(xcb_connection_t *, xcb_render_picture_t, int16_t clip_x_origin, + int16_t clip_y_origin, const region_t *); + +void x_clear_picture_clip_region(xcb_connection_t *, xcb_render_picture_t pict); + +/** + * Log a X11 error + */ +void x_print_error(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_code); + +/* + * Convert a xcb_generic_error_t to a string that describes the error + * + * @return a pointer to a string. this pointer shouldn NOT be freed, same buffer is used + * for multiple calls to this function, + */ +const char *x_strerror(xcb_generic_error_t *e); + +xcb_pixmap_t x_create_pixmap(xcb_connection_t *, uint8_t depth, xcb_drawable_t drawable, + int width, int height); + +bool x_validate_pixmap(xcb_connection_t *, xcb_pixmap_t pxmap); + +/** + * Free a <code>winprop_t</code>. + * + * @param pprop pointer to the <code>winprop_t</code> to free. + */ +static inline void free_winprop(winprop_t *pprop) { + // Empty the whole structure to avoid possible issues + if (pprop->r) + free(pprop->r); + pprop->ptr = NULL; + pprop->r = NULL; + pprop->nitems = 0; +} + +/// Get the back pixmap of the root window +xcb_pixmap_t +x_get_root_back_pixmap(xcb_connection_t *c, xcb_window_t root, struct atom *atoms); + +/// Return true if the atom refers to a property name that is used for the +/// root window background pixmap +bool x_is_root_back_pixmap_atom(struct atom *atoms, xcb_atom_t atom); + +bool x_fence_sync(xcb_connection_t *, xcb_sync_fence_t); + +struct x_convolution_kernel { + int size; + int capacity; + xcb_render_fixed_t kernel[]; +}; + +/** + * Convert a struct conv to a X picture convolution filter, normalizing the kernel + * in the process. Allow the caller to specify the element at the center of the kernel, + * for compatibility with legacy code. + * + * @param[in] kernel the convolution kernel + * @param[in] center the element to put at the center of the matrix + * @param[inout] ret pointer to an array of `size`, if `size` is too small, more space + * will be allocated, and `*ret` will be updated. + * @param[inout] size size of the array pointed to by `ret`. + */ +void attr_nonnull(1, 3) x_create_convolution_kernel(const conv *kernel, double center, + struct x_convolution_kernel **ret); + +/// Generate a search criteria for fbconfig from a X visual. +/// Returns {-1, -1, -1, -1, -1, -1} on failure +struct xvisual_info x_get_visual_info(xcb_connection_t *c, xcb_visualid_t visual); + +xcb_visualid_t x_get_visual_for_standard(xcb_connection_t *c, xcb_pict_standard_t std); + +xcb_render_pictformat_t +x_get_pictfmt_for_standard(xcb_connection_t *c, xcb_pict_standard_t std); + +xcb_screen_t *x_screen_of_display(xcb_connection_t *c, int screen); + +uint32_t attr_deprecated xcb_generate_id(xcb_connection_t *c); diff --git a/src/xrescheck.c b/src/xrescheck.c new file mode 100644 index 0000000..1785fc8 --- /dev/null +++ b/src/xrescheck.c @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2014 Richard Grenville <[email protected]> + +#include "compiler.h" +#include "log.h" + +#include "xrescheck.h" + +static xrc_xid_record_t *gs_xid_records = NULL; + +#define HASH_ADD_XID(head, xidfield, add) HASH_ADD(hh, head, xidfield, sizeof(xid), add) + +#define HASH_FIND_XID(head, findxid, out) HASH_FIND(hh, head, findxid, sizeof(xid), out) + +#define M_CPY_POS_DATA(prec) \ + prec->file = file; \ + prec->func = func; \ + prec->line = line; + +/** + * @brief Add a record of given XID to the allocation table. + */ +void xrc_add_xid_(XID xid, const char *type, M_POS_DATA_PARAMS) { + auto prec = ccalloc(1, xrc_xid_record_t); + prec->xid = xid; + prec->type = type; + M_CPY_POS_DATA(prec); + + HASH_ADD_XID(gs_xid_records, xid, prec); +} + +/** + * @brief Delete a record of given XID in the allocation table. + */ +void xrc_delete_xid_(XID xid, M_POS_DATA_PARAMS) { + xrc_xid_record_t *prec = NULL; + HASH_FIND_XID(gs_xid_records, &xid, prec); + if (!prec) { + log_error("XRC: %s:%d %s(): Can't find XID %#010lx we want to delete.", + file, line, func, xid); + return; + } + HASH_DEL(gs_xid_records, prec); + free(prec); +} + +/** + * @brief Report about issues found in the XID allocation table. + */ +void xrc_report_xid(void) { + for (xrc_xid_record_t *prec = gs_xid_records; prec; prec = prec->hh.next) + log_trace("XRC: %s:%d %s(): %#010lx (%s) not freed.\n", prec->file, + prec->line, prec->func, prec->xid, prec->type); +} + +/** + * @brief Clear the XID allocation table. + */ +void xrc_clear_xid(void) { + xrc_xid_record_t *prec = NULL, *ptmp = NULL; + HASH_ITER(hh, gs_xid_records, prec, ptmp) { + HASH_DEL(gs_xid_records, prec); + free(prec); + } +} diff --git a/src/xrescheck.h b/src/xrescheck.h new file mode 100644 index 0000000..5ad5c46 --- /dev/null +++ b/src/xrescheck.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2014 Richard Grenville <[email protected]> +#pragma once + +#include "common.h" +#include "uthash.h" + +typedef struct { + XID xid; + const char *type; + const char *file; + const char *func; + int line; + UT_hash_handle hh; +} xrc_xid_record_t; + +#define M_POS_DATA_PARAMS const char *file, int line, const char *func +#define M_POS_DATA_PASSTHROUGH file, line, func +#define M_POS_DATA __FILE__, __LINE__, __func__ + +void xrc_add_xid_(XID xid, const char *type, M_POS_DATA_PARAMS); + +#define xrc_add_xid(xid, type) xrc_add_xid_(xid, type, M_POS_DATA) + +void xrc_delete_xid_(XID xid, M_POS_DATA_PARAMS); + +#define xrc_delete_xid(xid) xrc_delete_xid_(xid, M_POS_DATA) + +void xrc_report_xid(void); + +void xrc_clear_xid(void); + +// Pixmap + +static inline void xcb_create_pixmap_(xcb_connection_t *c, uint8_t depth, + xcb_pixmap_t pixmap, xcb_drawable_t drawable, + uint16_t width, uint16_t height, M_POS_DATA_PARAMS) { + xcb_create_pixmap(c, depth, pixmap, drawable, width, height); + xrc_add_xid_(pixmap, "Pixmap", M_POS_DATA_PASSTHROUGH); +} + +#define xcb_create_pixmap(c, depth, pixmap, drawable, width, height) \ + xcb_create_pixmap_(c, depth, pixmap, drawable, width, height, M_POS_DATA) + +static inline xcb_void_cookie_t +xcb_composite_name_window_pixmap_(xcb_connection_t *c, xcb_window_t window, + xcb_pixmap_t pixmap, M_POS_DATA_PARAMS) { + xcb_void_cookie_t ret = xcb_composite_name_window_pixmap(c, window, pixmap); + xrc_add_xid_(pixmap, "PixmapC", M_POS_DATA_PASSTHROUGH); + return ret; +} + +#define xcb_composite_name_window_pixmap(dpy, window, pixmap) \ + xcb_composite_name_window_pixmap_(dpy, window, pixmap, M_POS_DATA) + +static inline void +xcb_free_pixmap_(xcb_connection_t *c, xcb_pixmap_t pixmap, M_POS_DATA_PARAMS) { + xcb_free_pixmap(c, pixmap); + xrc_delete_xid_(pixmap, M_POS_DATA_PASSTHROUGH); +} + +#define xcb_free_pixmap(c, pixmap) xcb_free_pixmap_(c, pixmap, M_POS_DATA); |