diff --git a/clutter/clutter/clutter-blur-private.h b/clutter/clutter/clutter-blur-private.h
new file mode 100644
index 000000000..feb288f31
--- /dev/null
+++ b/clutter/clutter/clutter-blur-private.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 Endless OS Foundation, LLC
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see .
+ */
+
+#ifndef CLUTTER_BLUR_PRIVATE_H
+#define CLUTTER_BLUR_PRIVATE_H
+
+#include
+
+#include
+
+G_BEGIN_DECLS
+
+typedef struct _ClutterBlur ClutterBlur;
+
+ClutterBlur * clutter_blur_new (CoglTexture *texture,
+ unsigned int sigma);
+
+void clutter_blur_apply (ClutterBlur *blur);
+
+CoglTexture * clutter_blur_get_texture (ClutterBlur *blur);
+
+void clutter_blur_free (ClutterBlur *blur);
+
+G_END_DECLS
+
+#endif /* CLUTTER_BLUR_PRIVATE_H */
diff --git a/clutter/clutter/clutter-blur.c b/clutter/clutter/clutter-blur.c
new file mode 100644
index 000000000..0531bdd45
--- /dev/null
+++ b/clutter/clutter/clutter-blur.c
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2020 Endless OS Foundation, LLC
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see .
+ */
+
+#include "clutter-blur-private.h"
+
+#include "clutter-backend.h"
+
+/**
+ * SECTION:clutter-blur
+ * @short_description: Blur textures
+ *
+ * #ClutterBlur is a moderately fast gaussian blur implementation.
+ *
+ * # Optimizations
+ *
+ * There are a number of optimizations in place to make this blur implementation
+ * real-time. All in all, the implementation performs best when using large
+ * blur-radii that allow downscaling the texture to smaller sizes, at small
+ * radii where no downscaling is possible this can easily halve the framerate.
+ *
+ * ## Multipass
+ *
+ * It is implemented in 2 passes: vertical and horizontal.
+ *
+ * ## Downscaling
+ *
+ * #ClutterBlur uses dynamic downscaling to speed up blurring. Downscaling
+ * happens in factors of 2 (the image is downscaled either by 2, 4, 8, 16, …)
+ * and depends on the blur radius, the texture size, among others.
+ *
+ * The texture is drawn into a downscaled framebuffer; the blur passes are
+ * applied on the downscaled texture contents; and finally, the blurred
+ * contents are drawn
+ * upscaled again.
+ *
+ * ## Hardware Interpolation
+ *
+ * This blur implementation cuts down the number of sampling operations by
+ * exploiting the hardware interpolation that is performed when sampling between
+ * pixel boundaries. This technique is described at:
+ *
+ * http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
+ *
+ * ## Incremental gauss-factor calculation
+ *
+ * The kernel values for the gaussian kernel are computed incrementally instead
+ * of running the expensive calculations multiple times inside the blur shader.
+ * The implementation is based on the algorithm presented by K. Turkowski in
+ * GPU Gems 3, chapter 40:
+ *
+ * https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch40.html
+ *
+ */
+
+static const char *gaussian_blur_glsl_declarations =
+"uniform float sigma; \n"
+"uniform float pixel_step; \n"
+"uniform int vertical; \n";
+
+static const char *gaussian_blur_glsl =
+" int horizontal = 1 - vertical; \n"
+" \n"
+" vec2 uv = vec2 (cogl_tex_coord.st); \n"
+" \n"
+" vec3 gauss_coefficient; \n"
+" gauss_coefficient.x = 1.0 / (sqrt (2.0 * 3.14159265) * sigma); \n"
+" gauss_coefficient.y = exp (-0.5 / (sigma * sigma)); \n"
+" gauss_coefficient.z = gauss_coefficient.y * gauss_coefficient.y; \n"
+" \n"
+" float gauss_coefficient_total = gauss_coefficient.x; \n"
+" \n"
+" vec4 ret = texture2D (cogl_sampler, uv) * gauss_coefficient.x; \n"
+" gauss_coefficient.xy *= gauss_coefficient.yz; \n"
+" \n"
+" int n_steps = int (ceil (3 * sigma)); \n"
+" \n"
+" for (int i = 1; i < n_steps; i += 2) { \n"
+" float coefficient_subtotal = gauss_coefficient.x; \n"
+" gauss_coefficient.xy *= gauss_coefficient.yz; \n"
+" coefficient_subtotal += gauss_coefficient.x; \n"
+" \n"
+" float gauss_ratio = gauss_coefficient.x / coefficient_subtotal; \n"
+" \n"
+" float foffset = float (i) + gauss_ratio; \n"
+" vec2 offset = vec2 (foffset * pixel_step * float (horizontal), \n"
+" foffset * pixel_step * float (vertical)); \n"
+" \n"
+" ret += texture2D (cogl_sampler, uv + offset) * coefficient_subtotal; \n"
+" ret += texture2D (cogl_sampler, uv - offset) * coefficient_subtotal; \n"
+" \n"
+" gauss_coefficient_total += 2.0 * coefficient_subtotal; \n"
+" gauss_coefficient.xy *= gauss_coefficient.yz; \n"
+" } \n"
+" \n"
+" cogl_texel = ret / gauss_coefficient_total; \n";
+
+#define MIN_DOWNSCALE_SIZE 256.f
+#define MAX_SIGMA 6.f
+
+enum
+{
+ VERTICAL,
+ HORIZONTAL,
+};
+
+typedef struct
+{
+ CoglFramebuffer *framebuffer;
+ CoglPipeline *pipeline;
+ CoglTexture *texture;
+ int orientation;
+ int sigma_uniform;
+ int pixel_step_uniform;
+ int vertical_uniform;
+} BlurPass;
+
+struct _ClutterBlur
+{
+ CoglTexture *source_texture;
+ unsigned int sigma;
+ float downscale_factor;
+
+ BlurPass pass[2];
+};
+
+static CoglPipeline*
+create_blur_pipeline (void)
+{
+ static CoglPipelineKey blur_pipeline_key = "clutter-blur-pipeline-private";
+ CoglContext *ctx =
+ clutter_backend_get_cogl_context (clutter_get_default_backend ());
+ CoglPipeline *blur_pipeline;
+
+ blur_pipeline =
+ cogl_context_get_named_pipeline (ctx, &blur_pipeline_key);
+
+ if (G_UNLIKELY (blur_pipeline == NULL))
+ {
+ CoglSnippet *snippet;
+
+ blur_pipeline = cogl_pipeline_new (ctx);
+ cogl_pipeline_set_layer_null_texture (blur_pipeline, 0);
+ cogl_pipeline_set_layer_filters (blur_pipeline,
+ 0,
+ COGL_PIPELINE_FILTER_LINEAR,
+ COGL_PIPELINE_FILTER_LINEAR);
+ cogl_pipeline_set_layer_wrap_mode (blur_pipeline,
+ 0,
+ COGL_PIPELINE_WRAP_MODE_CLAMP_TO_EDGE);
+
+ snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_TEXTURE_LOOKUP,
+ gaussian_blur_glsl_declarations,
+ NULL);
+ cogl_snippet_set_replace (snippet, gaussian_blur_glsl);
+ cogl_pipeline_add_layer_snippet (blur_pipeline, 0, snippet);
+ cogl_object_unref (snippet);
+
+ cogl_context_set_named_pipeline (ctx, &blur_pipeline_key, blur_pipeline);
+ }
+
+ return cogl_pipeline_copy (blur_pipeline);
+}
+
+static void
+update_blur_uniforms (ClutterBlur *blur,
+ BlurPass *pass)
+{
+ gboolean vertical = pass->orientation == VERTICAL;
+
+ pass->pixel_step_uniform =
+ cogl_pipeline_get_uniform_location (pass->pipeline, "pixel_step");
+ if (pass->pixel_step_uniform > -1)
+ {
+ float pixel_step;
+
+ if (vertical)
+ pixel_step = 1.f / cogl_texture_get_height (pass->texture);
+ else
+ pixel_step = 1.f / cogl_texture_get_width (pass->texture);
+
+ cogl_pipeline_set_uniform_1f (pass->pipeline,
+ pass->pixel_step_uniform,
+ pixel_step);
+ }
+
+ pass->sigma_uniform =
+ cogl_pipeline_get_uniform_location (pass->pipeline, "sigma");
+ if (pass->sigma_uniform > -1)
+ {
+ cogl_pipeline_set_uniform_1f (pass->pipeline,
+ pass->sigma_uniform,
+ blur->sigma / blur->downscale_factor);
+ }
+
+ pass->vertical_uniform =
+ cogl_pipeline_get_uniform_location (pass->pipeline, "vertical");
+ if (pass->vertical_uniform > -1)
+ {
+ cogl_pipeline_set_uniform_1i (pass->pipeline,
+ pass->vertical_uniform,
+ vertical);
+ }
+}
+
+static gboolean
+create_fbo (ClutterBlur *blur,
+ BlurPass *pass)
+{
+ CoglContext *ctx =
+ clutter_backend_get_cogl_context (clutter_get_default_backend ());
+ graphene_matrix_t projection;
+ float scaled_height;
+ float scaled_width;
+ float height;
+ float width;
+
+ g_clear_pointer (&pass->texture, cogl_object_unref);
+ g_clear_object (&pass->framebuffer);
+
+ width = cogl_texture_get_width (blur->source_texture);
+ height = cogl_texture_get_height (blur->source_texture);
+ scaled_width = floorf (width / blur->downscale_factor);
+ scaled_height = floorf (height / blur->downscale_factor);
+
+ pass->texture = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx,
+ scaled_width,
+ scaled_height));
+ if (!pass->texture)
+ return FALSE;
+
+ pass->framebuffer =
+ COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (pass->texture));
+ if (!pass->framebuffer)
+ {
+ g_warning ("%s: Unable to create an Offscreen buffer", G_STRLOC);
+ return FALSE;
+ }
+
+ graphene_matrix_init_translate (&projection,
+ &GRAPHENE_POINT3D_INIT (-scaled_width / 2.f,
+ -scaled_height / 2.f,
+ 0.f));
+ graphene_matrix_scale (&projection,
+ 2.f / scaled_width,
+ -2.f / scaled_height,
+ 1.f);
+
+ cogl_framebuffer_set_projection_matrix (pass->framebuffer, &projection);
+
+ return TRUE;
+}
+
+static gboolean
+setup_blur_pass (ClutterBlur *blur,
+ BlurPass *pass,
+ int orientation,
+ CoglTexture *texture)
+{
+ pass->orientation = orientation;
+ pass->pipeline = create_blur_pipeline ();
+ cogl_pipeline_set_layer_texture (pass->pipeline, 0, texture);
+
+ if (!create_fbo (blur, pass))
+ return FALSE;
+
+ update_blur_uniforms (blur, pass);
+ return TRUE;
+}
+
+static float
+calculate_downscale_factor (float width,
+ float height,
+ float sigma)
+{
+ float downscale_factor = 1.f;
+ float scaled_width = width;
+ float scaled_height = height;
+ float scaled_sigma = sigma;
+
+ /* This is the algorithm used by Firefox; keep downscaling until either the
+ * blur radius is lower than the threshold, or the downscaled texture is too
+ * small.
+ */
+ while (scaled_sigma > MAX_SIGMA &&
+ scaled_width > MIN_DOWNSCALE_SIZE &&
+ scaled_height > MIN_DOWNSCALE_SIZE)
+ {
+ downscale_factor *= 2.f;
+
+ scaled_width = width / downscale_factor;
+ scaled_height = height / downscale_factor;
+ scaled_sigma = sigma / downscale_factor;
+ }
+
+ return downscale_factor;
+}
+
+static void
+apply_blur_pass (BlurPass *pass)
+{
+ CoglColor transparent;
+
+ cogl_color_init_from_4ub (&transparent, 0, 0, 0, 0);
+
+ cogl_framebuffer_clear (pass->framebuffer,
+ COGL_BUFFER_BIT_COLOR,
+ &transparent);
+
+ cogl_framebuffer_draw_rectangle (pass->framebuffer,
+ pass->pipeline,
+ 0, 0,
+ cogl_texture_get_width (pass->texture),
+ cogl_texture_get_height (pass->texture));
+}
+
+static void
+clear_blur_pass (BlurPass *pass)
+{
+ g_clear_pointer (&pass->pipeline, cogl_object_unref);
+ g_clear_pointer (&pass->texture, cogl_object_unref);
+ g_clear_object (&pass->framebuffer);
+}
+
+/**
+ * clutter_blur_new:
+ * @texture: a #CoglTexture
+ * @sigma: blur sigma
+ *
+ * Creates a new #ClutterBlur.
+ *
+ * Returns: (transfer full) (nullable): A newly created #ClutterBlur
+ */
+ClutterBlur *
+clutter_blur_new (CoglTexture *texture,
+ unsigned int sigma)
+{
+ ClutterBlur *blur;
+ unsigned int height;
+ unsigned int width;
+ BlurPass *hpass;
+ BlurPass *vpass;
+
+ width = cogl_texture_get_width (texture);
+ height = cogl_texture_get_height (texture);
+
+ blur = g_new0 (ClutterBlur, 1);
+ blur->sigma = sigma;
+ blur->source_texture = cogl_object_ref (texture);
+ blur->downscale_factor = calculate_downscale_factor (width, height, sigma);
+
+ vpass = &blur->pass[VERTICAL];
+ hpass = &blur->pass[HORIZONTAL];
+
+ if (!setup_blur_pass (blur, vpass, VERTICAL, texture) ||
+ !setup_blur_pass (blur, hpass, HORIZONTAL, vpass->texture))
+ {
+ clutter_blur_free (blur);
+ return NULL;
+ }
+
+ return g_steal_pointer (&blur);
+}
+
+/**
+ * clutter_blur_apply:
+ * @blur: a #ClutterBlur
+ *
+ * Applies the blur. The resulting texture can be retrieved by
+ * clutter_blur_get_texture().
+ */
+void
+clutter_blur_apply (ClutterBlur *blur)
+{
+ apply_blur_pass (&blur->pass[VERTICAL]);
+ apply_blur_pass (&blur->pass[HORIZONTAL]);
+}
+
+/**
+ * clutter_blur_get_texture:
+ * @blur: a #ClutterBlur
+ *
+ * Retrieves the texture where the blurred contents are stored. The
+ * contents are undefined until clutter_blur_apply() is called.
+ *
+ * Returns: (transfer none): a #CoglTexture
+ */
+CoglTexture *
+clutter_blur_get_texture (ClutterBlur *blur)
+{
+ return blur->pass[HORIZONTAL].texture;
+}
+
+/**
+ * clutter_blur_free:
+ * @blur: A #ClutterBlur
+ *
+ * Frees @blur.
+ */
+void
+clutter_blur_free (ClutterBlur *blur)
+{
+ g_assert (blur);
+
+ clear_blur_pass (&blur->pass[VERTICAL]);
+ clear_blur_pass (&blur->pass[HORIZONTAL]);
+ cogl_clear_object (&blur->source_texture);
+ g_free (blur);
+}
diff --git a/clutter/clutter/meson.build b/clutter/clutter/meson.build
index f914370ec..4b617bd98 100644
--- a/clutter/clutter/meson.build
+++ b/clutter/clutter/meson.build
@@ -101,6 +101,7 @@ clutter_sources = [
'clutter-bind-constraint.c',
'clutter-binding-pool.c',
'clutter-bin-layout.c',
+ 'clutter-blur.c',
'clutter-blur-effect.c',
'clutter-box-layout.c',
'clutter-brightness-contrast-effect.c',
@@ -184,6 +185,7 @@ clutter_private_headers = [
'clutter-actor-private.h',
'clutter-backend-private.h',
'clutter-bezier.h',
+ 'clutter-blur-private.h',
'clutter-constraint-private.h',
'clutter-content-private.h',
'clutter-damage-history.h',