From 8b4dd043ed39b0b0689a11795b87913587d1e255 Mon Sep 17 00:00:00 2001 From: Ernesto Cruz Date: Fri, 3 Jul 2026 03:49:58 +0100 Subject: [PATCH] feat: custom GLSL shader transitions for window open/close --- docs/visuals/meta.json | 2 +- docs/visuals/shaders.md | 121 ++++++++ meson.build | 6 +- src/animation/client.h | 163 ++++++++++- src/animation/tag.h | 3 +- src/config/parse_config.h | 22 ++ src/draw/effect_pass.c | 562 ++++++++++++++++++++++++++++++++++++++ src/draw/effect_pass.h | 29 ++ src/mango.c | 70 +++++ 9 files changed, 970 insertions(+), 8 deletions(-) create mode 100644 docs/visuals/shaders.md create mode 100644 src/draw/effect_pass.c create mode 100644 src/draw/effect_pass.h diff --git a/docs/visuals/meta.json b/docs/visuals/meta.json index 58723c4e..4cc938a4 100644 --- a/docs/visuals/meta.json +++ b/docs/visuals/meta.json @@ -1,4 +1,4 @@ { "title": "Visuals", - "pages": ["theming", "status-bar", "effects", "animations"] + "pages": ["theming", "status-bar", "effects", "animations", "shaders"] } diff --git a/docs/visuals/shaders.md b/docs/visuals/shaders.md new file mode 100644 index 00000000..2fc27c82 --- /dev/null +++ b/docs/visuals/shaders.md @@ -0,0 +1,121 @@ +--- +title: Shader Transitions +description: Write custom GLSL fragment shaders for window open/close animations. +--- + +mangowm can drive a window's open and close animation with a custom GLSL fragment shader instead of (or alongside) the built-in parametric fades and zooms. Each window is snapshotted to a texture once, then your shader runs every animation tick against that texture to produce the visible frame. + +This is a small, focused feature. It only covers the open and close transitions (not move/tag/focus animations), and shader files are only discovered at startup and after a GPU reset. There is no hot-reload while the compositor is running. + +## Quick start + +1. Drop a `.frag` file in `~/.config/mango/shaders/`, e.g. `dissolve.frag`. +2. Reference it by filename (without the `.frag` extension) in your config: + +```ini +effect_open=dissolve +effect_close=burn +``` + +3. Restart mangowm. Shader files are scanned once at startup and again after a GPU reset. Adding or editing a `.frag` file while the compositor is already running has no effect until the next restart. + +## Writing a shader + +A shader file is just the body of a GLSL ES 3.00 fragment shader, typically a single `void main() { ... }` function. The loader wraps your body with a fixed header before compiling it, equivalent to: + +```glsl +#version 300 es +precision highp float; +uniform sampler2D u_texture; +uniform float u_progress; +uniform float u_time; +uniform vec2 u_size; +in vec2 v_texcoord; +out vec4 fragColor; +``` + +Do not redeclare any of these names in your file: `u_texture`, `u_progress`, `u_time`, `u_size`, `v_texcoord`, or `fragColor`. The header already declares them. Redeclaring any of them is a GLSL compile error and your shader will fail to load (see [Fail-safe behavior](#fail-safe-behavior)). + +| Name | Type | Meaning | +| :--- | :--- | :--- | +| `u_texture` | `sampler2D` | A snapshot of the window's contents, sampled with `texture(u_texture, v_texcoord)`. | +| `u_progress` | `float` | Animation progress, `0.0` to `1.0`. See [Progress convention](#progress-convention) below. This is the one thing every shader must get right. | +| `u_time` | `float` | Seconds elapsed since the animation started. Useful for time-based effects (e.g. a glitch shimmer) independent of progress. | +| `u_size` | `vec2` | Output size in pixels (the window width and height being rendered into). | +| `v_texcoord` | `vec2` | Interpolated texture coordinate, `0.0` to `1.0` across the window. | +| `fragColor` | `out vec4` | Your shader's output color for the pixel. Write to this instead of `gl_FragColor`. | + +Your shader writes its output to `fragColor`, which the header declares as `out vec4`. Since the engine composites the result with premultiplied alpha, the RGB channels you write must already be multiplied by the alpha you write. That means `fragColor = vec4(color.rgb * alpha, alpha)`, not `vec4(color.rgb, alpha)`. Forgetting this produces a white or light halo around fading-out windows. + +### GLSL ES 3.00 + +The shader is compiled as GLSL ES 3.00. The loader prepends the `#version 300 es` line and the header above, so you write only the body: + +- Sample textures with `texture(sampler, uv)`, not `texture2D(...)`. +- Write the output to `fragColor`, not `gl_FragColor`. The `out` variable is already declared in the header, so do not declare your own. +- Read the interpolated coordinate from `v_texcoord` (declared `in` by the header). Inputs from the vertex stage use `in`, not `varying`. +- No redeclaring `u_texture` / `u_progress` / `u_time` / `u_size` / `v_texcoord` / `fragColor` (see above). + +### Precision: highp is guaranteed + +GLSL ES 3.00 fragment shaders are guaranteed `highp` (fp32) floats. The classic noise hash works with no workaround: + +```glsl +float hash12(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} +``` + +You do not need the multi-round fp16-safe hashes found in older GLES2 shader code, and you do not need `GL_FRAGMENT_PRECISION_HIGH` guards. `mediump` is still available if you want it for a hot loop, but the default `highp` is fine everywhere. + +## Progress convention + +Write your shader so `u_progress == 0.0` means the window is fully present and `u_progress == 1.0` means the window is fully gone. The compositor runs progress in the right direction for both open and close, so a single shader body works for both: + +- **Close**: progress is driven directly from the close animation's elapsed fraction (`progress = min(animation_passed, 1.0)`), so it runs `0 -> 1` as the window closes: present, then gone. +- **Open**: progress is driven from `1.0 - min(animation_passed, 1.0)`, so it runs `1 -> 0` as the window opens: gone, then present. + +The engine inverts progress for you on open. You never need two different shaders (or an `if` on direction) for the open vs. close case. One body covering `progress: 0 = present, 1 = gone` dissolves a window away on close and dissolves it in on open. + +## Configuration + +| Setting | Scope | Description | +| :--- | :--- | :--- | +| `effect_open` | global | Name (no `.frag`) of the shader to use for window open animations. | +| `effect_close` | global | Name (no `.frag`) of the shader to use for window close animations. | + +```ini +effect_open=dissolve +effect_close=burn +``` + +Both can also be set per-window via a `windowrule`, which takes precedence over the global setting for matching windows: + +```ini +windowrule=appid:firefox,effect_close:glitch +``` + +If `effect_open`/`effect_close` is left unset (globally and per-window), or the window doesn't match a rule, the window uses the existing parametric animation (`animation_type_open`/`animation_type_close`, fade, zoom, etc.) exactly as before this feature existed. Shaders are strictly opt-in. + +## Fail-safe behavior + +Shader loading and selection are designed to never break your session: + +- **Compile/link failure**: if a `.frag` file fails to compile or link, the loader logs an error (visible with `WLR_ERROR` logging) and skips that file. It is never registered. Other shaders in the directory still load normally. +- **Unknown shader name**: if `effect_open`/`effect_close` (global or per-window) names a shader that was never successfully loaded (typo, compile failure, or you just haven't created the file), that window silently falls back to the normal parametric open/close animation. No crash, no missing window content. It just behaves as if the shader setting weren't there. +- **Missing shader directory**: if `~/.config/mango/shaders/` doesn't exist, it's logged at `INFO` level and skipped. Having no custom shaders at all is a normal, supported configuration. + +## Restart required for new files + +`~/.config/mango/shaders/` is scanned for `*.frag` files once at startup, and again automatically after a GPU reset (which also clears all previously compiled shader programs). There is no file-watching or hot-reload. If you add a new `.frag` file or rename one while mangowm is running, you need to restart the compositor before `effect_open` / `effect_close` can reference it. Editing the contents of an already-loaded shader also requires a restart to take effect. + +## Example shaders + +Example shaders live in a separate repo, [ernestoCruz05/shader_examples](https://github.com/ernestoCruz05/shader_examples). Clone it and copy whichever `.frag` files you want into your shader directory: + +```sh +git clone https://github.com/ernestoCruz05/shader_examples.git +cp shader_examples/*.frag ~/.config/mango/shaders/ +``` + +Then restart mangowm and reference them by basename (without the `.frag` extension) in your config (see [Quick start](#quick-start)). diff --git a/meson.build b/meson.build index 938371e5..29d63eac 100644 --- a/meson.build +++ b/meson.build @@ -39,6 +39,8 @@ libscenefx_dep = dependency('scenefx-0.5',version: '>=0.5.0') pixman_dep = dependency('pixman-1') cjson_dep = dependency('libcjson') pangocairo_dep = dependency('pangocairo') +egl_dep = dependency('egl') +glesv2_dep = dependency('glesv2') # version info git = find_program('git', required : false) @@ -97,7 +99,7 @@ executable('mango', 'src/mango.c', 'src/common/util.c', 'src/draw/text-node.c', - + 'src/draw/effect_pass.c', wayland_sources, dependencies : [ libm, @@ -114,6 +116,8 @@ executable('mango', pixman_dep, cjson_dep, pangocairo_dep, + egl_dep, + glesv2_dep, ], install : true, c_args : c_args, diff --git a/src/animation/client.h b/src/animation/client.h index 555922c4..643e9194 100644 --- a/src/animation/client.h +++ b/src/animation/client.h @@ -9,6 +9,11 @@ void set_rect_size(struct wlr_scene_rect *rect, int32_t width, int32_t height) { wlr_scene_rect_set_size(rect, GEZERO(width), GEZERO(height)); } +static void client_surface_set_enabled(Client *c, bool enabled) { + if (!c->fx.active && c->scene_surface) + wlr_scene_node_set_enabled(&c->scene_surface->node, enabled); +} + struct fx_corner_radii set_client_corner_location(Client *c) { struct fx_corner_radii current_corner_location = corner_radii_all(config.border_radius); @@ -1049,6 +1054,7 @@ void client_apply_clip(Client *c, float factor) { set_client_corner_location(c); if (!config.animations && !c->overview_scene_surface) { + client_fx_settle(c); c->animation.running = false; c->need_output_flush = false; c->animainit_geom = c->current = c->pending = c->animation.current = @@ -1119,10 +1125,10 @@ void client_apply_clip(Client *c, float factor) { // 如果窗口剪切区域已经剪切到0,则不渲染窗口表面 if (clip_box.width <= 0 || clip_box.height <= 0) { should_render_client_surface = false; - wlr_scene_node_set_enabled(&c->scene_surface->node, false); + client_surface_set_enabled(c, false); } else { should_render_client_surface = true; - wlr_scene_node_set_enabled(&c->scene_surface->node, true); + client_surface_set_enabled(c, true); } // 不用在执行下面的窗口表面剪切和缩放等效果操作 @@ -1223,7 +1229,10 @@ void fadeout_client_animation_next_tick(Client *c) { &c->scene->node, snap_scene_buffer_apply_effect, &buffer_data); } + client_fx_tick(c, MANGO_MIN(animation_passed, 1.0), passed_time); + if (animation_passed >= 1.0) { + client_fx_settle(c); wl_list_remove(&c->fadeout_link); wlr_scene_node_destroy(&c->scene->node); free(c); @@ -1231,6 +1240,99 @@ void fadeout_client_animation_next_tick(Client *c) { } } +static void client_fx_settle(Client *c) { + if (!c || !c->fx.active) + return; + if (c->fx.scene_buffer) + wlr_scene_node_destroy(&c->fx.scene_buffer->node); + if (c->fx.src) + wlr_texture_destroy(c->fx.src); + if (c->fx.swap) + wlr_swapchain_destroy(c->fx.swap); + c->fx.scene_buffer = NULL; + c->fx.src = NULL; + c->fx.swap = NULL; + c->fx.shader = NULL; + c->fx.active = false; + if (c->fx.fadeout) { + if (c->scene) + wlr_scene_node_set_enabled(&c->scene->node, true); + return; + } + if (c->scene_surface && !c->is_clip_to_hide && !c->is_logic_hide) + wlr_scene_node_set_enabled(&c->scene_surface->node, true); +} + +static bool client_fx_begin(Client *c, struct effect_shader *shader, + struct wlr_scene_node *flatten_root, + struct wlr_scene_tree *parent, int width, + int height, bool include_rects) { + struct wlr_buffer *flat = + effect_pass_flatten(flatten_root, width, height, include_rects); + if (!flat) { + wlr_log(WLR_ERROR, + "effect_pass: fx setup failed (effect_pass_flatten), " + "falling back to parametric animation"); + return false; + } + struct wlr_texture *src = wlr_texture_from_buffer(drw, flat); + if (src && !effect_pass_texture_usable(src)) { + wlr_texture_destroy(src); + wlr_buffer_drop(flat); + wlr_log(WLR_ERROR, + "effect_pass: snapshot texture is not shader-compatible " + "(external-only import), falling back to parametric animation"); + return false; + } + struct wlr_swapchain *swap = + src ? effect_pass_create_swapchain(width, height) : NULL; + struct wlr_scene_buffer *scene_buffer = + swap ? wlr_scene_buffer_create(parent, NULL) : NULL; + wlr_buffer_drop(flat); + if (!scene_buffer) { + if (src) + wlr_texture_destroy(src); + if (swap) + wlr_swapchain_destroy(swap); + wlr_log(WLR_ERROR, + "effect_pass: fx setup failed (%s), falling back to " + "parametric animation", + !src ? "wlr_texture_from_buffer" + : !swap ? "effect_pass_create_swapchain" + : "wlr_scene_buffer_create"); + return false; + } + c->fx.active = true; + c->fx.shader = shader; + c->fx.src = src; + c->fx.swap = swap; + c->fx.scene_buffer = scene_buffer; + c->fx.width = width; + c->fx.height = height; + return true; +} + +static void client_fx_tick(Client *c, float progress, int32_t passed_time) { + if (!c->fx.active || !c->fx.swap) + return; + struct wlr_buffer *dst = wlr_swapchain_acquire(c->fx.swap); + if (!dst) + return; + struct effect_uniforms u = { + .progress = progress, + .time = (float)passed_time / 1000.0f, + }; + bool ok = effect_pass_run(c->fx.shader, c->fx.src, dst, u); + if (ok) + wlr_scene_buffer_set_buffer(c->fx.scene_buffer, dst); + wlr_buffer_unlock(dst); + if (!ok) { + wlr_log(WLR_ERROR, "effect_pass: shader pass failed, falling back to " + "parametric animation"); + client_fx_settle(c); + } +} + void client_animation_next_tick(Client *c) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); @@ -1268,9 +1370,37 @@ void client_animation_next_tick(Client *c) { c->is_pending_open_animation = false; + if (c->animation.action == OPEN && c->animation.running && + !c->fx.open_setup_done && !c->fx.active && c->scene && + c->scene->node.enabled && c->scene_surface) { + struct wlr_box fx_sgeom; + client_get_geometry(c, &fx_sgeom); + bool fx_size_ready = fx_sgeom.width >= c->geom.width - 2 * (int)c->bw && + fx_sgeom.height >= c->geom.height - 2 * (int)c->bw; + if (fx_size_ready || animation_passed > 0.33) + c->fx.open_setup_done = true; + struct effect_shader *fx_shader = + fx_size_ready ? pick_effect(c->effect_open, config.effect_open) + : NULL; + if (fx_shader) { + struct wlr_box fx_clip; + client_get_clip(c, &fx_clip); + wlr_scene_subsurface_tree_set_clip(&c->scene_surface->node, + &fx_clip); + if (client_fx_begin(c, fx_shader, &c->scene->node, c->scene, + c->geom.width, c->geom.height, false)) { + wlr_scene_node_set_enabled(&c->scene_surface->node, false); + wlr_log(WLR_DEBUG, + "effect_pass: open-fade shader active for client"); + } + } + } + client_apply_clip(c, factor); + client_fx_tick(c, 1.0 - MANGO_MIN(animation_passed, 1.0), passed_time); if (animation_passed >= 1.0) { + client_fx_settle(c); // clear the open action state // To prevent him from being mistaken that @@ -1304,10 +1434,9 @@ void client_animation_next_tick(Client *c) { } void init_fadeout_client(Client *c) { - + client_fx_settle(c); if (!c->mon || client_is_unmanaged(c)) return; - if (!c->scene) { return; } @@ -1340,6 +1469,19 @@ void init_fadeout_client(Client *c) { return; } + struct effect_shader *fx_shader = + pick_effect(c->effect_close, config.effect_close); + fadeout_client->fx.fadeout = true; + if (fx_shader && + client_fx_begin(fadeout_client, fx_shader, &fadeout_client->scene->node, + layers[LyrFadeOut], c->geom.width, c->geom.height, + true)) { + wlr_scene_node_set_enabled(&fadeout_client->scene->node, false); + wlr_scene_node_set_position(&fadeout_client->fx.scene_buffer->node, + c->geom.x, c->geom.y); + wlr_log(WLR_DEBUG, "effect_pass: close-fade shader active for client"); + } + fadeout_client->animation.duration = config.animation_duration_close; fadeout_client->geom = fadeout_client->current = fadeout_client->animainit_geom = fadeout_client->animation.initial = @@ -1390,7 +1532,8 @@ void init_fadeout_client(Client *c) { } fadeout_client->animation.time_started = get_now_in_ms(); - wlr_scene_node_set_enabled(&fadeout_client->scene->node, true); + if (!fadeout_client->fx.active) + wlr_scene_node_set_enabled(&fadeout_client->scene->node, true); wl_list_insert(&fadeout_clients, &fadeout_client->fadeout_link); // 请求刷新屏幕 @@ -1474,6 +1617,10 @@ void resize(Client *c, struct wlr_box geo, int32_t interact) { if (!c->mon) return; + if (c->fx.active && + (geo.width != c->fx.width || geo.height != c->fx.height)) + client_fx_settle(c); + c->need_output_flush = true; c->dirty = true; @@ -1553,6 +1700,8 @@ void resize(Client *c, struct wlr_box geo, int32_t interact) { } if (c == grabc) { + client_fx_settle(c); + c->animation.running = false; c->need_output_flush = false; @@ -1767,6 +1916,9 @@ bool client_draw_frame(Client *c) { if (!c || !client_surface(c)->mapped) return false; + if (c->fx.active && !c->animation.running) + client_fx_settle(c); + if (!c->need_output_flush) { return client_apply_focus_opacity(c); } @@ -1774,6 +1926,7 @@ bool client_draw_frame(Client *c) { if (config.animations && c->animation.running) { client_animation_next_tick(c); } else { + client_fx_settle(c); wlr_scene_node_set_position(&c->scene->node, c->pending.x, c->pending.y); c->animation.current = c->animainit_geom = c->animation.initial = diff --git a/src/animation/tag.h b/src/animation/tag.h index 4395fa67..41030668 100644 --- a/src/animation/tag.h +++ b/src/animation/tag.h @@ -49,7 +49,7 @@ void set_arrange_visible(Monitor *m, Client *c, bool want_animation) { c->is_clip_to_hide = false; c->is_logic_hide = false; wlr_scene_node_set_enabled(&c->scene->node, true); - wlr_scene_node_set_enabled(&c->scene_surface->node, true); + client_surface_set_enabled(c, true); } if (!c->animation.tag_from_rule && want_animation && @@ -118,6 +118,7 @@ void set_arrange_hidden(Monitor *m, Client *c, bool want_animation) { c->animation.tagining = false; set_tagout_animation(m, c); } else { + client_fx_settle(c); c->animation.running = false; wlr_scene_node_set_enabled(&c->scene->node, false); c->animainit_geom = c->current = c->pending = c->animation.current = diff --git a/src/config/parse_config.h b/src/config/parse_config.h index 1cdc01cf..1e715b36 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -67,6 +67,8 @@ typedef struct { float scroller_proportion; const char *animation_type_open; const char *animation_type_close; + const char *effect_open; + const char *effect_close; const char *layer_animation_type_open; const char *layer_animation_type_close; int32_t isnoborder; @@ -201,6 +203,8 @@ typedef struct { char animation_type_close[10]; char layer_animation_type_open[10]; char layer_animation_type_close[10]; + char effect_open[64]; + char effect_close[64]; int32_t animation_fade_in; int32_t animation_fade_out; int32_t tag_animation_direction; @@ -1362,6 +1366,12 @@ bool parse_option(Config *config, char *key, char *value) { snprintf(config->animation_type_close, sizeof(config->animation_type_close), "%.9s", value); // string limit to 9 char + } else if (strcmp(key, "effect_open") == 0) { + snprintf(config->effect_open, sizeof(config->effect_open), "%.63s", + value); + } else if (strcmp(key, "effect_close") == 0) { + snprintf(config->effect_close, sizeof(config->effect_close), "%.63s", + value); } else if (strcmp(key, "layer_animation_type_open") == 0) { snprintf(config->layer_animation_type_open, sizeof(config->layer_animation_type_open), "%.9s", @@ -2411,6 +2421,8 @@ bool parse_option(Config *config, char *key, char *value) { // string rule value, relay to a client property rule->animation_type_open = NULL; rule->animation_type_close = NULL; + rule->effect_open = NULL; + rule->effect_close = NULL; // float rule value, relay to a client property rule->focused_opacity = 0; @@ -2452,6 +2464,10 @@ bool parse_option(Config *config, char *key, char *value) { rule->animation_type_open = strdup(val); } else if (strcmp(key, "animation_type_close") == 0) { rule->animation_type_close = strdup(val); + } else if (strcmp(key, "effect_open") == 0) { + rule->effect_open = strdup(val); + } else if (strcmp(key, "effect_close") == 0) { + rule->effect_close = strdup(val); } else if (strcmp(key, "tags") == 0) { rule->tags = 1 << (atoi(val) - 1); } else if (strcmp(key, "monitor") == 0) { @@ -3221,12 +3237,18 @@ void free_config(void) { free((void *)rule->animation_type_open); if (rule->animation_type_close) free((void *)rule->animation_type_close); + if (rule->effect_open) + free((void *)rule->effect_open); + if (rule->effect_close) + free((void *)rule->effect_close); if (rule->monitor) free((void *)rule->monitor); rule->id = NULL; rule->title = NULL; rule->animation_type_open = NULL; rule->animation_type_close = NULL; + rule->effect_open = NULL; + rule->effect_close = NULL; rule->monitor = NULL; // 释放 globalkeybinding 的 arg.v(如果动态分配) if (rule->globalkeybinding.arg.v) { diff --git a/src/draw/effect_pass.c b/src/draw/effect_pass.c new file mode 100644 index 00000000..85224795 --- /dev/null +++ b/src/draw/effect_pass.c @@ -0,0 +1,562 @@ +#include "effect_pass.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool wlr_texture_is_fx(struct wlr_texture *texture); +bool wlr_renderer_is_fx(struct wlr_renderer *renderer); + +static struct wlr_buffer *effect_alloc_render_buffer(int w, int h); + +static struct { + bool ready; + struct wlr_renderer *renderer; + struct wlr_allocator *alloc; + EGLDisplay egl_display; + EGLContext egl_context; +} state; + +bool effect_pass_init(struct wlr_renderer *renderer, + struct wlr_allocator *alloc) { + if (!wlr_renderer_is_fx(renderer)) { + wlr_log(WLR_ERROR, "effect_pass: renderer is not a SceneFX renderer, " + "shaders disabled"); + return false; + } + state.renderer = renderer; + state.alloc = alloc; + state.egl_display = EGL_NO_DISPLAY; + state.egl_context = EGL_NO_CONTEXT; + state.ready = true; + + struct wlr_buffer *probe = effect_alloc_render_buffer(1, 1); + if (probe) { + struct wlr_render_pass *pass = + wlr_renderer_begin_buffer_pass(renderer, probe, NULL); + if (pass) { + state.egl_display = eglGetCurrentDisplay(); + state.egl_context = eglGetCurrentContext(); + wlr_render_pass_submit(pass); + } + wlr_buffer_drop(probe); + } + + if (state.egl_display == EGL_NO_DISPLAY) { + wlr_log(WLR_ERROR, + "effect_pass: could not acquire EGL context, shaders disabled"); + state.ready = false; + return false; + } + return true; +} + +struct effect_shader { + GLuint program; + GLint loc_tex, loc_progress, loc_time, loc_size, loc_pos, loc_uv; +}; + +static const char *VERT_SRC = + "#version 300 es\n" + "in vec2 a_pos;\n" + "in vec2 a_uv;\n" + "out vec2 v_texcoord;\n" + "void main() { v_texcoord = a_uv; gl_Position = vec4(a_pos, 0.0, 1.0); }\n"; + +static const char *INJECT_HEADER = "#version 300 es\n" + "precision highp float;\n" + "uniform sampler2D u_texture;\n" + "uniform float u_progress;\n" + "uniform float u_time;\n" + "uniform vec2 u_size;\n" + "in vec2 v_texcoord;\n" + "out vec4 fragColor;\n"; + +static GLuint effect_compile_shader(GLenum type, const char *src1, + const char *src2) { + GLuint s = glCreateShader(type); + const char *srcs[2] = {src1, src2 ? src2 : ""}; + glShaderSource(s, src2 ? 2 : 1, srcs, NULL); + glCompileShader(s); + GLint ok = 0; + glGetShaderiv(s, GL_COMPILE_STATUS, &ok); + if (!ok) { + char log[1024]; + glGetShaderInfoLog(s, sizeof(log), NULL, log); + wlr_log(WLR_ERROR, "effect_pass: shader compile failed: %s", log); + glDeleteShader(s); + return 0; + } + return s; +} + +static struct effect_shader *make_shader(const char *frag_body) { + GLuint vs = effect_compile_shader(GL_VERTEX_SHADER, VERT_SRC, NULL); + GLuint fs = + effect_compile_shader(GL_FRAGMENT_SHADER, INJECT_HEADER, frag_body); + if (!vs || !fs) { + if (vs) + glDeleteShader(vs); + if (fs) + glDeleteShader(fs); + return NULL; + } + GLuint prog = glCreateProgram(); + glAttachShader(prog, vs); + glAttachShader(prog, fs); + glLinkProgram(prog); + glDeleteShader(vs); + glDeleteShader(fs); + GLint ok = 0; + glGetProgramiv(prog, GL_LINK_STATUS, &ok); + if (!ok) { + glDeleteProgram(prog); + return NULL; + } + struct effect_shader *sh = calloc(1, sizeof(*sh)); + sh->program = prog; + sh->loc_tex = glGetUniformLocation(prog, "u_texture"); + sh->loc_progress = glGetUniformLocation(prog, "u_progress"); + sh->loc_time = glGetUniformLocation(prog, "u_time"); + sh->loc_size = glGetUniformLocation(prog, "u_size"); + sh->loc_pos = glGetAttribLocation(prog, "a_pos"); + sh->loc_uv = glGetAttribLocation(prog, "a_uv"); + return sh; +} + +struct registry_entry { + char name[64]; + struct effect_shader *shader; +}; + +static struct registry_entry *registry; +static size_t registry_count; +static size_t registry_cap; + +static void registry_put(const char *name, struct effect_shader *shader) { + for (size_t i = 0; i < registry_count; i++) { + if (strcmp(registry[i].name, name) == 0) { + wlr_log(WLR_INFO, + "effect_pass: shader '%s' redefined, replacing previous", + name); + glDeleteProgram(registry[i].shader->program); + free(registry[i].shader); + registry[i].shader = shader; + return; + } + } + if (registry_count == registry_cap) { + size_t new_cap = registry_cap ? registry_cap * 2 : 8; + struct registry_entry *grown = + realloc(registry, new_cap * sizeof(*grown)); + if (!grown) { + wlr_log(WLR_ERROR, + "effect_pass: out of memory growing shader registry, " + "dropping '%s'", + name); + glDeleteProgram(shader->program); + free(shader); + return; + } + registry = grown; + registry_cap = new_cap; + } + snprintf(registry[registry_count].name, + sizeof(registry[registry_count].name), "%s", name); + registry[registry_count].shader = shader; + registry_count++; +} + +void effect_pass_load_dir(const char *dir) { + if (!state.ready || !dir) { + return; + } + DIR *d = opendir(dir); + if (!d) { + wlr_log( + WLR_INFO, + "effect_pass: shader directory '%s' not available (%s), skipping", + dir, strerror(errno)); + return; + } + + EGLContext prev_ctx = eglGetCurrentContext(); + EGLSurface prev_draw = eglGetCurrentSurface(EGL_DRAW); + EGLSurface prev_read = eglGetCurrentSurface(EGL_READ); + EGLDisplay prev_dpy = eglGetCurrentDisplay(); + eglMakeCurrent(state.egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + state.egl_context); + + static const char ext[] = ".frag"; + const size_t ext_len = sizeof(ext) - 1; + + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + const char *fname = ent->d_name; + size_t len = strlen(fname); + if (len <= ext_len || strcmp(fname + len - ext_len, ext) != 0) { + continue; + } + size_t name_len = len - ext_len; + if (name_len >= sizeof(registry[0].name)) { + wlr_log(WLR_ERROR, + "effect_pass: shader filename '%s' too long, skipping", + fname); + continue; + } + + char path[PATH_MAX]; + int n = snprintf(path, sizeof(path), "%s/%s", dir, fname); + if (n < 0 || (size_t)n >= sizeof(path)) { + wlr_log(WLR_ERROR, + "effect_pass: shader path '%s/%s' too long, skipping", dir, + fname); + continue; + } + + FILE *f = fopen(path, "rb"); + if (!f) { + wlr_log(WLR_ERROR, + "effect_pass: failed to open '%s' (%s), skipping", path, + strerror(errno)); + continue; + } + if (fseek(f, 0, SEEK_END) != 0) { + wlr_log(WLR_ERROR, "effect_pass: failed to read '%s', skipping", + path); + fclose(f); + continue; + } + long size = ftell(f); + if (size < 0 || fseek(f, 0, SEEK_SET) != 0) { + wlr_log(WLR_ERROR, "effect_pass: failed to read '%s', skipping", + path); + fclose(f); + continue; + } + char *buf = malloc((size_t)size + 1); + if (!buf) { + wlr_log(WLR_ERROR, + "effect_pass: out of memory reading '%s', skipping", path); + fclose(f); + continue; + } + size_t got = fread(buf, 1, (size_t)size, f); + fclose(f); + buf[got] = '\0'; + + char name[sizeof(registry[0].name)]; + snprintf(name, sizeof(name), "%.*s", (int)name_len, fname); + + struct effect_shader *sh = make_shader(buf); + free(buf); + if (!sh) { + wlr_log(WLR_ERROR, + "effect_pass: shader '%s' failed to compile, skipping", + name); + continue; + } + registry_put(name, sh); + wlr_log(WLR_INFO, "effect_pass: registered shader '%s'", name); + } + closedir(d); + wlr_log(WLR_INFO, "effect_pass: %zu shader(s) registered from '%s'", + registry_count, dir); + + eglMakeCurrent(prev_dpy == EGL_NO_DISPLAY ? state.egl_display : prev_dpy, + prev_draw, prev_read, prev_ctx); +} + +struct effect_shader *effect_pass_get(const char *name) { + if (!name) { + return NULL; + } + for (size_t i = 0; i < registry_count; i++) { + if (strcmp(registry[i].name, name) == 0) { + return registry[i].shader; + } + } + return NULL; +} + +bool effect_pass_texture_usable(struct wlr_texture *src) { + if (!src || !wlr_texture_is_fx(src)) + return false; + struct fx_texture_attribs sa; + fx_texture_get_attribs(src, &sa); + return sa.target == GL_TEXTURE_2D; +} + +bool effect_pass_run(struct effect_shader *shader, struct wlr_texture *src, + struct wlr_buffer *dst, struct effect_uniforms u) { + if (!state.ready || !shader || !src || !dst) + return false; + struct fx_texture_attribs sa; + if (!effect_pass_texture_usable(src)) + return false; + fx_texture_get_attribs(src, &sa); + GLuint fbo = fx_renderer_get_buffer_fbo(state.renderer, dst); + if (!fbo) + return false; + + EGLContext prev_ctx = eglGetCurrentContext(); + EGLSurface prev_draw = eglGetCurrentSurface(EGL_DRAW); + EGLSurface prev_read = eglGetCurrentSurface(EGL_READ); + EGLDisplay prev_dpy = eglGetCurrentDisplay(); + eglMakeCurrent(state.egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + state.egl_context); + + while (glGetError() != GL_NO_ERROR) + ; + + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glViewport(0, 0, dst->width, dst->height); + glDisable(GL_BLEND); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_STENCIL_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + glUseProgram(shader->program); + + static const GLfloat pos[] = {-1, -1, 1, -1, -1, 1, 1, 1}; + static const GLfloat uv[] = {0, 0, 1, 0, 0, 1, 1, 1}; + if (shader->loc_pos >= 0) { + glVertexAttribPointer(shader->loc_pos, 2, GL_FLOAT, GL_FALSE, 0, pos); + glEnableVertexAttribArray(shader->loc_pos); + } + if (shader->loc_uv >= 0) { + glVertexAttribPointer(shader->loc_uv, 2, GL_FLOAT, GL_FALSE, 0, uv); + glEnableVertexAttribArray(shader->loc_uv); + } + + glActiveTexture(GL_TEXTURE0); + glBindTexture(sa.target, sa.tex); + glTexParameteri(sa.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(sa.target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glUniform1i(shader->loc_tex, 0); + glUniform1f(shader->loc_progress, u.progress); + glUniform1f(shader->loc_time, u.time); + glUniform2f(shader->loc_size, (float)dst->width, (float)dst->height); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + if (shader->loc_pos >= 0) + glDisableVertexAttribArray(shader->loc_pos); + if (shader->loc_uv >= 0) + glDisableVertexAttribArray(shader->loc_uv); + glBindTexture(sa.target, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glUseProgram(0); + glFlush(); + GLenum err = glGetError(); + + eglMakeCurrent(prev_dpy == EGL_NO_DISPLAY ? state.egl_display : prev_dpy, + prev_draw, prev_read, prev_ctx); + return err == GL_NO_ERROR; +} + +static const struct wlr_drm_format *effect_render_format(void) { + const struct wlr_drm_format_set *texture_formats = + wlr_renderer_get_texture_formats(state.renderer, WLR_BUFFER_CAP_DMABUF); + if (!texture_formats) { + return NULL; + } + return wlr_drm_format_set_get(texture_formats, DRM_FORMAT_ABGR8888); +} + +static struct wlr_buffer *effect_alloc_render_buffer(int w, int h) { + const struct wlr_drm_format *fmt = effect_render_format(); + if (!fmt) { + return NULL; + } + return wlr_allocator_create_buffer(state.alloc, w, h, fmt); +} + +struct wlr_swapchain *effect_pass_create_swapchain(int width, int height) { + if (!state.ready || width <= 0 || height <= 0) { + return NULL; + } + const struct wlr_drm_format *fmt = effect_render_format(); + if (!fmt) { + return NULL; + } + return wlr_swapchain_create(state.alloc, width, height, fmt); +} + +static void effect_flatten_bounds(struct wlr_scene_node *node, int *min_x, + int *min_y, bool *found) { + if (!node->enabled) { + return; + } + if (node->type == WLR_SCENE_NODE_BUFFER || + node->type == WLR_SCENE_NODE_RECT) { + int ax = 0, ay = 0; + wlr_scene_node_coords(node, &ax, &ay); + if (!*found) { + *min_x = ax; + *min_y = ay; + *found = true; + } else { + if (ax < *min_x) + *min_x = ax; + if (ay < *min_y) + *min_y = ay; + } + } else if (node->type == WLR_SCENE_NODE_TREE) { + struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node); + struct wlr_scene_node *child; + wl_list_for_each(child, &tree->children, link) { + effect_flatten_bounds(child, min_x, min_y, found); + } + } +} + +static void effect_flatten_paint(struct wlr_scene_node *node, + struct wlr_render_pass *pass, int ox, int oy, + bool include_rects) { + if (!node->enabled) { + return; + } + int abs_x = 0, abs_y = 0; + wlr_scene_node_coords(node, &abs_x, &abs_y); + int nx = abs_x - ox, ny = abs_y - oy; + switch (node->type) { + case WLR_SCENE_NODE_BUFFER: { + struct wlr_scene_buffer *sb = wlr_scene_buffer_from_node(node); + if (sb->buffer) { + struct wlr_texture *t = + wlr_texture_from_buffer(state.renderer, sb->buffer); + if (t) { + int dw = sb->dst_width > 0 ? sb->dst_width : t->width; + int dh = sb->dst_height > 0 ? sb->dst_height : t->height; + wlr_render_pass_add_texture( + pass, &(struct wlr_render_texture_options){ + .texture = t, + .dst_box = + { + .x = nx, + .y = ny, + .width = dw, + .height = dh, + }, + }); + wlr_texture_destroy(t); + } else { + wlr_log(WLR_ERROR, + "effect_pass: flatten failed to create texture from " + "buffer, snapshot will be incomplete"); + } + } + break; + } + case WLR_SCENE_NODE_RECT: { + if (!include_rects) + break; + struct wlr_scene_rect *r = wlr_scene_rect_from_node(node); + wlr_render_pass_add_rect(pass, &(struct wlr_render_rect_options){ + .box = {.x = nx, + .y = ny, + .width = r->width, + .height = r->height}, + .color = {r->color[0], r->color[1], + r->color[2], r->color[3]}, + }); + break; + } + case WLR_SCENE_NODE_TREE: { + struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node); + struct wlr_scene_node *child; + wl_list_for_each(child, &tree->children, link) { + effect_flatten_paint(child, pass, ox, oy, include_rects); + } + break; + } + case WLR_SCENE_NODE_SHADOW: + case WLR_SCENE_NODE_OPTIMIZED_BLUR: + case WLR_SCENE_NODE_BLUR: + break; + } +} + +struct wlr_buffer *effect_pass_flatten(struct wlr_scene_node *node, int width, + int height, bool include_rects) { + if (!state.ready || !node || width <= 0 || height <= 0) { + return NULL; + } + + struct wlr_buffer *dst = effect_alloc_render_buffer(width, height); + if (!dst) { + wlr_log(WLR_ERROR, "effect_pass: failed to allocate flatten buffer"); + return NULL; + } + + struct wlr_render_pass *pass = + wlr_renderer_begin_buffer_pass(state.renderer, dst, NULL); + if (!pass) { + wlr_buffer_drop(dst); + return NULL; + } + + wlr_render_pass_add_rect( + pass, &(struct wlr_render_rect_options){ + .box = {.x = 0, .y = 0, .width = width, .height = height}, + .color = {0, 0, 0, 0}, + .blend_mode = WLR_RENDER_BLEND_MODE_NONE, + }); + + int ox = 0, oy = 0; + bool found = false; + effect_flatten_bounds(node, &ox, &oy, &found); + if (!found) { + wlr_scene_node_coords(node, &ox, &oy); + } + effect_flatten_paint(node, pass, ox, oy, include_rects); + + if (!wlr_render_pass_submit(pass)) { + wlr_buffer_drop(dst); + return NULL; + } + + return dst; +} + +void effect_pass_finish(void) { + if (registry_count > 0) { + EGLContext prev_ctx = eglGetCurrentContext(); + EGLSurface prev_draw = eglGetCurrentSurface(EGL_DRAW); + EGLSurface prev_read = eglGetCurrentSurface(EGL_READ); + EGLDisplay prev_dpy = eglGetCurrentDisplay(); + eglMakeCurrent(state.egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, + state.egl_context); + for (size_t i = 0; i < registry_count; i++) { + if (registry[i].shader) { + glDeleteProgram(registry[i].shader->program); + free(registry[i].shader); + } + } + + eglMakeCurrent(prev_dpy == EGL_NO_DISPLAY ? state.egl_display + : prev_dpy, + prev_draw, prev_read, prev_ctx); + } + free(registry); + registry = NULL; + registry_count = 0; + registry_cap = 0; + + state.ready = false; +} diff --git a/src/draw/effect_pass.h b/src/draw/effect_pass.h new file mode 100644 index 00000000..ec583047 --- /dev/null +++ b/src/draw/effect_pass.h @@ -0,0 +1,29 @@ +#ifndef MANGO_EFFECT_PASS_H +#define MANGO_EFFECT_PASS_H +#include +#include +#include +#include +#include +#include + +bool effect_pass_init(struct wlr_renderer *renderer, + struct wlr_allocator *alloc); + +void effect_pass_finish(void); + +struct effect_uniforms { + float progress; + float time; +}; +struct effect_shader; +void effect_pass_load_dir(const char *dir); +struct effect_shader *effect_pass_get(const char *name); +bool effect_pass_run(struct effect_shader *shader, struct wlr_texture *src, + struct wlr_buffer *dst, struct effect_uniforms u); +bool effect_pass_texture_usable(struct wlr_texture *src); +struct wlr_buffer *effect_pass_flatten(struct wlr_scene_node *node, int width, + int height, bool include_rects); +struct wlr_swapchain *effect_pass_create_swapchain(int width, int height); + +#endif diff --git a/src/mango.c b/src/mango.c index 2719c91b..684b5740 100644 --- a/src/mango.c +++ b/src/mango.c @@ -99,7 +99,9 @@ #include #endif #include "common/util.h" +#include "draw/effect_pass.h" #include "draw/text-node.h" +#include /* macros */ #define MANGO_MAX(A, B) ((A) > (B) ? (A) : (B)) @@ -357,6 +359,16 @@ struct Client { struct wl_list link; struct wl_list flink; struct wl_list fadeout_link; + struct { + bool active; + bool open_setup_done; + bool fadeout; + struct effect_shader *shader; + struct wlr_scene_buffer *scene_buffer; + struct wlr_swapchain *swap; + struct wlr_texture *src; + int width, height; + } fx; union { struct wlr_xdg_surface *xdg; struct wlr_xwayland_surface *xwayland; @@ -406,6 +418,8 @@ struct Client { const char *animation_type_open; const char *animation_type_close; + char effect_open[64]; + char effect_close[64]; int32_t is_in_scratchpad; int32_t iscustomsize; int32_t iscustompos; @@ -973,6 +987,36 @@ static bool mango_scene_output_commit(struct wlr_scene_output *scene_output, static bool mango_output_commit(Monitor *m); static bool check_tearing_frame_allow(Monitor *m); static void client_set_group_config(Client *c); +static void client_fx_settle(Client *c); +static void client_surface_set_enabled(Client *c, bool enabled); +static bool client_fx_begin(Client *c, struct effect_shader *shader, + struct wlr_scene_node *flatten_root, + struct wlr_scene_tree *parent, int width, + int height, bool include_rects); +static void client_fx_tick(Client *c, float progress, int32_t passed_time); +static struct effect_shader *pick_effect(const char *per_client, + const char *global) { + const char *name = (per_client && per_client[0]) ? per_client + : (global && global[0]) ? global + : NULL; + struct effect_shader *sh = name ? effect_pass_get(name) : NULL; + if (name && !sh) + wlr_log(WLR_DEBUG, + "effect_pass: shader '%s' not found, using " + "parametric animation", + name); + return sh; +} + +static void load_user_shader_dir(void) { + const char *home = getenv("HOME"); + if (!home) { + return; + } + char shader_dir[512]; + snprintf(shader_dir, sizeof(shader_dir), "%s/.config/mango/shaders", home); + effect_pass_load_dir(shader_dir); +} #include "data/static_keymap.h" #include "dispatch/bind_declare.h" @@ -1353,6 +1397,8 @@ void client_replace(Client *c, Client *w, bool is_group_change_member, wl_list_safe_reinsert_next(&w->link, &c->link); wl_list_safe_reinsert_prev(&w->flink, &c->flink); + client_fx_settle(w); + client_fx_settle(c); wlr_scene_node_set_enabled(&w->scene->node, false); if (!c->is_logic_hide) { @@ -1497,6 +1543,8 @@ void gpureset(struct wl_listener *listener, void *data) { struct wlr_renderer *old_drw = drw; struct wlr_allocator *old_alloc = alloc; struct Monitor *m = NULL; + Client *fxc = NULL, *fxtmp = NULL; + Client *oc = NULL, *octmp = NULL; wlr_log(WLR_DEBUG, "gpu reset"); @@ -1506,6 +1554,15 @@ void gpureset(struct wl_listener *listener, void *data) { if (!(alloc = wlr_allocator_autocreate(backend, drw))) die("couldn't recreate allocator"); + wl_list_for_each_safe(fxc, fxtmp, &fadeout_clients, fadeout_link) + client_fx_settle(fxc); + + wl_list_for_each_safe(oc, octmp, &clients, link) client_fx_settle(oc); + + effect_pass_finish(); + effect_pass_init(drw, alloc); + load_user_shader_dir(); + wl_list_remove(&gpu_reset.link); wl_signal_add(&drw->events.lost, &gpu_reset); @@ -1637,6 +1694,11 @@ static void apply_rule_properties(Client *c, const ConfigWinRule *r) { APPLY_STRING_PROP(c, r, animation_type_open); APPLY_STRING_PROP(c, r, animation_type_close); + if (r->effect_open) + snprintf(c->effect_open, sizeof(c->effect_open), "%s", r->effect_open); + if (r->effect_close) + snprintf(c->effect_close, sizeof(c->effect_close), "%s", + r->effect_close); } void set_float_malposition(Client *tc) { @@ -2654,6 +2716,8 @@ void cleanup(void) { dwl_im_relay_finish(dwl_input_method_relay); + effect_pass_finish(); + /* If it's not destroyed manually it will cause a use-after-free of * wlr_seat. Destroy it until it's fixed in the wlroots side */ wlr_backend_destroy(backend); @@ -6241,6 +6305,9 @@ void setup(void) { if (!(alloc = wlr_allocator_autocreate(backend, drw))) die("couldn't create allocator"); + effect_pass_init(drw, alloc); + load_user_shader_dir(); + /* This creates some hands-off wlroots interfaces. The compositor is * necessary for clients to allocate surfaces and the data device * manager handles the clipboard. Each of these wlroots interfaces has @@ -6558,6 +6625,8 @@ void overview_backup_surface(Client *c) { return; } + client_fx_settle(c); + struct wlr_box geometry; client_get_geometry(c, &geometry); struct wlr_box clip_box = (struct wlr_box){ @@ -6782,6 +6851,7 @@ void unmapnotify(struct wl_listener *listener, void *data) { Client *c = wl_container_of(listener, c, unmap); Monitor *m = NULL; Client *nextfocus = NULL; + client_fx_settle(c); c->iskilling = 1; struct ScrollerStackNode *target_node = c->mon ? find_scroller_node(