diff --git a/data/gala.metainfo.xml.in b/data/gala.metainfo.xml.in
index d5763d941..359d4af3d 100644
--- a/data/gala.metainfo.xml.in
+++ b/data/gala.metainfo.xml.in
@@ -36,6 +36,7 @@
Pinch gestures are switching between workspaces instead of zooming
+ Dock background blinks when Greyscale filter is enabled
diff --git a/lib/SimpleShaderEffect.vala b/lib/SimpleShaderEffect.vala
new file mode 100644
index 000000000..accf7f8f6
--- /dev/null
+++ b/lib/SimpleShaderEffect.vala
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2026 elementary, Inc.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/**
+ * {@link Clutter.Effect} implementation to apply shaders quickly and easily.
+ * The main difference between Gala.SimpleShaderEffect and Clutter.ShaderEffect is that
+ * we don't use {@link Clutter.OffscreenEffect} that enlarges the texture for fractional scaling which
+ * can produce some graphical glitches.
+ */
+public abstract class Gala.SimpleShaderEffect : Clutter.Effect {
+ /**
+ * Fallback shader that outputs the original content.
+ */
+ public const string FALLBACK_SHADER = "uniform sampler2D tex; void main () { cogl_color_out = texture2D (tex, cogl_tex_coord0_in.xy); }";
+
+ private Cogl.Program program;
+ private Cogl.Pipeline pipeline;
+ private Cogl.Framebuffer framebuffer;
+ private Cogl.Texture texture;
+ private int texture_width;
+ private int texture_height;
+
+ protected SimpleShaderEffect (string shader_source) {
+ unowned var ctx = Clutter.get_default_backend ().get_cogl_context ();
+
+ var shader = Cogl.Shader.create (FRAGMENT);
+ shader.source (shader_source);
+
+ program = Cogl.Program.create ();
+ program.attach_shader (shader);
+ program.link ();
+
+ pipeline = new Cogl.Pipeline (ctx);
+ pipeline.set_user_program (program);
+ }
+
+ private bool update_framebuffer () {
+ var actor_box = actor.get_allocation_box ();
+ var new_width = (int) actor_box.get_width ();
+ var new_height = (int) actor_box.get_height ();
+
+ if (new_width <= 0 || new_height <= 0) {
+ warning ("SimpleShaderEffect: Couldn't update framebuffers, incorrect size");
+ return false;
+ }
+
+ if (texture_width == new_width && texture_height == new_height && framebuffer != null) {
+ return true;
+ }
+
+ unowned var ctx = Clutter.get_default_backend ().get_cogl_context ();
+
+#if HAS_MUTTER46
+ texture = new Cogl.Texture2D.with_size (ctx, new_width, new_height);
+#else
+ var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, new_width, new_height);
+ try {
+ texture = new Cogl.Texture2D.from_data (ctx, new_width, new_height, Cogl.PixelFormat.BGRA_8888_PRE, surface.get_stride (), surface.get_data ());
+ } catch (Error e) {
+ critical ("SimpleShaderEffect: Couldn't create texture: %s", e.message);
+ return false;
+ }
+#endif
+
+ pipeline.set_layer_texture (0, texture);
+ framebuffer = new Cogl.Offscreen.with_texture (texture);
+
+ Graphene.Matrix projection = {};
+ projection.init_translate ({ -new_width / 2.0f, -new_height / 2.0f, 0.0f });
+ projection.scale (2.0f / new_width, -2.0f / new_height, 1.0f);
+
+ framebuffer.set_projection_matrix (projection);
+
+ texture_width = new_width;
+ texture_height = new_height;
+
+ return true;
+ }
+
+ public override void paint_node (Clutter.PaintNode node, Clutter.PaintContext paint_context, Clutter.EffectPaintFlags flags) {
+ var actor_node = new Clutter.ActorNode (actor, 255);
+
+ if (BYPASS_EFFECT in flags || !update_framebuffer ()) {
+ node.add_child (actor_node);
+ return;
+ }
+
+ var layer_node = new Clutter.LayerNode.to_framebuffer (framebuffer, pipeline);
+ layer_node.add_rectangle ({0, 0, texture_width, texture_height});
+ layer_node.add_child (actor_node);
+
+ node.add_child (layer_node);
+ }
+
+ private bool get_and_validate_uniform_location (string uniform, out int uniform_location) {
+ uniform_location = program.get_uniform_location (uniform);
+
+ if (uniform_location == -1) {
+ warning ("Can't update uniform '%s'", uniform);
+ return false;
+ }
+
+ return true;
+ }
+
+ protected void set_uniform_1f (string uniform, float value) {
+ int uniform_location;
+ if (get_and_validate_uniform_location (uniform, out uniform_location)) {
+ program.set_uniform_1f (uniform_location, value);
+ }
+ }
+
+ protected void set_uniform_1i (string uniform, int value) {
+ int uniform_location;
+ if (get_and_validate_uniform_location (uniform, out uniform_location)) {
+ program.set_uniform_1i (uniform_location, value);
+ }
+ }
+
+ protected void set_uniform_float (string uniform, int n_components, float[] value) {
+ int uniform_location;
+ if (get_and_validate_uniform_location (uniform, out uniform_location)) {
+ program.set_uniform_float (uniform_location, n_components, value);
+ }
+ }
+
+ protected void set_uniform_int (string uniform, int n_components, int[] value) {
+ int uniform_location;
+ if (get_and_validate_uniform_location (uniform, out uniform_location)) {
+ program.set_uniform_int (uniform_location, n_components, value);
+ }
+ }
+
+ protected void set_uniform_matrix (string uniform, int dimensions, bool transpose, float[] value) {
+ int uniform_location;
+ if (get_and_validate_uniform_location (uniform, out uniform_location)) {
+ program.set_uniform_matrix (uniform_location, dimensions, transpose, value);
+ }
+ }
+}
diff --git a/lib/meson.build b/lib/meson.build
index 737db46f1..8b1703ae7 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -22,6 +22,7 @@ gala_lib_sources = files(
'Plugin.vala',
'RoundedCornersEffect.vala',
'ShadowEffect.vala',
+ 'SimpleShaderEffect.vala',
'Text.vala',
'Utils.vala',
'WindowIcon.vala',
diff --git a/src/ColorFilters/ColorblindnessCorrectionEffect.vala b/src/ColorFilters/ColorblindnessCorrectionEffect.vala
index 4cb5416f4..f589ce9dc 100644
--- a/src/ColorFilters/ColorblindnessCorrectionEffect.vala
+++ b/src/ColorFilters/ColorblindnessCorrectionEffect.vala
@@ -1,9 +1,9 @@
/*
- * Copyright 2023 elementary, Inc.
+ * Copyright 2023-2026 elementary, Inc.
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-public class Gala.ColorblindnessCorrectionEffect : Clutter.ShaderEffect {
+public class Gala.ColorblindnessCorrectionEffect : SimpleShaderEffect {
public const string EFFECT_NAME = "colorblindness-correction-filter";
private int _mode;
@@ -11,7 +11,7 @@ public class Gala.ColorblindnessCorrectionEffect : Clutter.ShaderEffect {
get { return _mode; }
construct set {
_mode = value;
- set_uniform_value ("COLORBLIND_MODE", _mode);
+ set_uniform_1i ("COLORBLIND_MODE", _mode);
}
}
private double _strength;
@@ -19,13 +19,13 @@ public class Gala.ColorblindnessCorrectionEffect : Clutter.ShaderEffect {
get { return _strength; }
construct set {
_strength = value;
- set_uniform_value ("STRENGTH", value);
+ set_uniform_1f ("STRENGTH", (float) value);
queue_repaint ();
}
}
public bool pause_for_screenshot {
set {
- set_uniform_value ("PAUSE_FOR_SCREENSHOT", (int) value);
+ set_uniform_1i ("PAUSE_FOR_SCREENSHOT", (int) value);
queue_repaint ();
}
}
@@ -36,23 +36,18 @@ public class Gala.ColorblindnessCorrectionEffect : Clutter.ShaderEffect {
public Clutter.Actor? transition_actor { get; set; default = null; }
public ColorblindnessCorrectionEffect (int mode, double strength) {
- Object (
-#if HAS_MUTTER48
- shader_type: Cogl.ShaderType.FRAGMENT,
-#else
- shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
-#endif
- mode: mode,
- strength: strength
- );
-
+ string shader_source;
try {
var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/colorblindness-correction.frag", GLib.ResourceLookupFlags.NONE);
- set_shader_source ((string) bytes.get_data ());
+ shader_source = (string) bytes.get_data ();
} catch (Error e) {
- critical ("Unable to load colorblindness-correction.frag: %s", e.message);
+ warning ("Unable to load colorblindness-correction.frag: %s", e.message);
+ shader_source = FALLBACK_SHADER;
}
+ base (shader_source);
+ this.mode = mode;
+ this.strength = strength;
pause_for_screenshot = false;
}
}
diff --git a/src/ColorFilters/MonochromeEffect.vala b/src/ColorFilters/MonochromeEffect.vala
index c5b8034d7..87b1f0b25 100644
--- a/src/ColorFilters/MonochromeEffect.vala
+++ b/src/ColorFilters/MonochromeEffect.vala
@@ -1,23 +1,23 @@
/*
- * Copyright 2023 elementary, Inc.
+ * Copyright 2023-2026 elementary, Inc.
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-public class Gala.MonochromeEffect : Clutter.ShaderEffect {
+public class Gala.MonochromeEffect : SimpleShaderEffect {
public const string EFFECT_NAME = "monochrome-filter";
private double _strength;
public double strength {
get { return _strength; }
- construct set {
+ set {
_strength = value;
- set_uniform_value ("STRENGTH", value);
+ set_uniform_1f ("STRENGTH", (float) value);
queue_repaint ();
}
}
public bool pause_for_screenshot {
set {
- set_uniform_value ("PAUSE_FOR_SCREENSHOT", (int) value);
+ set_uniform_1i ("PAUSE_FOR_SCREENSHOT", (int) value);
queue_repaint ();
}
}
@@ -28,22 +28,17 @@ public class Gala.MonochromeEffect : Clutter.ShaderEffect {
public Clutter.Actor? transition_actor { get; set; default = null; }
public MonochromeEffect (double strength) {
- Object (
-#if HAS_MUTTER48
- shader_type: Cogl.ShaderType.FRAGMENT,
-#else
- shader_type: Clutter.ShaderType.FRAGMENT_SHADER,
-#endif
- strength: strength
- );
-
+ string shader_source;
try {
- var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/monochrome.frag", GLib.ResourceLookupFlags.NONE);
- set_shader_source ((string) bytes.get_data ());
+ var bytes = GLib.resources_lookup_data ("/io/elementary/desktop/gala/shaders/monochrome.frag", NONE);
+ shader_source = (string) bytes.get_data ();
} catch (Error e) {
- critical ("Unable to load monochrome.frag: %s", e.message);
+ warning ("Unable to load monochrome.frag: %s", e.message);
+ shader_source = FALLBACK_SHADER;
}
+ base (shader_source);
+ this.strength = strength;
pause_for_screenshot = false;
}
}