1 /*
2  * Copyright (C) 2020 Endless OS Foundation, LLC
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "clutter-blur-private.h"
19 
20 #include "clutter-backend.h"
21 
22 /**
23  * SECTION:clutter-blur
24  * @short_description: Blur textures
25  *
26  * #ClutterBlur is a moderately fast gaussian blur implementation.
27  *
28  * # Optimizations
29  *
30  * There are a number of optimizations in place to make this blur implementation
31  * real-time. All in all, the implementation performs best when using large
32  * blur-radii that allow downscaling the texture to smaller sizes, at small
33  * radii where no downscaling is possible this can easily halve the framerate.
34  *
35  * ## Multipass
36  *
37  * It is implemented in 2 passes: vertical and horizontal.
38  *
39  * ## Downscaling
40  *
41  * #ClutterBlur uses dynamic downscaling to speed up blurring. Downscaling
42  * happens in factors of 2 (the image is downscaled either by 2, 4, 8, 16, …)
43  * and depends on the blur radius, the texture size, among others.
44  *
45  * The texture is drawn into a downscaled framebuffer; the blur passes are
46  * applied on the downscaled texture contents; and finally, the blurred
47  * contents are drawn
48  * upscaled again.
49  *
50  * ## Hardware Interpolation
51  *
52  * This blur implementation cuts down the number of sampling operations by
53  * exploiting the hardware interpolation that is performed when sampling between
54  * pixel boundaries. This technique is described at:
55  *
56  * http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/
57  *
58  * ## Incremental gauss-factor calculation
59  *
60  * The kernel values for the gaussian kernel are computed incrementally instead
61  * of running the expensive calculations multiple times inside the blur shader.
62  * The implementation is based on the algorithm presented by K. Turkowski in
63  * GPU Gems 3, chapter 40:
64  *
65  * https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch40.html
66  *
67  */
68 
69 static const char *gaussian_blur_glsl_declarations =
70 "uniform float sigma;                                                      \n"
71 "uniform float pixel_step;                                                 \n"
72 "uniform vec2 direction;                                                   \n";
73 
74 static const char *gaussian_blur_glsl =
75 "  vec2 uv = vec2 (cogl_tex_coord.st);                                     \n"
76 "                                                                          \n"
77 "  vec3 gauss_coefficient;                                                 \n"
78 "  gauss_coefficient.x = 1.0 / (sqrt (2.0 * 3.14159265) * sigma);          \n"
79 "  gauss_coefficient.y = exp (-0.5 / (sigma * sigma));                     \n"
80 "  gauss_coefficient.z = gauss_coefficient.y * gauss_coefficient.y;        \n"
81 "                                                                          \n"
82 "  float gauss_coefficient_total = gauss_coefficient.x;                    \n"
83 "                                                                          \n"
84 "  vec4 ret = texture2D (cogl_sampler, uv) * gauss_coefficient.x;          \n"
85 "  gauss_coefficient.xy *= gauss_coefficient.yz;                           \n"
86 "                                                                          \n"
87 "  int n_steps = int (ceil (1.5 * sigma)) * 2;                             \n"
88 "                                                                          \n"
89 "  for (int i = 1; i <= n_steps; i += 2) {                                 \n"
90 "    float coefficient_subtotal = gauss_coefficient.x;                     \n"
91 "    gauss_coefficient.xy *= gauss_coefficient.yz;                         \n"
92 "    coefficient_subtotal += gauss_coefficient.x;                          \n"
93 "                                                                          \n"
94 "    float gauss_ratio = gauss_coefficient.x / coefficient_subtotal;       \n"
95 "                                                                          \n"
96 "    float foffset = float (i) + gauss_ratio;                              \n"
97 "    vec2 offset = direction * foffset * pixel_step;                       \n"
98 "                                                                          \n"
99 "    ret += texture2D (cogl_sampler, uv + offset) * coefficient_subtotal;  \n"
100 "    ret += texture2D (cogl_sampler, uv - offset) * coefficient_subtotal;  \n"
101 "                                                                          \n"
102 "    gauss_coefficient_total += 2.0 * coefficient_subtotal;                \n"
103 "    gauss_coefficient.xy *= gauss_coefficient.yz;                         \n"
104 "  }                                                                       \n"
105 "                                                                          \n"
106 "  cogl_texel = ret / gauss_coefficient_total;                             \n";
107 
108 #define MIN_DOWNSCALE_SIZE 256.f
109 #define MAX_SIGMA 6.f
110 
111 enum
112 {
113   VERTICAL,
114   HORIZONTAL,
115 };
116 
117 typedef struct
118 {
119   CoglFramebuffer *framebuffer;
120   CoglPipeline *pipeline;
121   CoglTexture *texture;
122   int orientation;
123 } BlurPass;
124 
125 struct _ClutterBlur
126 {
127   CoglTexture *source_texture;
128   float sigma;
129   float downscale_factor;
130 
131   BlurPass pass[2];
132 };
133 
134 static CoglPipeline*
create_blur_pipeline(void)135 create_blur_pipeline (void)
136 {
137   static CoglPipelineKey blur_pipeline_key = "clutter-blur-pipeline-private";
138   CoglContext *ctx =
139     clutter_backend_get_cogl_context (clutter_get_default_backend ());
140   CoglPipeline *blur_pipeline;
141 
142   blur_pipeline =
143     cogl_context_get_named_pipeline (ctx, &blur_pipeline_key);
144 
145   if (G_UNLIKELY (blur_pipeline == NULL))
146     {
147       CoglSnippet *snippet;
148 
149       blur_pipeline = cogl_pipeline_new (ctx);
150       cogl_pipeline_set_layer_null_texture (blur_pipeline, 0);
151       cogl_pipeline_set_layer_filters (blur_pipeline,
152                                        0,
153                                        COGL_PIPELINE_FILTER_LINEAR,
154                                        COGL_PIPELINE_FILTER_LINEAR);
155       cogl_pipeline_set_layer_wrap_mode (blur_pipeline,
156                                          0,
157                                          COGL_PIPELINE_WRAP_MODE_CLAMP_TO_EDGE);
158 
159       snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_TEXTURE_LOOKUP,
160                                   gaussian_blur_glsl_declarations,
161                                   NULL);
162       cogl_snippet_set_replace (snippet, gaussian_blur_glsl);
163       cogl_pipeline_add_layer_snippet (blur_pipeline, 0, snippet);
164       cogl_object_unref (snippet);
165 
166       cogl_context_set_named_pipeline (ctx, &blur_pipeline_key, blur_pipeline);
167     }
168 
169   return cogl_pipeline_copy (blur_pipeline);
170 }
171 
172 static void
update_blur_uniforms(ClutterBlur * blur,BlurPass * pass)173 update_blur_uniforms (ClutterBlur *blur,
174                       BlurPass    *pass)
175 {
176   gboolean vertical = pass->orientation == VERTICAL;
177   int sigma_uniform;
178   int pixel_step_uniform;
179   int direction_uniform;
180 
181   pixel_step_uniform =
182     cogl_pipeline_get_uniform_location (pass->pipeline, "pixel_step");
183   if (pixel_step_uniform > -1)
184     {
185       float pixel_step;
186 
187       if (vertical)
188         pixel_step = 1.f / cogl_texture_get_height (pass->texture);
189       else
190         pixel_step = 1.f / cogl_texture_get_width (pass->texture);
191 
192       cogl_pipeline_set_uniform_1f (pass->pipeline,
193                                     pixel_step_uniform,
194                                     pixel_step);
195     }
196 
197   sigma_uniform = cogl_pipeline_get_uniform_location (pass->pipeline, "sigma");
198   if (sigma_uniform > -1)
199     {
200       cogl_pipeline_set_uniform_1f (pass->pipeline,
201                                     sigma_uniform,
202                                     blur->sigma / blur->downscale_factor);
203     }
204 
205   direction_uniform =
206     cogl_pipeline_get_uniform_location (pass->pipeline, "direction");
207   if (direction_uniform > -1)
208     {
209       gboolean horizontal = !vertical;
210       float direction[2] = {
211         horizontal,
212         vertical,
213       };
214 
215       cogl_pipeline_set_uniform_float (pass->pipeline,
216                                        direction_uniform,
217                                        2, 1,
218                                        direction);
219     }
220 }
221 
222 static gboolean
create_fbo(ClutterBlur * blur,BlurPass * pass)223 create_fbo (ClutterBlur *blur,
224             BlurPass    *pass)
225 {
226   CoglContext *ctx =
227     clutter_backend_get_cogl_context (clutter_get_default_backend ());
228   float scaled_height;
229   float scaled_width;
230   float height;
231   float width;
232 
233   g_clear_pointer (&pass->texture, cogl_object_unref);
234   g_clear_object (&pass->framebuffer);
235 
236   width = cogl_texture_get_width (blur->source_texture);
237   height = cogl_texture_get_height (blur->source_texture);
238   scaled_width = floorf (width / blur->downscale_factor);
239   scaled_height = floorf (height / blur->downscale_factor);
240 
241   pass->texture = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx,
242                                                                scaled_width,
243                                                                scaled_height));
244   if (!pass->texture)
245     return FALSE;
246 
247   pass->framebuffer =
248     COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (pass->texture));
249   if (!pass->framebuffer)
250     {
251       g_warning ("%s: Unable to create an Offscreen buffer", G_STRLOC);
252       return FALSE;
253     }
254 
255   cogl_framebuffer_orthographic (pass->framebuffer,
256                                  0.0, 0.0,
257                                  scaled_width,
258                                  scaled_height,
259                                  0.0, 1.0);
260   return TRUE;
261 }
262 
263 static gboolean
setup_blur_pass(ClutterBlur * blur,BlurPass * pass,int orientation,CoglTexture * texture)264 setup_blur_pass (ClutterBlur *blur,
265                  BlurPass    *pass,
266                  int          orientation,
267                  CoglTexture *texture)
268 {
269   pass->orientation = orientation;
270   pass->pipeline = create_blur_pipeline ();
271   cogl_pipeline_set_layer_texture (pass->pipeline, 0, texture);
272 
273   if (!create_fbo (blur, pass))
274     return FALSE;
275 
276   update_blur_uniforms (blur, pass);
277   return TRUE;
278 }
279 
280 static float
calculate_downscale_factor(float width,float height,float sigma)281 calculate_downscale_factor (float width,
282                             float height,
283                             float sigma)
284 {
285   float downscale_factor = 1.f;
286   float scaled_width = width;
287   float scaled_height = height;
288   float scaled_sigma = sigma;
289 
290   /* This is the algorithm used by Firefox; keep downscaling until either the
291    * blur radius is lower than the threshold, or the downscaled texture is too
292    * small.
293    */
294   while (scaled_sigma > MAX_SIGMA &&
295          scaled_width > MIN_DOWNSCALE_SIZE &&
296          scaled_height > MIN_DOWNSCALE_SIZE)
297     {
298       downscale_factor *= 2.f;
299 
300       scaled_width = width / downscale_factor;
301       scaled_height = height / downscale_factor;
302       scaled_sigma = sigma / downscale_factor;
303     }
304 
305   return downscale_factor;
306 }
307 
308 static void
apply_blur_pass(BlurPass * pass)309 apply_blur_pass (BlurPass *pass)
310 {
311   CoglColor transparent;
312 
313   cogl_color_init_from_4ub (&transparent, 0, 0, 0, 0);
314 
315   cogl_framebuffer_clear (pass->framebuffer,
316                           COGL_BUFFER_BIT_COLOR,
317                           &transparent);
318 
319   cogl_framebuffer_draw_rectangle (pass->framebuffer,
320                                    pass->pipeline,
321                                    0, 0,
322                                    cogl_texture_get_width (pass->texture),
323                                    cogl_texture_get_height (pass->texture));
324 }
325 
326 static void
clear_blur_pass(BlurPass * pass)327 clear_blur_pass (BlurPass *pass)
328 {
329   g_clear_pointer (&pass->pipeline, cogl_object_unref);
330   g_clear_pointer (&pass->texture, cogl_object_unref);
331   g_clear_object (&pass->framebuffer);
332 }
333 
334 /**
335  * clutter_blur_new:
336  * @texture: a #CoglTexture
337  * @sigma: blur sigma
338  *
339  * Creates a new #ClutterBlur.
340  *
341  * Returns: (transfer full) (nullable): A newly created #ClutterBlur
342  */
343 ClutterBlur *
clutter_blur_new(CoglTexture * texture,float sigma)344 clutter_blur_new (CoglTexture *texture,
345                   float        sigma)
346 {
347   ClutterBlur *blur;
348   unsigned int height;
349   unsigned int width;
350   BlurPass *hpass;
351   BlurPass *vpass;
352 
353   g_return_val_if_fail (texture != NULL, NULL);
354   g_return_val_if_fail (sigma >= 0.0, NULL);
355 
356   width = cogl_texture_get_width (texture);
357   height = cogl_texture_get_height (texture);
358 
359   blur = g_new0 (ClutterBlur, 1);
360   blur->sigma = sigma;
361   blur->source_texture = cogl_object_ref (texture);
362   blur->downscale_factor = calculate_downscale_factor (width, height, sigma);
363 
364   if (G_APPROX_VALUE (sigma, 0.0, FLT_EPSILON))
365     goto out;
366 
367   vpass = &blur->pass[VERTICAL];
368   hpass = &blur->pass[HORIZONTAL];
369 
370   if (!setup_blur_pass (blur, vpass, VERTICAL, texture) ||
371       !setup_blur_pass (blur, hpass, HORIZONTAL, vpass->texture))
372     {
373       clutter_blur_free (blur);
374       return NULL;
375     }
376 
377 out:
378   return g_steal_pointer (&blur);
379 }
380 
381 /**
382  * clutter_blur_apply:
383  * @blur: a #ClutterBlur
384  *
385  * Applies the blur. The resulting texture can be retrieved by
386  * clutter_blur_get_texture().
387  */
388 void
clutter_blur_apply(ClutterBlur * blur)389 clutter_blur_apply (ClutterBlur *blur)
390 {
391   if (G_APPROX_VALUE (blur->sigma, 0.0, FLT_EPSILON))
392     return;
393 
394   apply_blur_pass (&blur->pass[VERTICAL]);
395   apply_blur_pass (&blur->pass[HORIZONTAL]);
396 }
397 
398 /**
399  * clutter_blur_get_texture:
400  * @blur: a #ClutterBlur
401  *
402  * Retrieves the texture where the blurred contents are stored. The
403  * contents are undefined until clutter_blur_apply() is called.
404  *
405  * Returns: (transfer none): a #CoglTexture
406  */
407 CoglTexture *
clutter_blur_get_texture(ClutterBlur * blur)408 clutter_blur_get_texture (ClutterBlur *blur)
409 {
410   if (G_APPROX_VALUE (blur->sigma, 0.0, FLT_EPSILON))
411     return blur->source_texture;
412   else
413     return blur->pass[HORIZONTAL].texture;
414 }
415 
416 /**
417  * clutter_blur_free:
418  * @blur: A #ClutterBlur
419  *
420  * Frees @blur.
421  */
422 void
clutter_blur_free(ClutterBlur * blur)423 clutter_blur_free (ClutterBlur *blur)
424 {
425   g_assert (blur);
426 
427   clear_blur_pass (&blur->pass[VERTICAL]);
428   clear_blur_pass (&blur->pass[HORIZONTAL]);
429   cogl_clear_object (&blur->source_texture);
430   g_free (blur);
431 }
432