🎉 Add support for GNOME 51

This commit is contained in:
Tristan Tarrant
2026-08-26 11:33:34 +02:00
committed by Simon Schneegans
parent f71ed24ae0
commit a66f9ca71f
7 changed files with 181 additions and 93 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
}, },
"gettext-domain": "burn-my-windows", "gettext-domain": "burn-my-windows",
"settings-schema": "org.gnome.shell.extensions.burn-my-windows", "settings-schema": "org.gnome.shell.extensions.burn-my-windows",
"shell-version": ["45", "46", "47", "48", "49", "50"], "shell-version": ["45", "46", "47", "48", "49", "50", "51"],
"url": "https://github.com/Schneegans/Burn-My-Windows", "url": "https://github.com/Schneegans/Burn-My-Windows",
"version": 48 "version": 48
} }
+166 -78
View File
@@ -27,16 +27,13 @@ import * as utils from './utils.js';
// This is the base class for all shaders of Burn-My-Windows. It automagically loads // // This is the base class for all shaders of Burn-My-Windows. It automagically loads //
// the shader's source code from the resource file resources/shaders/<nick>.glsl and // // the shader's source code from the resource file resources/shaders/<nick>.glsl and //
// ensures that some standard uniforms are always updated. // // ensures that some standard uniforms are always updated. //
// Using Shell.GLSLEffect as a base class has some benefits and some drawbacks. The // // Since GNOME 51, Shell.GLSLEffect has been removed from gnome-shell. On newer //
// main benefit when compared to Clutter.ShaderEffect is that setting uniforms of types // // versions, we therefore use Clutter.ShaderEffect instead. It provides similar //
// vec2, vec3 or vec4 is supported via the API (with Clutter.ShaderEffect the GJS // // functionality: The GLSL snippet is cached per class (not per instance) and float //
// binding does not work properly). However, there are two drawbacks: On the one hand, // // uniforms can be set. The main difference is that uniforms are addressed by name //
// the shader source code is cached statically - this means if we want to have a // // instead of by location. To keep the rest of the code base unchanged, //
// different shader, we have to derive a new class. Therefore, each effect has to // // get_uniform_location() returns the uniform's name as an opaque handle in this case, //
// derive its own class from the class below. This is encapsulated in the // // which is understood by set_uniform_float() and setUniform1i(). //
// ShaderFactory, however it is some really awkward code. The other drawback is the //
// hard-coded use of straight alpha (as opposed to premultiplied). This makes the //
// shaders a bit more complicated than required. //
// // // //
// The Shader fires three signals: // // The Shader fires three signals: //
// * begin-animation: This is called each time a new animation is started. It can // // * begin-animation: This is called each time a new animation is started. It can //
@@ -44,11 +41,13 @@ import * as utils from './utils.js';
// animation. // // animation. //
// * update-animation: This is called at each frame during the animation. It can be // // * update-animation: This is called at each frame during the animation. It can be //
// used to set uniforms which change during the animation. // // used to set uniforms which change during the animation. //
// * end-animation: This is called when the animation is stopped. This can be // // * end-animation: This is called when the animation is stopped. It can be //
// used to clean up any resources. // used to clean up any resources. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
export var Shader = GObject.registerClass({ const _useShaderEffect = !Shell.GLSLEffect;
const _annotations = {
Signals: { Signals: {
'begin-animation': { 'begin-animation': {
param_types: [ param_types: [
@@ -59,49 +58,69 @@ export var Shader = GObject.registerClass({
'update-animation': {param_types: [GObject.TYPE_DOUBLE]}, 'update-animation': {param_types: [GObject.TYPE_DOUBLE]},
'end-animation': {} 'end-animation': {}
} }
}, };
class Shader extends Shell.GLSLEffect { // This is called from vfunc_paint_target() on both backends. It emits the update-
// -------------------------------------------- // animation signal, sets the blend mode of the pipeline and writes the progress uniform.
// The constructor automagically loads the shader's source code (in function _paintTarget(self) {
// vfunc_build_pipeline()) from the resource file resources/shaders/<nick>.glsl self.emit('update-animation', self._progress);
// resolving any #includes in this file.
_init(nick) {
this._nick = nick;
// This will call vfunc_build_pipeline(). // Starting with GNOME 44.2, the alpha channel is not written to by default. We need
super._init(); // to undo this. It is a pity that we have to do this here, as it is not really
// required to be done each frame. But it's the only place where we can do it.
// https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2650
self.get_pipeline().set_blend(
'RGBA = ADD (SRC_COLOR * (SRC_COLOR[A]), DST_COLOR * (1-SRC_COLOR[A]))');
self.set_uniform_float(self._uProgress, 1, [self._progress]);
}
// Splits the given GLSL source code into the declarations (everything before "void main")
// and the body of the main function.
function _splitShaderCode(code) {
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
return [code.substr(0, match.index), match[1]];
}
// This is called from the constructor of both backends after super._init() has been
// called. It sets up the standard uniforms and the timeline which drives the animation.
function _initRest(self) {
// These will be updated during the animation. // These will be updated during the animation.
this._progress = 0; self._progress = 0;
this._time = 0; self._time = 0;
// Store standard uniform locations. // Store standard uniform locations.
this._uForOpening = this.get_uniform_location('uForOpening'); self._uForOpening = self.get_uniform_location('uForOpening');
this._uIsFullscreen = this.get_uniform_location('uIsFullscreen'); self._uIsFullscreen = self.get_uniform_location('uIsFullscreen');
this._uProgress = this.get_uniform_location('uProgress'); self._uProgress = self.get_uniform_location('uProgress');
this._uDuration = this.get_uniform_location('uDuration'); self._uDuration = self.get_uniform_location('uDuration');
this._uSize = this.get_uniform_location('uSize'); self._uSize = self.get_uniform_location('uSize');
this._uPadding = this.get_uniform_location('uPadding'); self._uPadding = self.get_uniform_location('uPadding');
// Create a timeline to drive the animation. // Create a timeline to drive the animation.
this._timeline = new Clutter.Timeline(); self._timeline = new Clutter.Timeline();
// Call updateAnimation() once a frame. // Call updateAnimation() once a frame.
this._timeline.connect('new-frame', (t) => { self._timeline.connect('new-frame', (t) => {
if (this._testMode) { if (self._testMode) {
this.updateAnimation(0.5); self.updateAnimation(0.5);
} else { } else {
this.updateAnimation(t.get_progress()); self.updateAnimation(t.get_progress());
} }
}); });
// Clean up if the animation finished or was interrupted. // Clean up if the animation finished or was interrupted.
this._timeline.connect('stopped', (t, finished) => { self._timeline.connect('stopped', (t, finished) => {
this.endAnimation(); self.endAnimation();
}); });
} }
// The methods shared by both backends. They are added to the prototype of the class
// below before it is registered with GObject.
const _methods = {
// This is called once each time the shader is used. // This is called once each time the shader is used.
beginAnimation(settings, forOpening, testMode, duration, actor) { beginAnimation(settings, forOpening, testMode, duration, actor) {
if (this._timeline.is_playing()) { if (this._timeline.is_playing()) {
@@ -149,7 +168,7 @@ export var Shader = GObject.registerClass({
this.set_uniform_float(this._uSize, 2, [actor.width, actor.height]); this.set_uniform_float(this._uSize, 2, [actor.width, actor.height]);
this.emit('begin-animation', settings, forOpening, testMode, actor); this.emit('begin-animation', settings, forOpening, testMode, actor);
} },
// This is called at each frame during the animation. // This is called at each frame during the animation.
updateAnimation(progress) { updateAnimation(progress) {
@@ -159,7 +178,7 @@ export var Shader = GObject.registerClass({
this._progress = progress; this._progress = progress;
this.queue_repaint(); this.queue_repaint();
} },
// This will stop any running animation and emit the end-animation signal. // This will stop any running animation and emit the end-animation signal.
endAnimation() { endAnimation() {
@@ -179,43 +198,19 @@ export var Shader = GObject.registerClass({
global.end_work(); global.end_work();
this.emit('end-animation'); this.emit('end-animation');
},
// Sets an integer uniform on the pipeline. This is used by some effects to bind texture
// samplers. On newer versions, where uniforms are addressed by name, the location has
// to be looked up first.
setUniform1i(handle, value) {
if (_useShaderEffect) {
const pipeline = this.get_pipeline();
pipeline.set_uniform_1i(pipeline.get_uniform_location(handle), value);
} else {
this.get_pipeline().set_uniform_1i(handle, value);
} }
},
// This is called by the constructor. This means, it's only called when the
// effect is used for the first time.
vfunc_build_pipeline() {
// Shell.GLSLEffect requires the declarations and the main source code as separate
// strings. As it's more convenient to store the in one GLSL file, we use a regex
// here to split the source code in two parts.
const code = this._loadShaderResource(`/shaders/${this._nick}.frag`);
// Match anything between the curly brackets of "void main() {...}".
const regex = RegExp('void main *\\(\\) *\\{([\\S\\s]+)\\}');
const match = regex.exec(code);
const declarations = code.substr(0, match.index);
const main = match[1];
this.add_glsl_snippet(
Cogl.SnippetHook ? Cogl.SnippetHook.FRAGMENT : Shell.SnippetHook.FRAGMENT,
declarations, main, true);
}
// We use this vfunc to trigger the update as it allows calling this.get_pipeline() in
// the handler. This could still be null if called from the updateAnimation() above.
vfunc_paint_target(...params) {
this.emit('update-animation', this._progress);
// Starting with GNOME 44.2, the alpha channel is not written to by default. We need
// to undo this. It is a pity that we have to do this here, as it is not really
// required to be done each frame. But it's the only place where we can do it.
// https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2650
this.get_pipeline().set_blend(
'RGBA = ADD (SRC_COLOR * (SRC_COLOR[A]), DST_COLOR * (1-SRC_COLOR[A]))');
this.set_uniform_float(this._uProgress, 1, [this._progress]);
super.vfunc_paint_target(...params);
}
// --------------------------------------------------------------------- private stuff // --------------------------------------------------------------------- private stuff
@@ -228,4 +223,97 @@ export var Shader = GObject.registerClass({
// Add a trailing newline. Else the GLSL compiler complains... // Add a trailing newline. Else the GLSL compiler complains...
return common + '\n' + code + '\n'; return common + '\n' + code + '\n';
} }
}); };
// On newer versions, uniforms are addressed by name. We return the name as an opaque
// handle which is understood by set_uniform_float() and setUniform1i().
if (_useShaderEffect) {
_methods.get_uniform_location = function(name) {
return name;
};
}
let Shader;
if (_useShaderEffect) {
class ShaderImpl extends Clutter.ShaderEffect {
// The GLSL snippet (see vfunc_get_static_snippet()) is created lazily when the
// effect is used for the first time.
_init(nick) {
this._nick = nick;
super._init();
_initRest(this);
}
// This is called once per class (not per instance) to create the GLSL snippet which
// is then shared by all instances of this class.
vfunc_get_static_snippet() {
const code = this._loadShaderResource(`/shaders/${this._nick}.frag`);
const [declarations, main] = _splitShaderCode(code);
const snippet = Cogl.Snippet.new(Cogl.SnippetHook.FRAGMENT, declarations, null);
snippet.set_replace(main);
return snippet;
}
// We use this vfunc to trigger the update as it allows calling this.get_pipeline() in
// the handlers. This could still be null if called from the updateAnimation() above.
vfunc_paint_target(...params) {
_paintTarget(this);
super.vfunc_paint_target(...params);
}
// Clutter.ShaderEffect.set_uniform_float() ignores the component count and derives
// the uniform's size from the length of the values array. Since parseColor() returns
// four values, vec3 uniforms would be uploaded with four components, which is an
// invalid glUniform4fv() call. The uniform would then be left at its default value
// (typically black). We therefore cut the array to the declared size.
set_uniform_float(name, n_components, values) {
super.set_uniform_float(name, n_components, values.slice(0, n_components));
}
}
Object.assign(ShaderImpl.prototype, _methods);
Shader = GObject.registerClass(_annotations, ShaderImpl);
} else {
class ShaderImpl extends Shell.GLSLEffect {
// The constructor automagically loads the shader's source code (in
// vfunc_build_pipeline()) from the resource file resources/shaders/<nick>.glsl
// resolving any #includes in this file.
_init(nick) {
this._nick = nick;
// This will call vfunc_build_pipeline().
super._init();
_initRest(this);
}
// This is called by the constructor. This means, it's only called when the
// effect is used for the first time.
vfunc_build_pipeline() {
// Shell.GLSLEffect requires the declarations and the main source code as separate
// strings. As it's more convenient to store the in one GLSL file, we use a regex
// here to split the source code in two parts.
const code = this._loadShaderResource(`/shaders/${this._nick}.frag`);
const [declarations, main] = _splitShaderCode(code);
this.add_glsl_snippet(
Cogl.SnippetHook ? Cogl.SnippetHook.FRAGMENT : Shell.SnippetHook.FRAGMENT,
declarations, main, true);
}
// We use this vfunc to trigger the update as it allows calling this.get_pipeline() in
// the handlers. This could still be null if called from the updateAnimation() above.
vfunc_paint_target(...params) {
_paintTarget(this);
super.vfunc_paint_target(...params);
}
}
Object.assign(ShaderImpl.prototype, _methods);
Shader = GObject.registerClass(_annotations, ShaderImpl);
}
export {Shader};
+1 -1
View File
@@ -105,7 +105,7 @@ export default class Effect {
// Bind the shard texture. // Bind the shard texture.
pipeline.set_layer_texture(1, this._shardTexture.get_texture()); pipeline.set_layer_texture(1, this._shardTexture.get_texture());
pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT);
pipeline.set_uniform_1i(shader._uShardTexture, 1); shader.setUniform1i(shader._uShardTexture, 1);
}); });
}); });
} }
+1 -1
View File
@@ -75,7 +75,7 @@ export default class Effect {
// Bind the font texture. // Bind the font texture.
pipeline.set_layer_texture(1, this._fontTexture.get_texture()); pipeline.set_layer_texture(1, this._fontTexture.get_texture());
pipeline.set_uniform_1i(shader._uFontTexture, 1); shader.setUniform1i(shader._uFontTexture, 1);
}); });
}); });
} }
+1 -1
View File
@@ -69,7 +69,7 @@ export default class Effect {
// Bind the brush texture. // Bind the brush texture.
pipeline.set_layer_texture(1, this._brushTexture.get_texture()); pipeline.set_layer_texture(1, this._brushTexture.get_texture());
pipeline.set_uniform_1i(shader._uBrushTexture, 1); shader.setUniform1i(shader._uBrushTexture, 1);
}); });
}); });
} }
+1 -1
View File
@@ -79,7 +79,7 @@ export default class Effect {
// Bind the dust texture. // Bind the dust texture.
pipeline.set_layer_texture(1, this._dustTexture.get_texture()); pipeline.set_layer_texture(1, this._dustTexture.get_texture());
pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT); pipeline.set_layer_wrap_mode(1, Cogl.PipelineWrapMode.REPEAT);
pipeline.set_uniform_1i(shader._uDustTexture, 1); shader.setUniform1i(shader._uDustTexture, 1);
}); });
}); });
} }
+1 -1
View File
@@ -78,7 +78,7 @@ export default class Effect {
// Bind the claw texture. // Bind the claw texture.
pipeline.set_layer_texture(1, this._clawTexture.get_texture()); pipeline.set_layer_texture(1, this._clawTexture.get_texture());
pipeline.set_uniform_1i(shader._uClawTexture, 1); shader.setUniform1i(shader._uClawTexture, 1);
}); });
}); });
} }