git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit7620dae4a07e3e8848cdbb500bf46cc77c1a6cb2
parent2905eca7f4
authorLucas Galante <[email protected]>
date2026-06-30 16:04
Fix window decoration negotiation mode and transparent borders clipping, and complete migration to wlroots-0.20 and scenefx-0.5

 build.rs                                           |    9 +-
 scenefx/.github/workflows/flake-update.yml         |   24 +
 scenefx/.gitignore                                 |    6 +-
 scenefx/README.md                                  |   64 +
 scenefx/examples/scene-graph.c                     |    3 +-
 scenefx/flake.lock                                 |   45 +-
 scenefx/flake.nix                                  |   83 +-
 scenefx/include/render/color.h                     |  111 ++
 scenefx/include/render/fx_renderer/fx_renderer.h   |   28 +-
 scenefx/include/render/fx_renderer/shaders.h       |   82 +-
 scenefx/include/render/pass.h                      |    4 +
 scenefx/include/render/pixel_format.h              |    5 +
 scenefx/include/render/tracy.h                     |  178 +++
 .../render/fx_renderer/fx_effect_framebuffers.h    |   30 -
 .../render/fx_renderer/fx_offscreen_buffers.h      |   32 +
 scenefx/include/scenefx/render/pass.h              |   58 +-
 scenefx/include/scenefx/types/fx/blur_data.h       |    4 +-
 scenefx/include/scenefx/types/fx/clipped_region.h  |   94 +-
 scenefx/include/scenefx/types/fx/corner_location.h |   26 -
 scenefx/include/scenefx/types/linked_node.h        |   33 +
 scenefx/include/scenefx/types/wlr_scene.h          |  211 ++-
 scenefx/include/types/fx/clipped_region.h          |   27 +
 scenefx/include/types/wlr_output.h                 |    3 +
 scenefx/include/types/wlr_scene.h                  |    2 +
 scenefx/meson.build                                |   60 +-
 scenefx/meson_options.txt                          |    2 +
 scenefx/render/color.c                             |  433 ++++++
 scenefx/render/color_fallback.c                    |   24 +
 scenefx/render/color_lcms2.c                       |  113 ++
 scenefx/render/egl.c                               |   43 +-
 .../render/fx_renderer/fx_effect_framebuffers.c    |   57 -
 scenefx/render/fx_renderer/fx_framebuffer.c        |   75 +-
 scenefx/render/fx_renderer/fx_offscreen_buffers.c  |   92 ++
 scenefx/render/fx_renderer/fx_pass.c               |  830 +++++++-----
 scenefx/render/fx_renderer/fx_renderer.c           |  144 +-
 scenefx/render/fx_renderer/gles2/meson.build       |    2 -
 .../fx_renderer/gles2/shaders/corner_alpha.frag    |   35 -
 .../render/fx_renderer/gles2/shaders/meson.build   |   30 -
 scenefx/render/fx_renderer/gles2/shaders/quad.frag |   31 -
 .../fx_renderer/gles2/shaders/quad_round.frag      |   64 -
 scenefx/render/fx_renderer/gles3/meson.build       |    1 -
 .../render/fx_renderer/gles3/shaders/blur1.frag    |   23 -
 .../render/fx_renderer/gles3/shaders/blur2.frag    |   27 -
 .../fx_renderer/gles3/shaders/blur_effects.frag    |   58 -
 .../fx_renderer/gles3/shaders/box_shadow.frag      |   96 --
 .../render/fx_renderer/gles3/shaders/common.vert   |   15 -
 .../fx_renderer/gles3/shaders/corner_alpha.frag    |   35 -
 scenefx/render/fx_renderer/gles3/shaders/embed.sh  |   11 -
 .../render/fx_renderer/gles3/shaders/gradient.frag |   41 -
 .../render/fx_renderer/gles3/shaders/meson.build   |   30 -
 scenefx/render/fx_renderer/gles3/shaders/quad.frag |   31 -
 .../fx_renderer/gles3/shaders/quad_grad.frag       |   25 -
 .../fx_renderer/gles3/shaders/quad_grad_round.frag |   46 -
 .../fx_renderer/gles3/shaders/quad_round.frag      |   64 -
 scenefx/render/fx_renderer/gles3/shaders/tex.frag  |   65 -
 scenefx/render/fx_renderer/meson.build             |   13 +-
 scenefx/render/fx_renderer/pixel_format.c          |   11 +
 scenefx/render/fx_renderer/shaders.c               |  244 ++--
 .../fx_renderer/{gles2 => }/shaders/blur1.frag     |    0
 .../fx_renderer/{gles2 => }/shaders/blur2.frag     |    0
 .../{gles2 => }/shaders/blur_effects.frag          |    2 +-
 .../{gles2 => }/shaders/box_shadow.frag            |   11 +-
 .../fx_renderer/{gles2 => }/shaders/common.vert    |    0
 .../render/fx_renderer/shaders/corner_alpha.frag   |   74 +
 .../fx_renderer/{gles2 => }/shaders/embed.sh       |    0
 .../fx_renderer/{gles2 => }/shaders/gradient.frag  |    0
 scenefx/render/fx_renderer/shaders/meson.build     |   31 +
 scenefx/render/fx_renderer/shaders/quad.frag       |   45 +
 .../fx_renderer/{gles2 => }/shaders/quad_grad.frag |    0
 .../{gles2 => }/shaders/quad_grad_round.frag       |   26 +-
 scenefx/render/fx_renderer/shaders/quad_round.frag |   72 +
 .../fx_renderer/{gles2 => }/shaders/tex.frag       |   53 +-
 scenefx/render/fx_renderer/tracy.c                 |  212 +++
 scenefx/render/meson.build                         |   12 +-
 scenefx/render/pixel_format.c                      |   97 ++
 .../packagefiles/tracy_v0_13_1_build_fix.patch     |   22 +
 scenefx/subprojects/tracy.wrap                     |    8 +
 scenefx/tinywl/Makefile                            |   17 +-
 scenefx/tinywl/meson.build                         |    2 +-
 scenefx/tinywl/tinywl.c                            |   53 +-
 scenefx/types/fx/blur_data.c                       |   36 +
 scenefx/types/fx/clipped_region.c                  |   67 +-
 scenefx/types/fx/corner_location.c                 |   76 --
 scenefx/types/fx/meson.build                       |    1 -
 scenefx/types/meson.build                          |    3 +-
 scenefx/types/output/{wlr_output.c => output.c}    |    8 +
 scenefx/types/scene/linked_node.c                  |   78 ++
 scenefx/types/scene/wlr_scene.c                    | 1418 +++++++++++++-------
 scenefx/util/array.c                               |    2 +-
 src/server/cursor.rs                               |    6 +-
 src/server/input_manager.rs                        |    4 +-
 src/server/wlroots_log_wrapper.c                   |  101 +-
 src/server/xdg_toplevel.rs                         |   13 +-
 93 files changed, 4293 insertions(+), 2290 deletions(-)

diff --git a/build.rs b/build.rs
index 6089ae5..09eadd4 100644
--- a/build.rs
+++ b/build.rs
@@ -31,16 +31,17 @@ fn main() {
     let scenefx_include_paths = vec![scenefx_inc1, scenefx_inc2, scenefx_inc3];
 
     println!("cargo:rustc-link-search=native={}/scenefx/build", manifest_dir);
-    println!("cargo:rustc-link-lib=static=scenefx-0.4");
+    println!("cargo:rustc-link-lib=static=scenefx-0.5");
     println!("cargo:rustc-link-lib=dylib=GLESv2");
     println!("cargo:rustc-link-lib=dylib=EGL");
     println!("cargo:rustc-link-lib=dylib=drm");
     println!("cargo:rustc-link-lib=dylib=gbm");
+    println!("cargo:rustc-link-lib=dylib=lcms2");
 
     let wlroots = pkg_config::Config::new()
-        .atleast_version("0.19.0")
-        .probe("wlroots-0.19")
-        .expect("wlroots-0.19 is required");
+        .atleast_version("0.20.0")
+        .probe("wlroots-0.20")
+        .expect("wlroots-0.20 is required");
 
     let wl_server = pkg_config::probe_library("wayland-server")
         .expect("wayland-server is required");
diff --git a/scenefx/.github/workflows/flake-update.yml b/scenefx/.github/workflows/flake-update.yml
new file mode 100644
index 0000000..d94e1dc
--- /dev/null
+++ b/scenefx/.github/workflows/flake-update.yml
@@ -0,0 +1,24 @@
+name: "Flake.lock: update Nix dependencies"
+
+on:
+  workflow_dispatch: # allows manual triggering
+  schedule:
+    - cron: '0 0 * * 0' # runs weekly on Sunday at 00:00
+
+jobs:
+  nix-flake-update:
+    permissions:
+      contents: write
+      id-token: write
+      issues: write
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: DeterminateSystems/determinate-nix-action@v3
+      - uses: DeterminateSystems/update-flake-lock@main
+        with:
+          pr-title: "chore: update Nix flake inputs" # Title of PR to be created
+          pr-labels: |                  # Labels to be set on the PR
+            dependencies
+            automated
diff --git a/scenefx/.gitignore b/scenefx/.gitignore
index cb9554a..93805e6 100644
--- a/scenefx/.gitignore
+++ b/scenefx/.gitignore
@@ -1,3 +1,7 @@
-/subprojects/
+subprojects/*
 build/
 .cache/
+result
+
+!subprojects/tracy.wrap
+!subprojects/packagefiles
diff --git a/scenefx/README.md b/scenefx/README.md
index 8bdca76..bb32a23 100644
--- a/scenefx/README.md
+++ b/scenefx/README.md
@@ -4,6 +4,13 @@ wlroots is the de-facto library for building wayland compositors, and its scene
 
 **Please note: while SceneFX is in use by SwayFX version 0.4, it is not yet ready for usage by other compositors. Please refer to the [1.0 milestone](https://github.com/wlrfx/scenefx/milestone/2) to track the remaining tasks for our stable 1.0 release**
 
+## Compositors Using SceneFX
+Plenty of popular wayland compositors are using SceneFX to render eyecandy, including:
+- [SwayFX](https://github.com/WillPower3309/swayfx)
+- [MangoWC](https://github.com/DreamMaoMao/mangowc)
+- [mwc](https://github.com/nikoloc/mwc)
+- dwl [with a patch](https://codeberg.org/dwl/dwl-patches/src/branch/main/stale-patches/scenefx)
+
 ## Installation
 <a href="https://repology.org/project/scenefx/versions"><img src="https://repology.org/badge/vertical-allrepos/scenefx.svg"/></a>
 
@@ -31,6 +38,63 @@ Install like so:
 sudo ninja -C build/ install
 ```
 
+## Troubleshooting
+
+### Using scenefx features breaks the compositor
+
+This issue might be caused by compiling scenefx and wlroots with
+Clang compiler and thin LTO(`-flto=thin`) option enabled. Try to
+compile the libraries without LTO optimizations or with GCC compiler instead.
+
+## Debugging
+
+SceneFX includes the same debugging tools and environment variables as upstream wlroots does, but with some extra goodies.
+
+### Environment variables:
+
+- `WLR_RENDERER_ALLOW_SOFTWARE=1`: Use software rendering
+- `WLR_SCENE_DEBUG_DAMAGE=rerender`: Re-render the whole display on each commit (don't use damage)
+- `WLR_SCENE_DEBUG_DAMAGE=highlight`: Highlights where damage has occurred (where SwayFX get's re-rendered)
+- `WLR_SCENE_DISABLE_DIRECT_SCANOUT=1`: Disable direct scanout (always composites, even fullscreen windows)
+- `WLR_SCENE_DISABLE_VISIBILITY=1`: Disables culling of non-visible regions of a window/buffer (an example would be a small window fully covered by an opaque window)
+- `WLR_SCENE_HIGHLIGHT_TRANSPARENT_REGION=1`: Highlights the transparent areas of a window/buffer
+- `WLR_EGL_NO_MODIFIERS=1`: Disables modifiers for EGL
+
+### Tracy profiling
+
+Optional [Tracy](https://github.com/wolfpld/tracy) profiling can be enabled for an extra good view of when and what is happening.
+
+#### Enabling:
+
+Note: These instructions will enable basic profiling
+
+1. Add SceneFX as a subproject to your compositor
+2. Import it as a subproject dependency
+3. Run `meson subprojects download` to download the tracy project into the subproject directory
+4. Compile SceneFX with `-Dtracy_enable=true` (and `--buildtype=debugoptimized` if using meson).
+5. Start your compositor
+6. Start the `tracy-profiler` and connect to the running compositor (The version of the profiler should not matter, but if any issues are encountered, try using a version that matches the subproject).
+
+To enable more advanced profiling, the compositor in question needs to be run by the root user which comes with its own drawbacks, like DBus not working out of the box. A recommended way of doing this is by running your compositor with the `-E` sudo flag, such as `sudo -E sway`.
+
+To enable DBus, you need to give the root user access to the DBus session bus by adding the following lines to the `/etc/dbus-1/session-local.conf` file (you might have to create said file), and reboot your system.
+
+```xml
+<!-- /etc/dbus-1/session-local.conf -->
+<!-- Allow root to access session bus -->
+<busconfig>
+  <policy context="mandatory">
+    <allow user="root"/>
+  </policy>
+</busconfig>
+```
+
+#### Additional tracy documentation
+
+The links below are helpful when learning how to use tracy:
+
+- https://github.com/wolfpld/tracy/releases/latest/download/tracy.pdf
+- https://www.youtube.com/watch?v=ghXk3Bk5F2U
 
 ---
 [Join our Discord](https://discord.gg/qsSx397rkh)
diff --git a/scenefx/examples/scene-graph.c b/scenefx/examples/scene-graph.c
index 910d60b..7e5162c 100644
--- a/scenefx/examples/scene-graph.c
+++ b/scenefx/examples/scene-graph.c
@@ -1,7 +1,6 @@
 #include <assert.h>
 #include <getopt.h>
 #include <scenefx/render/fx_renderer/fx_renderer.h>
-#include <scenefx/types/fx/corner_location.h>
 #include <scenefx/types/wlr_scene.h>
 #include <stdbool.h>
 #include <stdio.h>
@@ -145,7 +144,7 @@ static void server_handle_new_surface(struct wl_listener *listener,
 	surface->scene_surface =
 		wlr_scene_surface_create(&server->scene->tree, wlr_surface);
 	wlr_scene_buffer_set_corner_radius(
-			surface->scene_surface->buffer, corner_radius, CORNER_LOCATION_ALL);
+			surface->scene_surface->buffer, corner_radius);
 
 	wlr_scene_node_set_position(&surface->scene_surface->buffer->node,
 			pos + border_width, pos + border_width);
diff --git a/scenefx/flake.lock b/scenefx/flake.lock
index 2e0310d..0bc8a1a 100644
--- a/scenefx/flake.lock
+++ b/scenefx/flake.lock
@@ -1,12 +1,32 @@
 {
   "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": [
+          "systems"
+        ]
+      },
+      "locked": {
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1750605355,
-        "narHash": "sha256-xT8cPLTxlktxI9vSdoBlAVK7dXgd8IK59j7ZwzkkhnI=",
+        "lastModified": 1781607440,
+        "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "3078b9a9e75f1790e6d6ef9955fdc6a2d1740cc6",
+        "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
         "type": "github"
       },
       "original": {
@@ -18,7 +38,24 @@
     },
     "root": {
       "inputs": {
-        "nixpkgs": "nixpkgs"
+        "flake-utils": "flake-utils",
+        "nixpkgs": "nixpkgs",
+        "systems": "systems"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1689347949,
+        "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
+        "owner": "nix-systems",
+        "repo": "default-linux",
+        "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default-linux",
+        "type": "github"
       }
     }
   },
diff --git a/scenefx/flake.nix b/scenefx/flake.nix
index 34b9257..95d81cb 100644
--- a/scenefx/flake.nix
+++ b/scenefx/flake.nix
@@ -1,15 +1,27 @@
 {
   description = "Scenefx development environment";
-  inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
-  outputs =
-    { self, nixpkgs, ... }:
-    let
-      mkPackages = pkgs: {
-        scenefx = pkgs.callPackage (
-          { wlroots_0_19, ... }:
-          pkgs.stdenv.mkDerivation {
+
+  inputs = {
+    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
+
+    systems.url = "github:nix-systems/default-linux";
+
+	flake-utils = {
+	  url = "github:numtide/flake-utils";
+	  inputs.systems.follows = "systems";
+	};
+  };
+
+  outputs = { self, nixpkgs, flake-utils, ... }:
+    flake-utils.lib.eachDefaultSystem (system:
+      let
+	    pkgs = import nixpkgs { inherit system; };
+
+      in {
+        packages = rec {
+          scenefx-git = pkgs.stdenv.mkDerivation {
             pname = "scenefx";
-            version = "0.4.0-git";
+            version = "git";
             src = ./.;
             outputs = [
               "out"
@@ -33,7 +45,11 @@
               mesa # gbm
               wayland # wayland-server
               wayland-protocols
-              wlroots_0_19
+              wlroots_0_20
+              libgbm
+              libxcb
+              libxcb-wm
+              lcms2
             ];
 
             meta = with pkgs.lib; {
@@ -42,44 +58,19 @@
               license = licenses.mit;
               platforms = platforms.linux;
             };
-          }
-        ) { };
-      };
-
-      targetSystems = [
-        "aarch64-linux"
-        "x86_64-linux"
-      ];
-      pkgsFor = system: import nixpkgs { inherit system; };
-      forEachSystem = f: nixpkgs.lib.genAttrs targetSystems (system: f (pkgsFor system));
-    in
-    {
-      overlays = rec {
-        default = insert;
-        override = _: prev: mkPackages prev;
-        insert = _: prev: mkPackages (pkgsFor prev.system);
-      };
+          };
 
-      packages = forEachSystem (
-        pkgs: (mkPackages pkgs) // { default = self.packages.${pkgs.system}.scenefx; }
-      );
+          default = scenefx-git;
+		};
 
-      devShells = forEachSystem (pkgs: {
-        default = pkgs.mkShell {
+        devShells.default = pkgs.mkShell {
           name = "scenefx-shell";
-          inputsFrom = [
-            self.packages.${pkgs.system}.scenefx
-            pkgs.wlroots_0_19
-          ];
-          shellHook = ''
-            (
-              # Copy the nix version of wlroots into the project
-              mkdir -p "$PWD/subprojects" && cd "$PWD/subprojects"
-              cp -R --no-preserve=mode,ownership ${pkgs.wlroots_0_19.src} wlroots
-            )'';
-        };
-      });
+	      inputsFrom = [ self.packages.${system}.scenefx-git ];
+		  hardeningDisable = [ "fortify" ];
+	    };
 
-      formatter = forEachSystem (pkgs: pkgs.nixfmt-rfc-style);
-    };
+        formatter = pkgs.nixfmt;
+      }
+    );
 }
+
diff --git a/scenefx/include/render/color.h b/scenefx/include/render/color.h
new file mode 100644
index 0000000..60c841d
--- /dev/null
+++ b/scenefx/include/render/color.h
@@ -0,0 +1,111 @@
+#ifndef RENDER_COLOR_H
+#define RENDER_COLOR_H
+
+#include <stdint.h>
+#include <wlr/render/color.h>
+#include <wlr/util/addon.h>
+
+enum wlr_color_transform_type {
+	COLOR_TRANSFORM_INVERSE_EOTF,
+	COLOR_TRANSFORM_LCMS2,
+	COLOR_TRANSFORM_LUT_3X1D,
+	COLOR_TRANSFORM_MATRIX,
+	COLOR_TRANSFORM_PIPELINE,
+};
+
+struct wlr_color_transform {
+	int ref_count;
+	struct wlr_addon_set addons; // per-renderer helper state
+
+	enum wlr_color_transform_type type;
+};
+
+struct wlr_color_transform_inverse_eotf {
+	struct wlr_color_transform base;
+
+	enum wlr_color_transfer_function tf;
+};
+
+/**
+ * The formula is approximated via three 1D look-up tables. The flat lut_3x1d
+ * array has a length of 3 * dim.
+ *
+ * The offset of a color value for a given channel and color index is:
+ *
+ *     offset = channel_index * dim + color_index
+ */
+struct wlr_color_transform_lut_3x1d {
+	struct wlr_color_transform base;
+
+	uint16_t *lut_3x1d;
+	size_t dim;
+};
+
+struct wlr_color_transform_matrix {
+	struct wlr_color_transform base;
+
+	float matrix[9];
+};
+
+struct wlr_color_transform_pipeline {
+	struct wlr_color_transform base;
+
+	struct wlr_color_transform **transforms;
+	size_t len;
+};
+
+void wlr_color_transform_init(struct wlr_color_transform *tr,
+	enum wlr_color_transform_type type);
+
+/**
+ * Get a struct wlr_color_transform_lcms2 from a generic struct wlr_color_transform.
+ * Asserts that the base type is COLOR_TRANSFORM_LCMS2.
+ */
+struct wlr_color_transform_lcms2 *color_transform_lcms2_from_base(
+	struct wlr_color_transform *tr);
+
+void color_transform_lcms2_finish(struct wlr_color_transform_lcms2 *tr);
+
+/**
+ * Evaluate a LCMS2 color transform for a given RGB triplet.
+ */
+void color_transform_lcms2_eval(struct wlr_color_transform_lcms2 *tr,
+	float out[static 3], const float in[static 3]);
+
+/**
+ * Gets a wlr_color_transform_inverse_eotf from a generic wlr_color_transform.
+ * Asserts that the base type is COLOR_TRANSFORM_INVERSE_EOTF
+ */
+struct wlr_color_transform_inverse_eotf *wlr_color_transform_inverse_eotf_from_base(
+	struct wlr_color_transform *tr);
+
+/**
+ * Get a struct wlr_color_transform_lut_3x1d from a generic
+ * struct wlr_color_transform. Asserts that the base type is
+ * COLOR_TRANSFORM_LUT_3X1D.
+ */
+struct wlr_color_transform_lut_3x1d *color_transform_lut_3x1d_from_base(
+	struct wlr_color_transform *tr);
+
+/**
+ * Create a simplified / normalized wlr_color_transform pipeline.
+ * `transforms` may contain NULL transforms, they will be interpreted as the
+ * identity transform, and removed.
+ * `*result` may be set to a transform of a type different from
+ * `wlr_color_transform_pipeline`, or to NULL if all input transforms are NULL
+ */
+bool color_transform_compose(struct wlr_color_transform **result,
+	struct wlr_color_transform **transforms, size_t len);
+
+/**
+ * Compute the matrix to convert RGB color values to CIE 1931 XYZ.
+ */
+void wlr_color_primaries_to_xyz(const struct wlr_color_primaries *primaries, float matrix[static 9]);
+
+/**
+ * Get default luminances for a transfer function.
+ */
+void wlr_color_transfer_function_get_default_luminance(enum wlr_color_transfer_function tf,
+	struct wlr_color_luminances *lum);
+
+#endif
diff --git a/scenefx/include/render/fx_renderer/fx_renderer.h b/scenefx/include/render/fx_renderer/fx_renderer.h
index ea8e7fb..ba6cc4b 100644
--- a/scenefx/include/render/fx_renderer/fx_renderer.h
+++ b/scenefx/include/render/fx_renderer/fx_renderer.h
@@ -13,6 +13,7 @@
 #include <wlr/util/box.h>
 
 #include "render/fx_renderer/shaders.h"
+#include "render/tracy.h"
 
 struct fx_framebuffer;
 
@@ -49,9 +50,12 @@ struct fx_framebuffer {
 	struct wlr_addon addon;
 };
 
-/** Should only be used with custom fbs */
+/**
+ * Should only be used with custom fbs.
+ * Note: Does not bind back to the default Framebuffer!
+ */
 void fx_framebuffer_get_or_create_custom(struct fx_renderer *fx_renderer,
-		struct wlr_output *output, struct wlr_swapchain *swapchain,
+		struct wlr_allocator *allocator, int width, int height, bool has_alpha,
 		struct fx_framebuffer **fx_buffer, bool *failed);
 
 struct fx_framebuffer *fx_framebuffer_get_or_create(struct fx_renderer *renderer,
@@ -59,6 +63,10 @@ struct fx_framebuffer *fx_framebuffer_get_or_create(struct fx_renderer *renderer
 
 void fx_framebuffer_bind(struct fx_framebuffer *buffer);
 
+/**
+ * Destroy the fx_framebuffer.
+ * Note: Doesn't drop the wlr_buffer, so should only be used internally.
+ */
 void fx_framebuffer_destroy(struct fx_framebuffer *buffer);
 
 ///
@@ -172,16 +180,25 @@ struct fx_renderer {
 		PFNGLGETQUERYOBJECTIVEXTPROC glGetQueryObjectivEXT;
 		PFNGLGETQUERYOBJECTUI64VEXTPROC glGetQueryObjectui64vEXT;
 		PFNGLGETINTEGER64VEXTPROC glGetInteger64vEXT;
+		TRACY_FN(
+			PFNGLGETQUERYIVEXTPROC glGetQueryivEXT;
+		)
 	} procs;
 
 	struct {
 		struct quad_shader quad;
+		struct quad_shader quad_clip;
 		struct quad_grad_shader quad_grad;
 		struct quad_round_shader quad_round;
 		struct quad_grad_round_shader quad_grad_round;
+
 		struct tex_shader tex_rgba;
 		struct tex_shader tex_rgbx;
 		struct tex_shader tex_ext;
+		struct tex_shader tex_effects_rgba;
+		struct tex_shader tex_effects_rgbx;
+		struct tex_shader tex_effects_ext;
+
 		struct box_shadow_shader box_shadow;
 		struct blur_shader blur1;
 		struct blur_shader blur2;
@@ -190,10 +207,11 @@ struct fx_renderer {
 
 	struct wl_list buffers; // fx_framebuffer.link
 	struct wl_list textures; // fx_texture.link
+	struct wl_list offscreen_buffers; // fx_offscreen_buffers.link
 
-	// Set to true when 'wlr_renderer_begin_buffer_pass' is called instead of
-	// our custom 'fx_renderer_begin_buffer_pass' function
-	bool basic_renderer;
+	TRACY_FN(
+		struct tracy_data *tracy_data;
+	)
 };
 
 #endif
diff --git a/scenefx/include/render/fx_renderer/shaders.h b/scenefx/include/render/fx_renderer/shaders.h
index 650b6e4..bd05f70 100644
--- a/scenefx/include/render/fx_renderer/shaders.h
+++ b/scenefx/include/render/fx_renderer/shaders.h
@@ -3,12 +3,14 @@
 
 #include <GLES2/gl2.h>
 #include <stdbool.h>
+#include <scenefx/types/fx/clipped_region.h>
+#include "types/fx/clipped_region.h"
 
 struct fx_renderer;
 
 GLuint compile_shader(GLuint type, const GLchar *src);
 
-GLuint link_program(const GLchar *frag_src, GLint client_version);
+GLuint link_program(const GLchar *frag_src);
 
 bool check_gl_ext(const char *exts, const char *ext);
 
@@ -20,21 +22,31 @@ enum fx_tex_shader_source {
 	SHADER_SOURCE_TEXTURE_EXTERNAL = 3,
 };
 
+struct shader_corner_radii {
+	GLint top_left;
+	GLint top_right;
+	GLint bottom_left;
+	GLint bottom_right;
+};
+
+void uniform_corner_radii_set(const struct shader_corner_radii *uniform,
+		const struct fx_corner_fradii *corners);
+
 struct quad_shader {
 	GLuint program;
 	GLint proj;
 	GLint color;
 	GLint pos_attrib;
 
-	GLint clip_size;
-	GLint clip_position;
-	GLint clip_radius_top_left;
-	GLint clip_radius_top_right;
-	GLint clip_radius_bottom_left;
-	GLint clip_radius_bottom_right;
+	// Only used for the effects shader
+	struct {
+		GLint clip_size;
+		GLint clip_position;
+		struct shader_corner_radii clip_radius;
+	} effects;
 };
 
-bool link_quad_program(struct quad_shader *shader, GLint client_version);
+bool link_quad_program(struct quad_shader *shader, bool clip);
 
 struct quad_grad_shader {
 	int max_len;
@@ -52,7 +64,7 @@ struct quad_grad_shader {
 	GLint blend;
 };
 
-bool link_quad_grad_program(struct quad_grad_shader *shader, GLint client_version, int max_len);
+bool link_quad_grad_program(struct quad_grad_shader *shader, int max_len);
 
 struct quad_round_shader {
 	GLuint program;
@@ -62,21 +74,15 @@ struct quad_round_shader {
 	GLint size;
 	GLint position;
 
-	GLint radius_top_left;
-	GLint radius_top_right;
-	GLint radius_bottom_left;
-	GLint radius_bottom_right;
+	struct shader_corner_radii radius;
 
 	GLint clip_size;
 	GLint clip_position;
-	GLint clip_radius_top_left;
-	GLint clip_radius_top_right;
-	GLint clip_radius_bottom_left;
-	GLint clip_radius_bottom_right;
+	struct shader_corner_radii clip_radius;
 	GLint fade_inset;
 };
 
-bool link_quad_round_program(struct quad_round_shader *shader, GLint client_version);
+bool link_quad_round_program(struct quad_round_shader *shader);
 
 struct quad_grad_round_shader {
 	GLuint program;
@@ -95,15 +101,12 @@ struct quad_grad_round_shader {
 	GLint count;
 	GLint blend;
 
-	GLint radius_top_left;
-	GLint radius_top_right;
-	GLint radius_bottom_left;
-	GLint radius_bottom_right;
+	struct shader_corner_radii radius;
 
 	int max_len;
 };
 
-bool link_quad_grad_round_program(struct quad_grad_round_shader *shader, GLint client_version, int max_len);
+bool link_quad_grad_round_program(struct quad_grad_round_shader *shader, int max_len);
 
 struct tex_shader {
 	GLuint program;
@@ -112,17 +115,23 @@ struct tex_shader {
 	GLint tex;
 	GLint alpha;
 	GLint pos_attrib;
-	GLint size;
-	GLint position;
-	GLint radius_top_left;
-	GLint radius_top_right;
-	GLint radius_bottom_left;
-	GLint radius_bottom_right;
 
 	GLint discard_transparent;
+
+	// Only used for the effects shader
+	struct {
+		GLint size;
+		GLint position;
+		struct shader_corner_radii radius;
+
+		GLint clip_size;
+		GLint clip_position;
+		struct shader_corner_radii clip_radius;
+	} effects;
 };
 
-bool link_tex_program(struct tex_shader *shader, GLint client_version, enum fx_tex_shader_source source);
+bool link_tex_program(struct tex_shader *shader, enum fx_tex_shader_source source,
+		bool effects);
 
 struct box_shadow_shader {
 	GLuint program;
@@ -136,13 +145,10 @@ struct box_shadow_shader {
 
 	GLint clip_position;
 	GLint clip_size;
-	GLint clip_radius_top_left;
-	GLint clip_radius_top_right;
-	GLint clip_radius_bottom_left;
-	GLint clip_radius_bottom_right;
+	struct shader_corner_radii clip_radius;
 };
 
-bool link_box_shadow_program(struct box_shadow_shader *shader, GLint client_version);
+bool link_box_shadow_program(struct box_shadow_shader *shader);
 
 struct blur_shader {
 	GLuint program;
@@ -154,8 +160,8 @@ struct blur_shader {
 	GLint halfpixel;
 };
 
-bool link_blur1_program(struct blur_shader *shader, GLint client_version);
-bool link_blur2_program(struct blur_shader *shader, GLint client_version);
+bool link_blur1_program(struct blur_shader *shader);
+bool link_blur2_program(struct blur_shader *shader);
 
 struct blur_effects_shader {
 	GLuint program;
@@ -169,6 +175,6 @@ struct blur_effects_shader {
 	GLfloat saturation;
 };
 
-bool link_blur_effects_program(struct blur_effects_shader *shader, GLint client_version);
+bool link_blur_effects_program(struct blur_effects_shader *shader);
 
 #endif
diff --git a/scenefx/include/render/pass.h b/scenefx/include/render/pass.h
index 0ee9550..33b8624 100644
--- a/scenefx/include/render/pass.h
+++ b/scenefx/include/render/pass.h
@@ -13,4 +13,8 @@ struct fx_render_texture_options fx_render_texture_options_default(
 struct fx_render_rect_options fx_render_rect_options_default(
 		const struct wlr_render_rect_options *base);
 
+struct fx_gles_render_pass *fx_begin_buffer_pass(struct fx_framebuffer *buffer,
+	struct wlr_egl_context *prev_ctx, struct fx_render_timer *timer,
+	struct wlr_drm_syncobj_timeline *signal_timeline, uint64_t signal_point);
+
 #endif
diff --git a/scenefx/include/render/pixel_format.h b/scenefx/include/render/pixel_format.h
index 0b20aa5..788dc74 100644
--- a/scenefx/include/render/pixel_format.h
+++ b/scenefx/include/render/pixel_format.h
@@ -63,4 +63,9 @@ enum wl_shm_format convert_drm_format_to_wl_shm(uint32_t fmt);
  */
 bool pixel_format_has_alpha(uint32_t fmt);
 
+/**
+ * Return true if the DRM FourCC fmt belongs to a YCbCr colorspace family, false otherwise.
+ */
+bool pixel_format_is_ycbcr(uint32_t fmt);
+
 #endif
diff --git a/scenefx/include/render/tracy.h b/scenefx/include/render/tracy.h
new file mode 100644
index 0000000..9d42371
--- /dev/null
+++ b/scenefx/include/render/tracy.h
@@ -0,0 +1,178 @@
+#ifndef TRACY_H
+#define TRACY_H
+
+#ifdef TRACY_ENABLE
+#define TRACY_FN(...) __VA_ARGS__
+#else
+#define TRACY_FN(...) // noop
+#endif  // TRACY_ENABLE
+
+#ifdef TRACY_ENABLE
+#include <tracy/TracyC.h>
+
+struct fx_renderer;
+
+struct tracy_data;
+struct tracy_gpu_zone_context {
+	struct tracy_data *tracy_data;
+	bool is_active;
+};
+
+// Private functions: Don't use these outside of this file!
+
+void tracy_gpu_zone_begin(struct tracy_data *tracy_data, struct tracy_gpu_zone_context *out_ctx,
+		const int line, const char *source, const char *func, const char *name);
+void tracy_gpu_zone_end(struct tracy_gpu_zone_context *ctx);
+
+void tracy_gpu_context_collect(struct tracy_data *tracy_data);
+void tracy_gpu_context_destroy(struct tracy_data *tracy_data);
+struct tracy_data *tracy_gpu_context_new(struct fx_renderer *renderer);
+#endif  // TRACY_ENABLE
+
+#define TRACY_WHEN_CONNECTED(fn) \
+	TRACY_FN(if (TracyCIsConnected) { fn })
+
+/**
+ * Frame
+ */
+
+#define TRACY_MARK_FRAME TRACY_FN(TracyCFrameMark)
+
+#define TRACY_RESCHEDULE TRACY_FN(TracyCFrameMark)
+
+/**
+ * Messaging
+ */
+
+#define TRACY_MESSAGE(...) \
+	TRACY_FN({ \
+		int len = snprintf(NULL, 0, __VA_ARGS__) + 1; \
+		char str[len]; \
+		snprintf(str, len, __VA_ARGS__); \
+		TracyCMessage(str, len); \
+	})
+#define TRACY_MESSAGE_COLOR(color, ...) \
+	TRACY_FN({ \
+		int len = snprintf(NULL, 0, __VA_ARGS__) + 1; \
+		char str[len]; \
+		snprintf(str, len, __VA_ARGS__); \
+		TracyCMessageC(str, len, color); \
+	})
+#define TRACY_MESSAGE_ERROR(...) \
+	TRACY_FN( \
+		TRACY_MESSAGE_COLOR(0xFF0000, __VA_ARGS__) \
+	)
+
+/**
+ * Zone
+ */
+
+#define TRACY_ZONE_START \
+	TRACY_FN( \
+		TracyCZone(ctx, true) \
+	)
+#define TRACY_ZONE_START_N(name) \
+	TRACY_FN( \
+		TracyCZoneN(ctx, name, true) \
+	)
+#define TRACY_ZONE_END \
+	TRACY_FN( \
+		TRACY_ZONE_TEXT_f("Success On Line: %d", __LINE__) \
+		TracyCZoneEnd(ctx); \
+	)
+#define TRACY_ZONE_END_FAIL \
+	TRACY_FN( \
+		TRACY_ZONE_TEXT_f("Fail On Line: %d", __LINE__) \
+		TracyCZoneEnd(ctx); \
+	)
+
+/**
+ * Zone Helpers
+ */
+
+#define TRACY_ZONE_TEXT(txt, size) \
+	TRACY_FN(TracyCZoneText(ctx, txt, size);)
+#define TRACY_ZONE_TEXT_f(...) \
+	TRACY_FN({ \
+		int len = snprintf(NULL, 0, __VA_ARGS__) + 1; \
+		char str[len]; \
+		snprintf(str, len, __VA_ARGS__); \
+		TracyCZoneText(ctx, str, len); \
+	})
+#define TRACY_ZONE_NAME(txt, size) \
+	TRACY_FN( \
+		TracyCZoneName(ctx, txt, size); \
+	)
+#define TRACY_ZONE_NAME_f(...) \
+	TRACY_FN({ \
+		int len = snprintf(NULL, 0, __VA_ARGS__) + 1; \
+		char str[len]; \
+		snprintf(str, len, __VA_ARGS__); \
+		TracyCZoneName(ctx, str, len); \
+	})
+#define TRACY_ZONE_COLOR(color) \
+	TRACY_FN( \
+		TracyCZoneColor(ctx, color); \
+	)
+#define TRACY_ZONE_VALUE(value) \
+	TRACY_FN( \
+		TracyCZoneValue(ctx, value); \
+	)
+
+/**
+ * GPU Zone
+ */
+
+#define TRACY_GPU_ZONE_START_NAME(renderer, name) \
+	TRACY_FN( \
+		struct tracy_gpu_zone_context gpu_ctx; \
+		tracy_gpu_zone_begin(renderer->tracy_data, &gpu_ctx, __LINE__, __FILE__, __func__, name) \
+	)
+#define TRACY_GPU_ZONE_START(renderer) \
+	TRACY_FN( \
+		struct tracy_gpu_zone_context gpu_ctx; \
+		tracy_gpu_zone_begin(renderer->tracy_data, &gpu_ctx, __LINE__, __FILE__, __func__, __func__) \
+	)
+#define TRACY_GPU_ZONE_END \
+	TRACY_FN( \
+		tracy_gpu_zone_end(&gpu_ctx) \
+	)
+#define TRACY_GPU_ZONE_COLLECT(renderer) \
+	TRACY_FN( \
+		tracy_gpu_context_collect(renderer->tracy_data) \
+	)
+
+/**
+ * GPU Context
+ */
+
+#define TRACY_GPU_CONTEXT_NEW(renderer) \
+	TRACY_FN( \
+		tracy_gpu_context_new(renderer) \
+	)
+#define TRACY_GPU_CONTEXT_DESTROY(tracy_data) \
+	TRACY_FN( \
+		tracy_gpu_context_destroy(tracy_data); \
+	)
+
+/**
+ * GPU/CPU Zones Helpers
+ */
+
+#define TRACY_BOTH_ZONES_START(renderer) \
+	TRACY_FN( \
+		TRACY_ZONE_START \
+		TRACY_GPU_ZONE_START(renderer) \
+	)
+#define TRACY_BOTH_ZONES_END \
+	TRACY_FN( \
+		TRACY_ZONE_END \
+		TRACY_GPU_ZONE_END \
+	)
+#define TRACY_BOTH_ZONES_END_FAIL \
+	TRACY_FN( \
+		TRACY_ZONE_END_FAIL \
+		TRACY_GPU_ZONE_END \
+	)
+
+#endif  // !TRACY_H
diff --git a/scenefx/include/scenefx/render/fx_renderer/fx_effect_framebuffers.h b/scenefx/include/scenefx/render/fx_renderer/fx_effect_framebuffers.h
deleted file mode 100644
index 0938db4..0000000
--- a/scenefx/include/scenefx/render/fx_renderer/fx_effect_framebuffers.h
+++ /dev/null
@@ -1,30 +0,0 @@
-#ifndef _FX_EFFECT_FRAMEBUFFERS_H
-#define _FX_EFFECT_FRAMEBUFFERS_H
-
-#include <wlr/types/wlr_output.h>
-#include <wlr/util/addon.h>
-
-/**
- * Used to add effect framebuffers per output instead of every output sharing
- * them.
- */
-struct fx_effect_framebuffers {
-	struct wlr_addon addon;
-
-	// Contains the blurred background for tiled windows
-	struct fx_framebuffer *optimized_blur_buffer;
-	// Contains the original pixels to draw over the areas where artifact are visible
-	struct fx_framebuffer *blur_saved_pixels_buffer;
-	// Blur swaps between the two effects buffers everytime it scales the image
-	// Buffer used for effects
-	struct fx_framebuffer *effects_buffer;
-	// Swap buffer used for effects
-	struct fx_framebuffer *effects_buffer_swapped;
-
-	// The region where there's blur
-	pixman_region32_t blur_padding_region;
-};
-
-struct fx_effect_framebuffers *fx_effect_framebuffers_try_get(struct wlr_output *output);
-
-#endif
diff --git a/scenefx/include/scenefx/render/fx_renderer/fx_offscreen_buffers.h b/scenefx/include/scenefx/render/fx_renderer/fx_offscreen_buffers.h
new file mode 100644
index 0000000..c3e2e02
--- /dev/null
+++ b/scenefx/include/scenefx/render/fx_renderer/fx_offscreen_buffers.h
@@ -0,0 +1,32 @@
+#ifndef _FX_OFFSCREEN_BUFFERS_H
+#define _FX_OFFSCREEN_BUFFERS_H
+
+#include <wlr/types/wlr_output.h>
+#include <wlr/util/addon.h>
+
+/**
+ * Used to add effect framebuffers per output instead of every output sharing
+ * them.
+ */
+struct fx_offscreen_buffers {
+	struct wl_list link; // fx_renderer.offscreen_buffers
+	struct wlr_addon addon;
+
+	// Contains the blurred background for tiled windows
+	struct fx_framebuffer *optimized_blur_buffer;
+	// Contains the non-blurred background for tiled windows. Used for blurring
+	// optimized surfaces with an alpha. Just as inefficient as the regular blur.
+	struct fx_framebuffer *optimized_no_blur_buffer;
+	// Contains the original pixels to draw over the areas where artifact are visible
+	struct fx_framebuffer *blur_saved_pixels_buffer;
+	// Blur swaps between the two effects buffers every time it scales the image
+	// Buffer used for effects
+	struct fx_framebuffer *effects_buffer;
+	// Swap buffer used for effects
+	struct fx_framebuffer *effects_buffer_swapped;
+};
+
+void fx_offscreen_buffers_destroy(struct fx_offscreen_buffers *fbos);
+struct fx_offscreen_buffers *fx_offscreen_buffers_try_get(struct wlr_output *output);
+
+#endif
diff --git a/scenefx/include/scenefx/render/pass.h b/scenefx/include/scenefx/render/pass.h
index e8adb68..75f0fd5 100644
--- a/scenefx/include/scenefx/render/pass.h
+++ b/scenefx/include/scenefx/render/pass.h
@@ -7,37 +7,26 @@
 #include <wlr/render/swapchain.h>
 
 #include "render/egl.h"
-#include "scenefx/types/fx/clipped_region.h"
-#include "scenefx/types/fx/corner_location.h"
+#include "types/fx/clipped_region.h"
 
 struct fx_gles_render_pass {
 	struct wlr_render_pass base;
 	struct fx_framebuffer *buffer;
-	struct fx_effect_framebuffers *fx_effect_framebuffers;
-	struct wlr_output *output;
 	float projection_matrix[9];
 	struct wlr_egl_context prev_ctx;
 	struct fx_render_timer *timer;
 	struct wlr_drm_syncobj_timeline *signal_timeline;
 	uint64_t signal_point;
-};
-
-struct fx_buffer_pass_options {
-	const struct wlr_buffer_pass_options *base;
 
-	struct wlr_swapchain *swapchain;
+	// The region where there's blur
+	pixman_region32_t blur_padding_region;
+	bool has_blur;
+	// Contains output-specific framebuffers.
+	// NULL when no advanced effects like blur is being used in the current pass.
+	// Call `fx_render_pass_init_offscreen_buffers` to use advanced effects.
+	struct fx_offscreen_buffers *fx_offscreen_buffers;
 };
 
-/**
- * Begin a new render pass with the supplied destination buffer.
- *
- * Callers must call wlr_render_pass_submit() once they are done with the
- * render pass.
- */
-struct fx_gles_render_pass *fx_renderer_begin_buffer_pass(struct wlr_renderer *wlr_renderer,
-		struct wlr_buffer *wlr_buffer, struct wlr_output *output,
-		const struct fx_buffer_pass_options *options);
-
 struct fx_gradient {
 	float degree;
 	/* The full area the gradient fit too, for borders use the window size */
@@ -55,14 +44,14 @@ struct fx_gradient {
 struct fx_render_texture_options {
 	struct wlr_render_texture_options base;
 	const struct wlr_box *clip_box; // Used to clip csd. Ignored if NULL
-	enum corner_location corners;
-	int corner_radius;
+	struct fx_corner_fradii corners;
 	bool discard_transparent;
+	struct clipped_fregion clipped_region;
 };
 
 struct fx_render_rect_options {
 	struct wlr_render_rect_options base;
-	struct clipped_region clipped_region;
+	struct clipped_fregion clipped_region;
 };
 
 struct fx_render_rect_grad_options {
@@ -72,22 +61,20 @@ struct fx_render_rect_grad_options {
 
 struct fx_render_rounded_rect_options {
 	struct wlr_render_rect_options base;
-	int corner_radius;
-	enum corner_location corners;
-	struct clipped_region clipped_region;
+	struct fx_corner_fradii corners;
+	struct clipped_fregion clipped_region;
 	int fade_inset;
 };
 
 struct fx_render_rounded_rect_grad_options {
 	struct wlr_render_rect_options base;
 	struct fx_gradient gradient;
-	int corner_radius;
-	enum corner_location corners;
+	struct fx_corner_fradii corners;
 };
 
 struct fx_render_box_shadow_options {
 	struct wlr_box box;
-	struct clipped_region clipped_region;
+	struct clipped_fregion clipped_region;
 	/* Clip region, leave NULL to disable clipping */
 	const pixman_region32_t *clip;
 
@@ -98,13 +85,24 @@ struct fx_render_box_shadow_options {
 
 struct fx_render_blur_pass_options {
 	struct fx_render_texture_options tex_options;
-	pixman_region32_t *opaque_region;
 	struct fx_framebuffer *current_buffer;
 	struct blur_data *blur_data;
 	bool use_optimized_blur;
 	bool ignore_transparent;
+	float blur_strength;
+	struct fx_corner_fradii corners;
+	struct clipped_fregion clipped_region;
 };
 
+struct fx_gles_render_pass *fx_get_render_pass(struct wlr_render_pass *render_pass);
+
+/**
+ * Initializes the render pass offscreen buffers required for advanced effects
+ * like blur.
+ */
+bool fx_render_pass_init_offscreen_buffers(struct wlr_render_pass *render_pass,
+		struct wlr_output *output);
+
 /**
  * Render a fx texture.
  */
@@ -156,7 +154,7 @@ bool fx_render_pass_add_optimized_blur(struct fx_gles_render_pass *pass,
 /**
  * Render from one buffer to another
  */
-void fx_renderer_read_to_buffer(struct fx_gles_render_pass *pass,
+void fx_render_pass_read_to_buffer(struct fx_gles_render_pass *pass,
 		pixman_region32_t *region, struct fx_framebuffer *dst_buffer,
 		struct fx_framebuffer *src_buffer);
 
diff --git a/scenefx/include/scenefx/types/fx/blur_data.h b/scenefx/include/scenefx/types/fx/blur_data.h
index 82b3c98..967c2a0 100644
--- a/scenefx/include/scenefx/types/fx/blur_data.h
+++ b/scenefx/include/scenefx/types/fx/blur_data.h
@@ -6,7 +6,7 @@
 
 struct blur_data {
 	int num_passes;
-	int radius;
+	float radius;
 	float noise;
 	float brightness;
 	float contrast;
@@ -21,4 +21,6 @@ bool blur_data_should_parameters_blur_effects(struct blur_data *blur_data);
 
 int blur_data_calc_size(struct blur_data *blur_data);
 
+struct blur_data blur_data_apply_strength(struct blur_data *blur_data, float strength);
+
 #endif
diff --git a/scenefx/include/scenefx/types/fx/clipped_region.h b/scenefx/include/scenefx/types/fx/clipped_region.h
index 8449004..02bac5d 100644
--- a/scenefx/include/scenefx/types/fx/clipped_region.h
+++ b/scenefx/include/scenefx/types/fx/clipped_region.h
@@ -1,16 +1,98 @@
-#ifndef TYPES_FX_CLIPPED_REGION_H
-#define TYPES_FX_CLIPPED_REGION_H
+#ifndef SCENEFX_TYPES_FX_CLIPPED_REGION_H
+#define SCENEFX_TYPES_FX_CLIPPED_REGION_H
 
 #include <wlr/util/box.h>
 
-#include "scenefx/types/fx/corner_location.h"
+// uint16_t is a reasonable enough range for corner radius
+// and makes it 8 bytes wide, making it cheap with optimisations
+// Note: layout is on purpose to be clockwise
+struct fx_corner_radii {
+	uint16_t top_left;
+	uint16_t top_right;
+	uint16_t bottom_right;
+	uint16_t bottom_left;
+};
+
+#define CORNER_RADIUS_MAX (UINT16_MAX)
+
+static __always_inline uint16_t corner_radius_clamp(int radius) {
+	if (radius <= 0) {
+		return 0;
+	}
+
+	if (radius > CORNER_RADIUS_MAX) {
+		return CORNER_RADIUS_MAX;
+	}
+
+	return radius;
+}
+
+static __always_inline struct fx_corner_radii corner_radii_new(int top_left, int top_right, int bottom_right, int bottom_left) {
+	return (struct fx_corner_radii) {
+		corner_radius_clamp(top_left),
+		corner_radius_clamp(top_right),
+		corner_radius_clamp(bottom_right),
+		corner_radius_clamp(bottom_left),
+	};
+}
+
+static __always_inline struct fx_corner_radii corner_radii_all(int radius) {
+	return corner_radii_new(radius, radius, radius, radius);
+}
+
+static __always_inline struct fx_corner_radii corner_radii_none(void) {
+	return corner_radii_all(0);
+}
+
+#define corner_radii_func(name, tl, tr, br, bl) static __always_inline struct fx_corner_radii corner_radii_##name(int radius) { return corner_radii_new(tl, tr, br, bl); }
+
+corner_radii_func(top, radius, radius, 0, 0);
+corner_radii_func(bottom, 0, 0, radius, radius);
+corner_radii_func(left, radius, 0, 0, radius);
+corner_radii_func(right, 0, radius, radius, 0);
+
+struct fx_corner_radii fx_corner_radii_extend(struct fx_corner_radii corners, int extend);
+
+void fx_corner_radii_transform(enum wl_output_transform transform,
+		struct fx_corner_radii *corners);
+
+bool fx_corner_radii_eq(struct fx_corner_radii lhs, struct fx_corner_radii rhs);
+
+/**
+ * Retains the corner values from `input` based on if the corresponding corner value in `filter` is non-zero
+ *
+ * This can be compared to an `AND` operation
+ */
+static __always_inline struct fx_corner_radii fx_corner_radii_filter(struct fx_corner_radii input, struct fx_corner_radii filter) {
+	return corner_radii_new(
+		input.top_left && filter.top_left ? input.top_left : 0,
+		input.top_right && filter.top_right ? input.top_right : 0,
+		input.bottom_right && filter.bottom_right ? input.bottom_right : 0,
+		input.bottom_left && filter.bottom_left ? input.bottom_left : 0
+	);
+};
+
+/**
+ * Picks the value of the corner from `lhs` if non-zero, otherwise picks the corner value from `rhs`
+ *
+ * This can be compared to an `OR` operation
+ */
+static __always_inline struct fx_corner_radii fx_corner_radii_pick(struct fx_corner_radii lhs, struct fx_corner_radii rhs) {
+	return corner_radii_new(
+		lhs.top_left ? lhs.top_left : rhs.top_left,
+		lhs.top_right ? lhs.top_right : rhs.top_right,
+		lhs.bottom_right ? lhs.bottom_right : rhs.bottom_right,
+		lhs.bottom_left ? lhs.bottom_left : rhs.bottom_left
+	);
+};
+
+bool fx_corner_radii_is_empty(const struct fx_corner_radii* corners);
 
 struct clipped_region {
 	struct wlr_box area;
-	int corner_radius;
-	enum corner_location corners;
+	struct fx_corner_radii corners;
 };
 
 struct clipped_region clipped_region_get_default(void);
 
-#endif // !TYPES_FX_CLIPPED_REGION_H
+#endif // !SCENEFX_TYPES_FX_CLIPPED_REGION_H
diff --git a/scenefx/include/scenefx/types/fx/corner_location.h b/scenefx/include/scenefx/types/fx/corner_location.h
deleted file mode 100644
index cd94046..0000000
--- a/scenefx/include/scenefx/types/fx/corner_location.h
+++ /dev/null
@@ -1,26 +0,0 @@
-#ifndef TYPES_FX_CORNER_LOCATION_H
-#define TYPES_FX_CORNER_LOCATION_H
-
-#include <wayland-server-protocol.h>
-
-#define CORNER_LOCATION_LAST (1 << 3) + 1
-#define CORNER_LOCATION_COUNT 5
-
-enum corner_location {
-	CORNER_LOCATION_NONE = 0,
-	CORNER_LOCATION_TOP_LEFT = 1 << 0,
-	CORNER_LOCATION_TOP_RIGHT = 1 << 1,
-	CORNER_LOCATION_BOTTOM_RIGHT = 1 << 2,
-	CORNER_LOCATION_BOTTOM_LEFT = 1 << 3,
-	CORNER_LOCATION_TOP = CORNER_LOCATION_TOP_LEFT | CORNER_LOCATION_TOP_RIGHT,
-	CORNER_LOCATION_BOTTOM = CORNER_LOCATION_BOTTOM_LEFT | CORNER_LOCATION_BOTTOM_RIGHT,
-	CORNER_LOCATION_LEFT = CORNER_LOCATION_TOP_LEFT | CORNER_LOCATION_BOTTOM_LEFT,
-	CORNER_LOCATION_RIGHT = CORNER_LOCATION_TOP_RIGHT | CORNER_LOCATION_BOTTOM_RIGHT,
-	CORNER_LOCATION_ALL = CORNER_LOCATION_TOP_LEFT | CORNER_LOCATION_TOP_RIGHT
-		| CORNER_LOCATION_BOTTOM_LEFT | CORNER_LOCATION_BOTTOM_RIGHT,
-};
-
-void corner_location_transform(enum wl_output_transform transform,
-		enum corner_location *corners);
-
-#endif // !TYPES_FX_CORNER_LOCATION_H
diff --git a/scenefx/include/scenefx/types/linked_node.h b/scenefx/include/scenefx/types/linked_node.h
new file mode 100644
index 0000000..4d74241
--- /dev/null
+++ b/scenefx/include/scenefx/types/linked_node.h
@@ -0,0 +1,33 @@
+#ifndef TYPES_LINKED_NODES_H
+#include <stdbool.h>
+#define TYPES_LINKED_NODES_H
+
+/**
+ * A node in a link between two objects. Can be used to safely couple two
+ * objects together.
+ */
+struct linked_node {
+    struct link *link;
+};
+
+#define linked_node_init() \
+((struct linked_node) { \
+.link = NULL \
+})
+
+bool linked_nodes_are_linked(struct linked_node *node_1,
+        struct linked_node *node_2);
+
+void linked_node_init_link(struct linked_node *main_node,
+        struct linked_node *reference_node);
+
+struct linked_node *linked_nodes_get_sibling(struct linked_node *node);
+
+void linked_node_orphan(struct linked_node *node);
+
+void linked_node_unlink(struct linked_node *main_node,
+        struct linked_node *reference_node);
+
+void linked_node_destroy(struct linked_node *node);
+
+#endif
diff --git a/scenefx/include/scenefx/types/wlr_scene.h b/scenefx/include/scenefx/types/wlr_scene.h
index d1011c7..7e59617 100644
--- a/scenefx/include/scenefx/types/wlr_scene.h
+++ b/scenefx/include/scenefx/types/wlr_scene.h
@@ -30,7 +30,7 @@
 
 #include "scenefx/types/fx/blur_data.h"
 #include "scenefx/types/fx/clipped_region.h"
-#include "scenefx/types/fx/corner_location.h"
+#include "scenefx/types/linked_node.h"
 
 struct wlr_output;
 struct wlr_output_layout;
@@ -46,6 +46,8 @@ struct wlr_scene_output_layout;
 
 struct wlr_presentation;
 struct wlr_linux_dmabuf_v1;
+struct wlr_gamma_control_manager_v1;
+struct wlr_color_manager_v1;
 struct wlr_output_state;
 
 typedef bool (*wlr_scene_buffer_point_accepts_input_func_t)(
@@ -57,9 +59,10 @@ typedef void (*wlr_scene_buffer_iterator_func_t)(
 enum wlr_scene_node_type {
 	WLR_SCENE_NODE_TREE,
 	WLR_SCENE_NODE_RECT,
-	WLR_SCENE_NODE_SHADOW,
 	WLR_SCENE_NODE_BUFFER,
+	WLR_SCENE_NODE_SHADOW,
 	WLR_SCENE_NODE_OPTIMIZED_BLUR,
+	WLR_SCENE_NODE_BLUR,
 };
 
 /** A node is an object in the scene. */
@@ -107,11 +110,15 @@ struct wlr_scene {
 	// May be NULL
 	struct wlr_linux_dmabuf_v1 *linux_dmabuf_v1;
 	struct wlr_gamma_control_manager_v1 *gamma_control_manager_v1;
+	struct wlr_color_manager_v1 *color_manager_v1;
+
+	bool restack_xwayland_surfaces;
 
 	struct {
 		struct wl_listener linux_dmabuf_v1_destroy;
 		struct wl_listener gamma_control_manager_v1_destroy;
 		struct wl_listener gamma_control_manager_v1_set_gamma;
+		struct wl_listener color_manager_v1_destroy;
 
 		enum wlr_scene_debug_damage_option debug_damage_option;
 		bool direct_scanout;
@@ -133,8 +140,6 @@ struct wlr_scene_surface {
 		struct wlr_addon addon;
 
 		struct wl_listener outputs_update;
-		struct wl_listener output_enter;
-		struct wl_listener output_leave;
 		struct wl_listener output_sample;
 		struct wl_listener frame_done;
 		struct wl_listener surface_destroy;
@@ -147,11 +152,8 @@ struct wlr_scene_rect {
 	struct wlr_scene_node node;
 	int width, height;
 	float color[4];
-	int corner_radius;
-	enum corner_location corners;
-	bool backdrop_blur;
-	bool backdrop_blur_optimized;
 
+	struct fx_corner_radii corners;
 	bool accepts_input;
 	struct clipped_region clipped_region;
 	int fade_inset;
@@ -168,6 +170,21 @@ struct wlr_scene_shadow {
 	struct clipped_region clipped_region;
 };
 
+struct wlr_scene_blur {
+	struct wlr_scene_node node;
+	int width, height;
+
+	struct fx_corner_radii corners;
+	struct clipped_region clipped_region;
+
+	float strength;
+	float alpha;
+
+	bool should_only_blur_bottom_layer;
+
+	struct linked_node transparency_mask_source;
+};
+
 /** A scene-graph node telling SceneFX to render the optimized blur */
 struct wlr_scene_optimized_blur {
 	struct wlr_scene_node node;
@@ -184,6 +201,13 @@ struct wlr_scene_outputs_update_event {
 struct wlr_scene_output_sample_event {
 	struct wlr_scene_output *output;
 	bool direct_scanout;
+	struct wlr_drm_syncobj_timeline *release_timeline;
+	uint64_t release_point;
+};
+
+struct wlr_scene_frame_done_event {
+	struct wlr_scene_output *output;
+	struct timespec when;
 };
 
 /** A scene-graph node displaying a buffer */
@@ -198,7 +222,7 @@ struct wlr_scene_buffer {
 		struct wl_signal output_enter; // struct wlr_scene_output
 		struct wl_signal output_leave; // struct wlr_scene_output
 		struct wl_signal output_sample; // struct wlr_scene_output_sample_event
-		struct wl_signal frame_done; // struct timespec
+		struct wl_signal frame_done; // struct wlr_scene_frame_done_event
 	} events;
 
 	// May be NULL
@@ -207,17 +231,10 @@ struct wlr_scene_buffer {
 	/**
 	 * The output that the largest area of this buffer is displayed on.
 	 * This may be NULL if the buffer is not currently displayed on any
-	 * outputs. This is the output that should be used for frame callbacks,
-	 * presentation feedback, etc.
+	 * outputs.
 	 */
 	struct wlr_scene_output *primary_output;
 
-	int corner_radius;
-	bool backdrop_blur;
-	bool backdrop_blur_optimized;
-	bool backdrop_blur_ignore_transparent;
-	enum corner_location corners;
-
 	float opacity;
 	enum wlr_scale_filter_mode filter_mode;
 	struct wlr_fbox src_box;
@@ -225,6 +242,11 @@ struct wlr_scene_buffer {
 	enum wl_output_transform transform;
 	pixman_region32_t opaque_region;
 
+	enum wlr_color_transfer_function transfer_function;
+	enum wlr_color_named_primaries primaries;
+	enum wlr_color_encoding color_encoding;
+	enum wlr_color_range color_range;
+
 	struct {
 		uint64_t active_outputs;
 		struct wlr_texture *texture;
@@ -246,6 +268,9 @@ struct wlr_scene_buffer {
 		// as {R, G, B, A} where the max value of each component is UINT32_MAX
 		uint32_t single_pixel_buffer_color[4];
 	} WLR_PRIVATE;
+
+	struct fx_corner_radii corners;
+	struct linked_node blur;
 };
 
 /** A viewport for an output in the scene-graph */
@@ -279,6 +304,11 @@ struct wlr_scene_output {
 
 		bool gamma_lut_changed;
 		struct wlr_gamma_control_v1 *gamma_lut;
+		struct wlr_color_transform *gamma_lut_color_transform;
+
+		struct wlr_color_transform *prev_gamma_lut_color_transform;
+		struct wlr_color_transform *prev_supplied_color_transform;
+		struct wlr_color_transform *combined_color_transform;
 
 		struct wl_listener output_commit;
 		struct wl_listener output_damage;
@@ -290,6 +320,8 @@ struct wlr_scene_output {
 
 		struct wlr_drm_syncobj_timeline *in_timeline;
 		uint64_t in_point;
+		struct wlr_drm_syncobj_timeline *out_timeline;
+		uint64_t out_point;
 	} WLR_PRIVATE;
 };
 
@@ -419,6 +451,12 @@ void wlr_scene_set_linux_dmabuf_v1(struct wlr_scene *scene,
 void wlr_scene_set_gamma_control_manager_v1(struct wlr_scene *scene,
 	struct wlr_gamma_control_manager_v1 *gamma_control);
 
+/**
+ * Handles color_management_v1 feedback for all surfaces in the scene.
+ *
+ * Asserts that a struct wlr_color_manager_v1 hasn't already been set for the scene.
+ */
+void wlr_scene_set_color_manager_v1(struct wlr_scene *scene, struct wlr_color_manager_v1 *manager);
 
 /**
  * Add a node displaying nothing but its children.
@@ -479,6 +517,8 @@ struct wlr_scene_rect *wlr_scene_rect_from_node(struct wlr_scene_node *node);
  */
 struct wlr_scene_shadow *wlr_scene_shadow_from_node(struct wlr_scene_node *node);
 
+struct wlr_scene_blur *wlr_scene_blur_from_node(struct wlr_scene_node *node);
+
 /**
  * If this buffer is backed by a surface, then the struct wlr_scene_surface is
  * returned. If not, NULL will be returned.
@@ -486,6 +526,12 @@ struct wlr_scene_shadow *wlr_scene_shadow_from_node(struct wlr_scene_node *node)
 struct wlr_scene_surface *wlr_scene_surface_try_from_buffer(
 	struct wlr_scene_buffer *scene_buffer);
 
+/**
+ * Call wlr_surface_send_frame_done() if the surface is visible.
+ */
+void wlr_scene_surface_send_frame_done(struct wlr_scene_surface *scene_surface,
+	const struct timespec *when);
+
 /**
  * Add a node displaying a solid-colored rectangle to the scene-graph.
  *
@@ -500,10 +546,14 @@ struct wlr_scene_rect *wlr_scene_rect_create(struct wlr_scene_tree *parent,
 void wlr_scene_rect_set_size(struct wlr_scene_rect *rect, int width, int height);
 
 /**
- * Change the corner radius of an existing rectangle node.
+ * Change the corner radius of all corners of an existing rectangle node.
  */
-void wlr_scene_rect_set_corner_radius(struct wlr_scene_rect *rect, int corner_radius,
-		enum corner_location corners);
+void wlr_scene_rect_set_corner_radius(struct wlr_scene_rect *rect, int corner_radius);
+
+/**
+ * Change the rounded corners of an existing rectangle node.
+ */
+void wlr_scene_rect_set_corner_radii(struct wlr_scene_rect *rect, struct fx_corner_radii);
 
 /**
  * Sets the region where to clip the rect.
@@ -523,18 +573,6 @@ void wlr_scene_rect_set_clipped_region(struct wlr_scene_rect *rect,
  */
 void wlr_scene_rect_set_color(struct wlr_scene_rect *rect, const float color[static 4]);
 
-/**
-* Sets whether or not the buffer should render backdrop blur
-*/
-void wlr_scene_rect_set_backdrop_blur(struct wlr_scene_rect *rect,
-		bool enabled);
-
-/**
-* Sets whether the backdrop blur should use optimized blur or not
-*/
-void wlr_scene_rect_set_backdrop_blur_optimized(struct wlr_scene_rect *rect,
-		bool enabled);
-
 /**
  * Sets the fade inset size for the rectangle's borders.
  */
@@ -578,6 +616,77 @@ void wlr_scene_shadow_set_color(struct wlr_scene_shadow *shadow, const float col
 void wlr_scene_shadow_set_clipped_region(struct wlr_scene_shadow *shadow,
 		struct clipped_region clipped_region);
 
+/**
+ * Add a node displaying a blur to the scene-graph.
+ */
+struct wlr_scene_blur *wlr_scene_blur_create(struct wlr_scene_tree *parent,
+	   int width, int height);
+
+/**
+ * Change the width and height of an existing blur node.
+ */
+void wlr_scene_blur_set_size(struct wlr_scene_blur *blur, int width, int height);
+
+/**
+ * Change the corner radius of all corners of an existing blur node.
+ */
+void wlr_scene_blur_set_corner_radius(struct wlr_scene_blur *blur, int corner_radius);
+
+/**
+ * Change the corners of an existing blur node.
+ */
+void wlr_scene_blur_set_corner_radii(struct wlr_scene_blur *blur, struct fx_corner_radii);
+
+/**
+ * Make the blur node only blur the bottom layer of the scene
+ */
+void wlr_scene_blur_set_should_only_blur_bottom_layer(struct wlr_scene_blur *blur,
+	bool should_only_blur_bottom_layer);
+
+/**
+ * Set the transparency mask source for the blur, only rendering blur where the
+ * Mask source is actually rendering (e.g. skip transparent spaces)
+ */
+void wlr_scene_blur_set_transparency_mask_source(struct wlr_scene_blur *blur,
+	   struct wlr_scene_buffer *source);
+
+/**
+ * get the transparency mask source for the blur
+ */
+struct wlr_scene_buffer *wlr_scene_blur_get_transparency_mask_source(
+	struct wlr_scene_blur *blur);
+
+/**
+ * Sets the blur alpha from 1.0f -> 0.0f. This adjusts the actual alpha of the blur.
+ * Default is 1.0f.
+ *
+ * Lower values without also adjusting the strength will look off.
+ *
+ * Can be used combined with strength to create a good-looking
+ * fade-out effect.
+ */
+void wlr_scene_blur_set_alpha(struct wlr_scene_blur *blur, float alpha);
+
+/**
+ * Sets the blur strength from 1.0f -> 0.0f. This adjusts how strong the blur is
+ * relative to the base 1.0 value.
+ *
+ * Can be used combined with alpha to create a good-looking
+ * fade-out effect.
+ */
+void wlr_scene_blur_set_strength(struct wlr_scene_blur *blur, float strength);
+
+/**
+ * Sets the region where to clip the blur.
+ *
+ * For there to be corner rounding of the clipped region, the corner radius and
+ * corners must be non-zero.
+ *
+ * NOTE: The positioning is node-relative.
+ */
+void wlr_scene_blur_set_clipped_region(struct wlr_scene_blur *blur,
+		struct clipped_region clipped_region);
+
 /**
  * If this node represents a wlr_scene_optimized_blur node, that node will
  * be returned. It is not legal to feed a node that does not represent
@@ -702,35 +811,34 @@ void wlr_scene_buffer_set_filter_mode(struct wlr_scene_buffer *scene_buffer,
 	enum wlr_scale_filter_mode filter_mode);
 
 /**
-* Sets the corner radius and which corners to round of this buffer
+* Sets the corner radius of all corners to round of this buffer
 */
 void wlr_scene_buffer_set_corner_radius(struct wlr_scene_buffer *scene_buffer,
-		int radii, enum corner_location corners);
+		int radii);
 
 /**
-* Sets whether or not the buffer should render backdrop blur
+* Sets the corner radii of this buffer
 */
-void wlr_scene_buffer_set_backdrop_blur(struct wlr_scene_buffer *scene_buffer,
-		bool enabled);
+void wlr_scene_buffer_set_corner_radii(struct wlr_scene_buffer *scene_buffer,
+	struct fx_corner_radii corner_radii);
 
-/**
-* Sets whether the backdrop blur should use optimized blur or not
-*/
-void wlr_scene_buffer_set_backdrop_blur_optimized(struct wlr_scene_buffer *scene_buffer,
-		bool enabled);
+void wlr_scene_buffer_set_transfer_function(struct wlr_scene_buffer *scene_buffer,
+	enum wlr_color_transfer_function transfer_function);
 
-/**
-* Sets whether the backdrop blur should not render in fully transparent
-* segments.
-*/
-void wlr_scene_buffer_set_backdrop_blur_ignore_transparent(
-		struct wlr_scene_buffer *scene_buffer, bool enabled);
+void wlr_scene_buffer_set_primaries(struct wlr_scene_buffer *scene_buffer,
+	enum wlr_color_named_primaries primaries);
+
+void wlr_scene_buffer_set_color_encoding(struct wlr_scene_buffer *scene_buffer,
+	enum wlr_color_encoding encoding);
+
+void wlr_scene_buffer_set_color_range(struct wlr_scene_buffer *scene_buffer,
+	enum wlr_color_range range);
 
 /**
  * Calls the buffer's frame_done signal.
  */
 void wlr_scene_buffer_send_frame_done(struct wlr_scene_buffer *scene_buffer,
-	struct timespec *now);
+	struct wlr_scene_frame_done_event *event);
 
 /**
  * Add a viewport for the specified output to the scene-graph.
@@ -751,6 +859,11 @@ void wlr_scene_output_set_position(struct wlr_scene_output *scene_output,
 
 struct wlr_scene_output_state_options {
 	struct wlr_scene_timer *timer;
+
+	/**
+	 * Color transform to apply before the output's color transform. Cannot be
+	 * used when the output has a non-NULL image description set.
+	 */
 	struct wlr_color_transform *color_transform;
 
 	/**
diff --git a/scenefx/include/types/fx/clipped_region.h b/scenefx/include/types/fx/clipped_region.h
new file mode 100644
index 0000000..44e8414
--- /dev/null
+++ b/scenefx/include/types/fx/clipped_region.h
@@ -0,0 +1,27 @@
+#ifndef TYPES_FX_CLIPPED_REGION_H
+#define TYPES_FX_CLIPPED_REGION_H
+#include <wlr/util/box.h>
+#include <scenefx/types/fx/clipped_region.h>
+
+struct fx_corner_fradii {
+    float top_left;
+    float top_right;
+    float bottom_right;
+    float bottom_left;
+};
+
+
+bool fx_corner_fradii_is_empty(const struct fx_corner_fradii* corners);
+
+struct fx_corner_fradii fx_corner_radii_scale(struct fx_corner_radii, float scale);
+
+struct clipped_fregion {
+    struct wlr_box area;
+    struct fx_corner_fradii corners;
+};
+
+static __always_inline bool clipped_fregion_is_valid(const struct clipped_fregion *fregion) {
+	return !fx_corner_fradii_is_empty(&fregion->corners) && !wlr_box_empty(&fregion->area);
+};
+
+#endif
diff --git a/scenefx/include/types/wlr_output.h b/scenefx/include/types/wlr_output.h
index b239b6a..3981bd2 100644
--- a/scenefx/include/types/wlr_output.h
+++ b/scenefx/include/types/wlr_output.h
@@ -7,4 +7,7 @@
 void output_pending_resolution(struct wlr_output *output,
 	const struct wlr_output_state *state, int *width, int *height);
 
+const struct wlr_output_image_description *output_pending_image_description(
+	struct wlr_output *output, const struct wlr_output_state *state);
+
 #endif
diff --git a/scenefx/include/types/wlr_scene.h b/scenefx/include/types/wlr_scene.h
index 80dcfd1..c4b40cd 100644
--- a/scenefx/include/types/wlr_scene.h
+++ b/scenefx/include/types/wlr_scene.h
@@ -5,6 +5,8 @@
 
 struct wlr_scene *scene_node_get_root(struct wlr_scene_node *node);
 
+void scene_node_get_size(struct wlr_scene_node *node, int *width, int *height);
+
 void scene_surface_set_clip(struct wlr_scene_surface *surface, struct wlr_box *clip);
 
 #endif
diff --git a/scenefx/meson.build b/scenefx/meson.build
index 4198ce0..5422a96 100644
--- a/scenefx/meson.build
+++ b/scenefx/meson.build
@@ -1,7 +1,7 @@
 project(
 	'scenefx',
 	'c',
-	version: '0.4.1',
+	version: '0.5.0',
 	license: 'MIT',
 	meson_version: '>=1.3',
 	default_options: [
@@ -66,8 +66,17 @@ else
 	)
 endif
 
+features = {
+	'gles2-renderer': true,
+	'color-management': false,
+}
+internal_features = {
+	'egl': false,
+	'tracy': false,
+}
+
 wayland_kwargs = {
-	'version': '>=1.23.1',
+	'version': '>=1.24.0',
 	'fallback': 'wayland',
 	'default_options': [
 		'tests=false',
@@ -79,8 +88,8 @@ wayland_server = dependency('wayland-server',
 )
 
 wlroots_options = [ 'examples=false' ]
-wlroots_version = ['>=0.19.0', '<0.20.0']
-wlroots = dependency('wlroots-0.19',
+wlroots_version = ['>=0.20.0', '<0.21.0']
+wlroots = dependency('wlroots-0.20',
 	version: wlroots_version,
 	default_options: wlroots_options,
 	required: false,
@@ -96,14 +105,23 @@ endif
 
 
 drm = dependency('libdrm',
-	version: '>=2.4.122',
+	version: '>=2.4.129',
 	fallback: 'libdrm',
 	default_options: [
 		'auto_features=disabled',
 		'tests=false',
 	],
 )
-xkbcommon = dependency('xkbcommon')
+xkbcommon = dependency('xkbcommon',
+	version: '>=1.8.0',
+	fallback: 'libxkbcommon',
+	default_options: [
+		'enable-tools=false',
+		'enable-x11=false',
+		'enable-docs=false',
+		'enable-xkbregistry=false',
+	],
+)
 pixman = dependency('pixman-1',
 	version: '>=0.43.0',
 	fallback: 'pixman',
@@ -123,6 +141,34 @@ scenefx_deps = [
 	rt,
 ]
 
+# Tracy
+if get_option('tracy_enable')
+	internal_features += { 'tracy': true }
+
+	if get_option('buildtype') != 'debugoptimized'
+		warning('Profiling builds should set --buildtype=debugoptimized')
+	endif
+
+	tracy_options = [
+		'default_library=static',
+		'werror=false',
+		'warning_level=0',
+	]
+	tracy_proj = subproject(
+		'tracy',
+		required: true,
+		default_options: tracy_options
+	)
+	scenefx_deps += tracy_proj.get_variable('tracy_dep')
+	add_project_arguments(
+		[
+			'-DTRACY_ENABLE',
+			'-DTRACY_ON_DEMAND',
+		],
+		language: 'c',
+	)
+endif
+
 subdir('protocol')
 subdir('render')
 
@@ -149,6 +195,8 @@ scenefx = declare_dependency(
 
 meson.override_dependency(versioned_name, scenefx)
 
+summary(features + internal_features, bool_yn: true)
+
 if get_option('examples')
 	subdir('examples')
 	subdir('tinywl')
diff --git a/scenefx/meson_options.txt b/scenefx/meson_options.txt
index 8b527ea..810ec9f 100644
--- a/scenefx/meson_options.txt
+++ b/scenefx/meson_options.txt
@@ -1,2 +1,4 @@
 option('examples', type: 'boolean', value: true, description: 'Build example applications')
 option('renderers', type: 'array', choices: ['auto', 'gles2'], value: ['auto'], description: 'Select built-in renderers')
+option('color-management', type: 'feature', value: 'auto', description: 'Enable support for color management')
+option('tracy_enable', type: 'boolean', value: false, description: 'Enable profiling')
diff --git a/scenefx/render/color.c b/scenefx/render/color.c
new file mode 100644
index 0000000..e89fab2
--- /dev/null
+++ b/scenefx/render/color.c
@@ -0,0 +1,433 @@
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wlr/render/color.h>
+#include "render/color.h"
+#include "util/matrix.h"
+
+// See H.273 ColourPrimaries
+
+static const struct wlr_color_primaries COLOR_PRIMARIES_SRGB = { // code point 1
+	.red = { 0.640, 0.330 },
+	.green = { 0.300, 0.600 },
+	.blue = { 0.150, 0.060 },
+	.white = { 0.3127, 0.3290 },
+};
+
+static const struct wlr_color_primaries COLOR_PRIMARIES_BT2020 = { // code point 9
+	.red = { 0.708, 0.292 },
+	.green = { 0.170, 0.797 },
+	.blue = { 0.131, 0.046 },
+	.white = { 0.3127, 0.3290 },
+};
+
+void wlr_color_transform_init(struct wlr_color_transform *tr, enum wlr_color_transform_type type) {
+	*tr = (struct wlr_color_transform){
+		.type = type,
+		.ref_count = 1,
+	};
+	wlr_addon_set_init(&tr->addons);
+}
+
+struct wlr_color_transform *wlr_color_transform_init_linear_to_inverse_eotf(
+		enum wlr_color_transfer_function tf) {
+	struct wlr_color_transform_inverse_eotf *tx = calloc(1, sizeof(*tx));
+	if (!tx) {
+		return NULL;
+	}
+	wlr_color_transform_init(&tx->base, COLOR_TRANSFORM_INVERSE_EOTF);
+	tx->tf = tf;
+	return &tx->base;
+}
+
+struct wlr_color_transform *wlr_color_transform_init_lut_3x1d(size_t dim,
+		const uint16_t *r, const uint16_t *g, const uint16_t *b) {
+	uint16_t *lut_3x1d = malloc(3 * dim * sizeof(lut_3x1d[0]));
+	if (lut_3x1d == NULL) {
+		return NULL;
+	}
+
+	memcpy(&lut_3x1d[0 * dim], r, dim * sizeof(lut_3x1d[0]));
+	memcpy(&lut_3x1d[1 * dim], g, dim * sizeof(lut_3x1d[0]));
+	memcpy(&lut_3x1d[2 * dim], b, dim * sizeof(lut_3x1d[0]));
+
+	struct wlr_color_transform_lut_3x1d *tx = calloc(1, sizeof(*tx));
+	if (!tx) {
+		free(lut_3x1d);
+		return NULL;
+	}
+	wlr_color_transform_init(&tx->base, COLOR_TRANSFORM_LUT_3X1D);
+	tx->lut_3x1d = lut_3x1d;
+	tx->dim = dim;
+	return &tx->base;
+}
+
+struct wlr_color_transform *wlr_color_transform_init_matrix(const float matrix[static 9]) {
+	struct wlr_color_transform_matrix *tx = calloc(1, sizeof(*tx));
+	if (!tx) {
+		return NULL;
+	}
+	wlr_color_transform_init(&tx->base, COLOR_TRANSFORM_MATRIX);
+	memcpy(tx->matrix, matrix, sizeof(tx->matrix));
+	return &tx->base;
+}
+
+struct wlr_color_transform *wlr_color_transform_init_pipeline(
+		struct wlr_color_transform **transforms, size_t len) {
+	assert(len > 0);
+
+	struct wlr_color_transform **copy = calloc(len, sizeof(copy[0]));
+	if (copy == NULL) {
+		return NULL;
+	}
+
+	struct wlr_color_transform_pipeline *tx = calloc(1, sizeof(*tx));
+	if (!tx) {
+		free(copy);
+		return NULL;
+	}
+	wlr_color_transform_init(&tx->base, COLOR_TRANSFORM_PIPELINE);
+
+	for (size_t i = 0; i < len; i++) {
+		assert(transforms[i] != NULL);
+		copy[i] = wlr_color_transform_ref(transforms[i]);
+	}
+
+	tx->transforms = copy;
+	tx->len = len;
+
+	return &tx->base;
+}
+
+static void color_transform_destroy(struct wlr_color_transform *tr) {
+	switch (tr->type) {
+	case COLOR_TRANSFORM_INVERSE_EOTF:
+	case COLOR_TRANSFORM_MATRIX:
+		break;
+	case COLOR_TRANSFORM_LCMS2:
+		color_transform_lcms2_finish(color_transform_lcms2_from_base(tr));
+		break;
+	case COLOR_TRANSFORM_LUT_3X1D:;
+		struct wlr_color_transform_lut_3x1d *lut_3x1d = color_transform_lut_3x1d_from_base(tr);
+		free(lut_3x1d->lut_3x1d);
+		break;
+	case COLOR_TRANSFORM_PIPELINE:;
+		struct wlr_color_transform_pipeline *pipeline =
+			wl_container_of(tr, pipeline, base);
+		for (size_t i = 0; i < pipeline->len; i++) {
+			wlr_color_transform_unref(pipeline->transforms[i]);
+		}
+		free(pipeline->transforms);
+		break;
+	}
+	wlr_addon_set_finish(&tr->addons);
+	free(tr);
+}
+
+struct wlr_color_transform *wlr_color_transform_ref(struct wlr_color_transform *tr) {
+	tr->ref_count += 1;
+	return tr;
+}
+
+void wlr_color_transform_unref(struct wlr_color_transform *tr) {
+	if (!tr) {
+		return;
+	}
+	assert(tr->ref_count > 0);
+	tr->ref_count -= 1;
+	if (tr->ref_count == 0) {
+		color_transform_destroy(tr);
+	}
+}
+
+struct wlr_color_transform_inverse_eotf *wlr_color_transform_inverse_eotf_from_base(
+		struct wlr_color_transform *tr) {
+	assert(tr->type == COLOR_TRANSFORM_INVERSE_EOTF);
+	struct wlr_color_transform_inverse_eotf *inverse_eotf = wl_container_of(tr, inverse_eotf, base);
+	return inverse_eotf;
+}
+
+struct wlr_color_transform_lut_3x1d *color_transform_lut_3x1d_from_base(
+		struct wlr_color_transform *tr) {
+	assert(tr->type == COLOR_TRANSFORM_LUT_3X1D);
+	struct wlr_color_transform_lut_3x1d *lut_3x1d = wl_container_of(tr, lut_3x1d, base);
+	return lut_3x1d;
+}
+
+static float srgb_eval_inverse_eotf(float x) {
+	// See https://www.w3.org/Graphics/Color/srgb
+	if (x <= 0.0031308) {
+		return 12.92 * x;
+	} else {
+		return 1.055 * powf(x, 1.0 / 2.4) - 0.055;
+	}
+}
+
+static float st2084_pq_eval_inverse_eotf(float x) {
+	// H.273 TransferCharacteristics code point 16
+	float c1 = 0.8359375;
+	float c2 = 18.8515625;
+	float c3 = 18.6875;
+	float m = 78.84375;
+	float n = 0.1593017578125;
+	if (x < 0) {
+		x = 0;
+	}
+	if (x > 1) {
+		x = 1;
+	}
+	float pow_n = powf(x, n);
+	return powf((c1 + c2 * pow_n) / (1 + c3 * pow_n), m);
+}
+
+static float bt1886_eval_inverse_eotf(float x) {
+	float lb = powf(0.0001, 1.0 / 2.4);
+	float lw = powf(1.0, 1.0 / 2.4);
+	float a  = powf(lw - lb, 2.4);
+	float b  = lb / (lw - lb);
+	return powf(x / a, 1.0 / 2.4) - b;
+}
+
+static float transfer_function_eval_inverse_eotf(
+		enum wlr_color_transfer_function tf, float x) {
+	switch (tf) {
+	case WLR_COLOR_TRANSFER_FUNCTION_SRGB:
+		return srgb_eval_inverse_eotf(x);
+	case WLR_COLOR_TRANSFER_FUNCTION_ST2084_PQ:
+		return st2084_pq_eval_inverse_eotf(x);
+	case WLR_COLOR_TRANSFER_FUNCTION_EXT_LINEAR:
+		return x;
+	case WLR_COLOR_TRANSFER_FUNCTION_GAMMA22:
+		return powf(x, 1.0 / 2.2);
+	case WLR_COLOR_TRANSFER_FUNCTION_BT1886:
+		return bt1886_eval_inverse_eotf(x);
+	}
+	abort(); // unreachable
+}
+
+static void color_transform_inverse_eotf_eval(
+		struct wlr_color_transform_inverse_eotf *tr,
+		float out[static 3], const float in[static 3]) {
+	for (size_t i = 0; i < 3; i++) {
+		out[i] = transfer_function_eval_inverse_eotf(tr->tf, in[i]);
+	}
+}
+
+static float lut_1d_get(const uint16_t *lut, size_t len, size_t i) {
+	if (i >= len) {
+		i = len - 1;
+	}
+	return (float) lut[i] / UINT16_MAX;
+}
+
+static float lut_1d_eval(const uint16_t *lut, size_t len, float x) {
+	double pos = x * (len - 1);
+	double int_part;
+	double frac_part = modf(pos, &int_part);
+	size_t i = (size_t) int_part;
+	double a = lut_1d_get(lut, len, i);
+	double b = lut_1d_get(lut, len, i + 1);
+	return a * (1 - frac_part) + b * frac_part;
+}
+
+static void color_transform_lut_3x1d_eval(struct wlr_color_transform_lut_3x1d *tr,
+		float out[static 3], const float in[static 3]) {
+	for (size_t i = 0; i < 3; i++) {
+		out[i] = lut_1d_eval(&tr->lut_3x1d[tr->dim * i], tr->dim, in[i]);
+	}
+}
+
+static void multiply_matrix_vector(float out[static 3], float m[static 9], const float v[static 3]);
+
+void wlr_color_transform_eval(struct wlr_color_transform *tr,
+		float out[static 3], const float in[static 3]) {
+	switch (tr->type) {
+	case COLOR_TRANSFORM_INVERSE_EOTF:
+		color_transform_inverse_eotf_eval(wlr_color_transform_inverse_eotf_from_base(tr), out, in);
+		break;
+	case COLOR_TRANSFORM_LCMS2:
+		color_transform_lcms2_eval(color_transform_lcms2_from_base(tr), out, in);
+		break;
+	case COLOR_TRANSFORM_LUT_3X1D:
+		color_transform_lut_3x1d_eval(color_transform_lut_3x1d_from_base(tr), out, in);
+		break;
+	case COLOR_TRANSFORM_MATRIX:;
+		struct wlr_color_transform_matrix *matrix = wl_container_of(tr, matrix, base);
+		multiply_matrix_vector(out, matrix->matrix, in);
+		break;
+	case COLOR_TRANSFORM_PIPELINE:;
+		struct wlr_color_transform_pipeline *pipeline =
+			wl_container_of(tr, pipeline, base);
+		float color[3];
+		memcpy(color, in, sizeof(color));
+		for (size_t i = 0; i < pipeline->len; i++) {
+			wlr_color_transform_eval(pipeline->transforms[i], color, color);
+		}
+		memcpy(out, color, sizeof(color));
+		break;
+	}
+}
+
+static size_t color_transform_compose_collect(struct wlr_color_transform **out,
+		size_t out_capacity, struct wlr_color_transform **transforms, size_t len) {
+	size_t count = 0;
+	for (size_t i = 0; i < len; i++) {
+		struct wlr_color_transform *transform = transforms[i];
+		if (transform == NULL) {
+			continue;
+		}
+
+		if (transform->type == COLOR_TRANSFORM_PIPELINE) {
+			struct wlr_color_transform_pipeline *pipeline = wl_container_of(transform,
+				pipeline, base);
+			count += color_transform_compose_collect(out, out_capacity,
+				pipeline->transforms, pipeline->len);
+		} else {
+			if (out_capacity > 0) {
+				*out = wlr_color_transform_ref(transform);
+				out++;
+				out_capacity--;
+			}
+			count++;
+		}
+	}
+	return count;
+}
+
+bool color_transform_compose(struct wlr_color_transform **result,
+		struct wlr_color_transform **transforms, size_t len) {
+	// The normalized form has the following properties :
+	// - No NULL transform in a pipeline
+	// - No pipeline of length 1
+	// - No nested pipelines
+	bool status = false;
+
+	size_t result_len = color_transform_compose_collect(NULL, 0, transforms, len);
+	if (result_len == 0) {
+		*result = NULL;
+		return true;
+	}
+
+	struct wlr_color_transform **result_transforms = calloc(result_len,
+		sizeof(result_transforms[0]));
+	if (result_transforms == NULL) {
+		return false;
+	}
+	color_transform_compose_collect(result_transforms, result_len, transforms, len);
+
+	if (result_len == 1) {
+		*result = wlr_color_transform_ref(result_transforms[0]);
+	} else {
+		*result = wlr_color_transform_init_pipeline(result_transforms, result_len);
+		if (*result == NULL) {
+			goto cleanup_transforms;
+		}
+	}
+	status = true;
+
+cleanup_transforms:
+	for (size_t i = 0; i < result_len; i++) {
+		wlr_color_transform_unref(result_transforms[i]);
+	}
+	free(result_transforms);
+	return status;
+}
+
+void wlr_color_primaries_from_named(struct wlr_color_primaries *out,
+		enum wlr_color_named_primaries named) {
+	switch (named) {
+	case WLR_COLOR_NAMED_PRIMARIES_SRGB:
+		*out = COLOR_PRIMARIES_SRGB;
+		return;
+	case WLR_COLOR_NAMED_PRIMARIES_BT2020:
+		*out = COLOR_PRIMARIES_BT2020;
+		return;
+	}
+	abort();
+}
+
+static void multiply_matrix_vector(float out[static 3], float m[static 9], const float v[static 3]) {
+	float result[3] = {
+		m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
+		m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
+		m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
+	};
+	memcpy(out, result, sizeof(result));
+}
+
+static void xy_to_xyz(float out[static 3], struct wlr_color_cie1931_xy src) {
+	if (src.y == 0) {
+		out[0] = out[1] = out[2] = 0;
+		return;
+	}
+
+	out[0] = src.x / src.y;
+	out[1] = 1;
+	out[2] = (1 - src.x - src.y) / src.y;
+}
+
+void wlr_color_primaries_to_xyz(const struct wlr_color_primaries *primaries, float matrix[static 9]) {
+	// See: http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+
+	float r[3], g[3], b[3], w[3];
+	xy_to_xyz(r, primaries->red);
+	xy_to_xyz(g, primaries->green);
+	xy_to_xyz(b, primaries->blue);
+	xy_to_xyz(w, primaries->white);
+
+	float xyz_matrix[9] = {
+		r[0], g[0], b[0],
+		r[1], g[1], b[1],
+		r[2], g[2], b[2],
+	};
+	matrix_invert(xyz_matrix, xyz_matrix);
+
+	float S[3];
+	multiply_matrix_vector(S, xyz_matrix, w);
+
+	float result[] = {
+		S[0] * r[0], S[1] * g[0], S[2] * b[0],
+		S[0] * r[1], S[1] * g[1], S[2] * b[1],
+		S[0] * r[2], S[1] * g[2], S[2] * b[2],
+	};
+	memcpy(matrix, result, sizeof(result));
+}
+
+void wlr_color_primaries_transform_absolute_colorimetric(
+		const struct wlr_color_primaries *source,
+		const struct wlr_color_primaries *destination, float matrix[static 9]) {
+	float source_to_xyz[9];
+	wlr_color_primaries_to_xyz(source, source_to_xyz);
+	float destination_to_xyz[9];
+	wlr_color_primaries_to_xyz(destination, destination_to_xyz);
+	float xyz_to_destination[9];
+	matrix_invert(xyz_to_destination, destination_to_xyz);
+	wlr_matrix_multiply(matrix, xyz_to_destination, source_to_xyz);
+}
+
+void wlr_color_transfer_function_get_default_luminance(enum wlr_color_transfer_function tf,
+		struct wlr_color_luminances *lum) {
+	switch (tf) {
+	case WLR_COLOR_TRANSFER_FUNCTION_ST2084_PQ:
+		*lum = (struct wlr_color_luminances){
+			.min = 0.005,
+			.max = 10000,
+			.reference = 203,
+		};
+		break;
+	case WLR_COLOR_TRANSFER_FUNCTION_BT1886:
+		*lum = (struct wlr_color_luminances){
+			.min = 0.01,
+			.max = 100,
+			.reference = 100,
+		};
+		break;
+	default:
+		*lum = (struct wlr_color_luminances){
+			.min = 0.2,
+			.max = 80,
+			.reference = 80,
+		};
+		break;
+	}
+}
diff --git a/scenefx/render/color_fallback.c b/scenefx/render/color_fallback.c
new file mode 100644
index 0000000..1b25881
--- /dev/null
+++ b/scenefx/render/color_fallback.c
@@ -0,0 +1,24 @@
+#include <stdlib.h>
+#include <wlr/util/log.h>
+#include "render/color.h"
+
+struct wlr_color_transform *wlr_color_transform_init_linear_to_icc(
+		const void *data, size_t size) {
+	wlr_log(WLR_ERROR, "Cannot create color transform from ICC profile: "
+		"LCMS2 is compile-time disabled");
+	return NULL;
+}
+
+struct wlr_color_transform_lcms2 *color_transform_lcms2_from_base(
+		struct wlr_color_transform *tr) {
+	abort(); // unreachable
+}
+
+void color_transform_lcms2_finish(struct wlr_color_transform_lcms2 *tr) {
+	abort(); // unreachable
+}
+
+void color_transform_lcms2_eval(struct wlr_color_transform_lcms2 *tr,
+		float out[static 3], const float in[static 3]) {
+	abort(); // unreachable
+}
diff --git a/scenefx/render/color_lcms2.c b/scenefx/render/color_lcms2.c
new file mode 100644
index 0000000..39e16cd
--- /dev/null
+++ b/scenefx/render/color_lcms2.c
@@ -0,0 +1,113 @@
+#include <assert.h>
+#include <lcms2.h>
+#include <stdlib.h>
+#include <wlr/util/log.h>
+#include <wlr/render/color.h>
+#include "render/color.h"
+
+struct wlr_color_transform_lcms2 {
+	struct wlr_color_transform base;
+
+	cmsContext ctx;
+	cmsHTRANSFORM lcms;
+};
+
+static const cmsCIExyY srgb_whitepoint = { 0.3127, 0.3291, 1 };
+
+static const cmsCIExyYTRIPLE srgb_primaries = {
+	.Red = { 0.64, 0.33, 1 },
+	.Green = { 0.3, 0.6, 1 },
+	.Blue = { 0.15, 0.06, 1},
+};
+
+static void handle_lcms_error(cmsContext ctx, cmsUInt32Number code, const char *text) {
+	wlr_log(WLR_ERROR, "[lcms] %s", text);
+}
+
+struct wlr_color_transform *wlr_color_transform_init_linear_to_icc(
+		const void *data, size_t size) {
+	struct wlr_color_transform_lcms2 *tx = NULL;
+
+	cmsContext ctx = cmsCreateContext(NULL, NULL);
+	if (ctx == NULL) {
+		wlr_log(WLR_ERROR, "cmsCreateContext failed");
+		return NULL;
+	}
+
+	cmsSetLogErrorHandlerTHR(ctx, handle_lcms_error);
+
+	cmsHPROFILE icc_profile = cmsOpenProfileFromMemTHR(ctx, data, size);
+	if (icc_profile == NULL) {
+		wlr_log(WLR_ERROR, "cmsOpenProfileFromMemTHR failed");
+		goto error_ctx;
+	}
+
+	if (cmsGetDeviceClass(icc_profile) != cmsSigDisplayClass) {
+		wlr_log(WLR_ERROR, "ICC profile must have the Display device class");
+		goto error_icc_profile;
+	}
+
+	cmsToneCurve *linear_tone_curve = cmsBuildGamma(ctx, 1);
+	if (linear_tone_curve == NULL) {
+		wlr_log(WLR_ERROR, "cmsBuildGamma failed");
+		goto error_icc_profile;
+	}
+
+	cmsToneCurve *linear_tf[] = {
+		linear_tone_curve,
+		linear_tone_curve,
+		linear_tone_curve,
+	};
+	cmsHPROFILE srgb_profile = cmsCreateRGBProfileTHR(ctx, &srgb_whitepoint,
+		&srgb_primaries, linear_tf);
+	cmsFreeToneCurve(linear_tone_curve);
+	if (srgb_profile == NULL) {
+		wlr_log(WLR_ERROR, "cmsCreateRGBProfileTHR failed");
+		goto error_icc_profile;
+	}
+
+	cmsHTRANSFORM lcms_tr = cmsCreateTransformTHR(ctx,
+		srgb_profile, TYPE_RGB_FLT, icc_profile, TYPE_RGB_FLT,
+		INTENT_RELATIVE_COLORIMETRIC, 0);
+	cmsCloseProfile(srgb_profile);
+	cmsCloseProfile(icc_profile);
+	if (lcms_tr == NULL) {
+		wlr_log(WLR_ERROR, "cmsCreateTransformTHR failed");
+		goto error_ctx;
+	}
+
+	tx = calloc(1, sizeof(*tx));
+	if (!tx) {
+		cmsDeleteTransform(lcms_tr);
+		goto error_ctx;
+	}
+	wlr_color_transform_init(&tx->base, COLOR_TRANSFORM_LCMS2);
+
+	tx->ctx = ctx;
+	tx->lcms = lcms_tr;
+
+	return &tx->base;
+
+error_icc_profile:
+	cmsCloseProfile(icc_profile);
+error_ctx:
+	cmsDeleteContext(ctx);
+	return NULL;
+}
+
+void color_transform_lcms2_finish(struct wlr_color_transform_lcms2 *tr) {
+	cmsDeleteTransform(tr->lcms);
+	cmsDeleteContext(tr->ctx);
+}
+
+struct wlr_color_transform_lcms2 *color_transform_lcms2_from_base(
+		struct wlr_color_transform *tr) {
+	assert(tr->type == COLOR_TRANSFORM_LCMS2);
+	struct wlr_color_transform_lcms2 *lcms2 = wl_container_of(tr, lcms2, base);
+	return lcms2;
+}
+
+void color_transform_lcms2_eval(struct wlr_color_transform_lcms2 *tr,
+		float out[static 3], const float in[static 3]) {
+	cmsDoTransform(tr->lcms, in, out, 1);
+}
diff --git a/scenefx/render/egl.c b/scenefx/render/egl.c
index 75cef15..85d5f6f 100644
--- a/scenefx/render/egl.c
+++ b/scenefx/render/egl.c
@@ -156,7 +156,7 @@ static void init_dmabuf_formats(struct wlr_egl *egl) {
 		}
 
 		if (modifiers_len == 0) {
-			// Asume the linear layout is supported if the driver doesn't
+			// Assume the linear layout is supported if the driver doesn't
 			// explicitly say otherwise
 			wlr_drm_format_set_add(&egl->dmabuf_texture_formats, fmt,
 				DRM_FORMAT_MOD_LINEAR);
@@ -260,7 +260,8 @@ static struct wlr_egl *egl_create(void) {
 	return egl;
 }
 
-static bool egl_init_display(struct wlr_egl *egl, EGLDisplay display) {
+static bool egl_init_display(struct wlr_egl *egl, EGLDisplay display,
+		bool allow_software) {
 	egl->display = display;
 
 	EGLint major, minor;
@@ -326,9 +327,8 @@ static bool egl_init_display(struct wlr_egl *egl, EGLDisplay display) {
 
 		// The only way a non-DRM device is selected is when the user
 		// explicitly picks software rendering
-		if (check_egl_ext(device_exts_str, "EGL_MESA_device_software") &&
-				egl->exts.EXT_device_drm) {
-			if (env_parse_bool("WLR_RENDERER_ALLOW_SOFTWARE")) {
+		if (check_egl_ext(device_exts_str, "EGL_MESA_device_software")) {
+			if (allow_software || env_parse_bool("WLR_RENDERER_ALLOW_SOFTWARE")) {
 				wlr_log(WLR_INFO, "Using software rendering");
 			} else {
 				wlr_log(WLR_ERROR, "Software rendering detected, please use "
@@ -382,7 +382,7 @@ static bool egl_init_display(struct wlr_egl *egl, EGLDisplay display) {
 }
 
 static bool egl_init(struct wlr_egl *egl, EGLenum platform,
-		void *remote_display) {
+		void *remote_display, bool allow_software) {
 	EGLint display_attribs[3] = {0};
 	size_t display_attribs_len = 0;
 
@@ -401,7 +401,7 @@ static bool egl_init(struct wlr_egl *egl, EGLenum platform,
 		return false;
 	}
 
-	if (!egl_init_display(egl, display)) {
+	if (!egl_init_display(egl, display, allow_software)) {
 		if (egl->exts.KHR_display_reference) {
 			eglTerminate(display);
 		}
@@ -411,10 +411,8 @@ static bool egl_init(struct wlr_egl *egl, EGLenum platform,
 	size_t atti = 0;
 	EGLint attribs[7];
 
-	// Try with gles3 first to check if the graphics drivers support it.
-	// If it fails, it will fallback to gles2
 	attribs[atti++] = EGL_CONTEXT_CLIENT_VERSION;
-	attribs[atti++] = 3;
+	attribs[atti++] = 2;
 
 	// Request a high priority context if possible
 	// TODO: only do this if we're running as the DRM master
@@ -435,24 +433,13 @@ static bool egl_init(struct wlr_egl *egl, EGLenum platform,
 	attribs[atti++] = EGL_NONE;
 	assert(atti <= sizeof(attribs)/sizeof(attribs[0]));
 
-	// Attempt to create a context with gles3
 	egl->context = eglCreateContext(egl->display, EGL_NO_CONFIG_KHR,
 		EGL_NO_CONTEXT, attribs);
 	if (egl->context != EGL_NO_CONTEXT) {
-		wlr_log(WLR_DEBUG, "Created EGL context using OpenGL ES 3.0");
+		wlr_log(WLR_DEBUG, "Created EGL context using OpenGL ES 2.0");
 	} else {
-		wlr_log(WLR_INFO, "Failed to create EGL context using OpenGL ES 3.0");
-
-		// Retry with gles2
-		attribs[1] = 2;
-		egl->context = eglCreateContext(egl->display, EGL_NO_CONFIG_KHR,
-			EGL_NO_CONTEXT, attribs);
-		if (egl->context != EGL_NO_CONTEXT) {
-			wlr_log(WLR_DEBUG, "Created EGL context using OpenGL ES 2.0");
-		} else {
-			wlr_log(WLR_ERROR, "Failed to create EGL context using OpenGL ES 2.0");
-			return false;
-		}
+		wlr_log(WLR_ERROR, "Failed to create EGL context using OpenGL ES 2.0");
+		return false;
 	}
 
 	if (request_high_priority) {
@@ -572,6 +559,8 @@ static int open_render_node(int drm_fd) {
 }
 
 struct wlr_egl *wlr_egl_create_with_drm_fd(int drm_fd) {
+	bool allow_software = drm_fd < 0;
+
 	struct wlr_egl *egl = egl_create();
 	if (egl == NULL) {
 		wlr_log(WLR_ERROR, "Failed to create EGL context");
@@ -585,7 +574,7 @@ struct wlr_egl *wlr_egl_create_with_drm_fd(int drm_fd) {
 		 */
 		EGLDeviceEXT egl_device = get_egl_device_from_drm_fd(egl, drm_fd);
 		if (egl_device != EGL_NO_DEVICE_EXT) {
-			if (egl_init(egl, EGL_PLATFORM_DEVICE_EXT, egl_device)) {
+			if (egl_init(egl, EGL_PLATFORM_DEVICE_EXT, egl_device, allow_software)) {
 				wlr_log(WLR_DEBUG, "Using EGL_PLATFORM_DEVICE_EXT");
 				return egl;
 			}
@@ -610,7 +599,7 @@ struct wlr_egl *wlr_egl_create_with_drm_fd(int drm_fd) {
 			goto error;
 		}
 
-		if (egl_init(egl, EGL_PLATFORM_GBM_KHR, egl->gbm_device)) {
+		if (egl_init(egl, EGL_PLATFORM_GBM_KHR, egl->gbm_device, allow_software)) {
 			wlr_log(WLR_DEBUG, "Using EGL_PLATFORM_GBM_KHR");
 			return egl;
 		}
@@ -649,7 +638,7 @@ struct wlr_egl *wlr_egl_create_with_context(EGLDisplay display,
 		return NULL;
 	}
 
-	if (!egl_init_display(egl, display)) {
+	if (!egl_init_display(egl, display, true)) {
 		free(egl);
 		return NULL;
 	}
diff --git a/scenefx/render/fx_renderer/fx_effect_framebuffers.c b/scenefx/render/fx_renderer/fx_effect_framebuffers.c
deleted file mode 100644
index b0caa7c..0000000
--- a/scenefx/render/fx_renderer/fx_effect_framebuffers.c
+++ /dev/null
@@ -1,57 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <wlr/types/wlr_output.h>
-#include <wlr/util/addon.h>
-#include <wlr/util/log.h>
-
-#include "scenefx/render/fx_renderer/fx_effect_framebuffers.h"
-
-static void addon_handle_destroy(struct wlr_addon *addon) {
-	struct fx_effect_framebuffers *fbos = wl_container_of(addon, fbos, addon);
-	wlr_addon_finish(&fbos->addon);
-	free(fbos);
-}
-
-static const struct wlr_addon_interface fbos_addon_impl = {
-	.name = "fx_effect_framebuffers",
-	.destroy = addon_handle_destroy,
-};
-
-static bool fx_effect_framebuffers_assign(struct wlr_output *output,
-		struct fx_effect_framebuffers *fbos) {
-	wlr_addon_init(&fbos->addon, &output->addons, output, &fbos_addon_impl);
-	return true;
-}
-
-struct fx_effect_framebuffers *fx_effect_framebuffers_try_get(struct wlr_output *output) {
-	struct fx_effect_framebuffers *fbos = NULL;
-	if (!output) {
-		return NULL;
-	}
-
-	struct wlr_addon *addon = wlr_addon_find(&output->addons, output,
-			&fbos_addon_impl);
-	if (!addon) {
-		goto create_new;
-	}
-
-	if (!(fbos = wl_container_of(addon, fbos, addon))) {
-		goto create_new;
-	}
-	return fbos;
-
-create_new:;
-	fbos = calloc(1, sizeof(*fbos));
-	if (!fbos) {
-		wlr_log(WLR_ERROR, "Could not allocate a fx_effect_framebuffers");
-		return NULL;
-	}
-
-	if (!fx_effect_framebuffers_assign(output, fbos)) {
-		wlr_log(WLR_ERROR, "Could not assign fx_effect_framebuffers to output: '%s'",
-				output->name);
-		free(fbos);
-		return NULL;
-	}
-	return fbos;
-}
diff --git a/scenefx/render/fx_renderer/fx_framebuffer.c b/scenefx/render/fx_renderer/fx_framebuffer.c
index c8835f4..d514b34 100644
--- a/scenefx/render/fx_renderer/fx_framebuffer.c
+++ b/scenefx/render/fx_renderer/fx_framebuffer.c
@@ -1,9 +1,7 @@
-#include <stdio.h>
 #include <stdlib.h>
 #include <wlr/interfaces/wlr_buffer.h>
 #include <wlr/render/allocator.h>
 #include <wlr/render/interface.h>
-#include <wlr/render/swapchain.h>
 #include <wlr/util/log.h>
 
 #include "render/egl.h"
@@ -69,47 +67,53 @@ GLuint fx_framebuffer_get_fbo(struct fx_framebuffer *buffer) {
 }
 
 void fx_framebuffer_get_or_create_custom(struct fx_renderer *renderer,
-		struct wlr_output *output, struct wlr_swapchain *swapchain,
+		struct wlr_allocator *allocator, int width, int height, bool has_alpha,
 		struct fx_framebuffer **fx_framebuffer, bool *failed) {
 	if (*failed) {
 		return;
 	}
 
-	struct wlr_allocator *allocator = output->allocator;
-	if (!swapchain) {
-		if (!output->swapchain) {
-			wlr_log(WLR_ERROR, "Failed to allocate buffer, no swapchain");
-			*failed = true;
-			return;
+	if (*fx_framebuffer != NULL) {
+		struct wlr_buffer *wlr_buffer = (*fx_framebuffer)->buffer;
+		if (wlr_buffer != NULL) {
+			if (wlr_buffer->width == width && wlr_buffer->height == height) {
+				return;
+			}
+			// Create a new wlr_buffer if it's null or if the output size has
+			// changed
+			wlr_buffer_drop(wlr_buffer);
+		} else {
+			fx_framebuffer_destroy(*fx_framebuffer);
 		}
-		swapchain = output->swapchain;
+		*fx_framebuffer = NULL;
 	}
-	int width = output->width;
-	int height = output->height;
-	struct wlr_buffer *wlr_buffer = NULL;
 
+	// Get the best supported DRM format (DMABUF if supported)
+	const struct wlr_drm_format_set *texture_formats = wlr_renderer_get_texture_formats(
+			&renderer->wlr_renderer, renderer->wlr_renderer.render_buffer_caps);
+	const struct fx_pixel_format *pix_format =
+		get_fx_format_from_gl(GL_RGBA, GL_UNSIGNED_BYTE, has_alpha);
+	const struct wlr_drm_format *format = wlr_drm_format_set_get(texture_formats, pix_format->drm_format);
+	if (format == NULL) {
+		wlr_log(WLR_ERROR, "Failed to get a supported texture format while allocating buffer");
+		*failed = true;
+		return;
+	}
+
+	struct wlr_buffer *wlr_buffer = wlr_allocator_create_buffer(allocator, width, height, format);
+	if (wlr_buffer == NULL) {
+		wlr_log(WLR_ERROR, "Failed to allocate wlr_buffer");
+		*failed = true;
+		return;
+	}
+
+	*fx_framebuffer = fx_framebuffer_get_or_create(renderer, wlr_buffer);
 	if (*fx_framebuffer == NULL) {
-		wlr_buffer = wlr_allocator_create_buffer(allocator, width, height,
-				&swapchain->format);
-		if (wlr_buffer == NULL) {
-			wlr_log(WLR_ERROR, "Failed to allocate buffer");
-			*failed = true;
-			return;
-		}
-	} else {
-		if ((wlr_buffer = (*fx_framebuffer)->buffer) &&
-				wlr_buffer->width == width &&
-				wlr_buffer->height == height) {
-			return;
-		}
-		// Create a new wlr_buffer if it's null or if the output size has
-		// changed
-		fx_framebuffer_destroy(*fx_framebuffer);
+		wlr_log(WLR_ERROR, "Failed to allocate fx_buffer");
 		wlr_buffer_drop(wlr_buffer);
-		wlr_buffer = wlr_allocator_create_buffer(allocator,
-				width, height, &swapchain->format);
+		*failed = true;
+		return;
 	}
-	*fx_framebuffer = fx_framebuffer_get_or_create(renderer, wlr_buffer);
 	fx_framebuffer_get_fbo(*fx_framebuffer);
 }
 
@@ -157,10 +161,15 @@ error_buffer:
 }
 
 void fx_framebuffer_bind(struct fx_framebuffer *fx_buffer) {
-	glBindFramebuffer(GL_FRAMEBUFFER, fx_buffer->fbo);
+	glBindFramebuffer(GL_FRAMEBUFFER, fx_framebuffer_get_fbo(fx_buffer));
 }
 
 void fx_framebuffer_destroy(struct fx_framebuffer *fx_buffer) {
+	if (!fx_buffer) {
+		wlr_log(WLR_ERROR, "Trying to destroy an already destroyed fx_framebuffer");
+		return;
+	}
+
 	// Release the framebuffer
 	wl_list_remove(&fx_buffer->link);
 	wlr_addon_finish(&fx_buffer->addon);
diff --git a/scenefx/render/fx_renderer/fx_offscreen_buffers.c b/scenefx/render/fx_renderer/fx_offscreen_buffers.c
new file mode 100644
index 0000000..8574a05
--- /dev/null
+++ b/scenefx/render/fx_renderer/fx_offscreen_buffers.c
@@ -0,0 +1,92 @@
+#include <stdlib.h>
+#include <wlr/types/wlr_output.h>
+#include <wlr/util/addon.h>
+#include <wlr/util/log.h>
+
+#include "render/fx_renderer/fx_renderer.h"
+#include "scenefx/render/fx_renderer/fx_renderer.h"
+#include "scenefx/render/fx_renderer/fx_offscreen_buffers.h"
+
+static void addon_handle_destroy(struct wlr_addon *addon) {
+	struct fx_offscreen_buffers *fbos = wl_container_of(addon, fbos, addon);
+
+	// Make sure to free the buffers
+	if (fbos->optimized_blur_buffer != NULL) {
+		wlr_buffer_drop(fbos->optimized_blur_buffer->buffer);
+		fbos->optimized_blur_buffer = NULL;
+	}
+	if (fbos->optimized_no_blur_buffer != NULL) {
+		wlr_buffer_drop(fbos->optimized_no_blur_buffer->buffer);
+		fbos->optimized_no_blur_buffer = NULL;
+	}
+	if (fbos->blur_saved_pixels_buffer != NULL) {
+		wlr_buffer_drop(fbos->blur_saved_pixels_buffer->buffer);
+		fbos->blur_saved_pixels_buffer = NULL;
+	}
+	if (fbos->effects_buffer != NULL) {
+		wlr_buffer_drop(fbos->effects_buffer->buffer);
+		fbos->effects_buffer = NULL;
+	}
+	if (fbos->effects_buffer_swapped != NULL) {
+		wlr_buffer_drop(fbos->effects_buffer_swapped->buffer);
+		fbos->effects_buffer_swapped = NULL;
+	}
+
+	wl_list_remove(&fbos->link);
+	wlr_addon_finish(&fbos->addon);
+	free(fbos);
+}
+
+static const struct wlr_addon_interface fbos_addon_impl = {
+	.name = "fx_offscreen_buffers",
+	.destroy = addon_handle_destroy,
+};
+
+static bool fx_offscreen_buffers_assign(struct wlr_output *output,
+		struct fx_offscreen_buffers *fbos) {
+	wlr_addon_init(&fbos->addon, &output->addons, output, &fbos_addon_impl);
+	return true;
+}
+
+void fx_offscreen_buffers_destroy(struct fx_offscreen_buffers *fbos) {
+	addon_handle_destroy(&fbos->addon);
+}
+
+struct fx_offscreen_buffers *fx_offscreen_buffers_try_get(struct wlr_output *output) {
+	struct fx_offscreen_buffers *fbos = NULL;
+	if (!output) {
+		return NULL;
+	}
+
+	struct wlr_addon *addon = wlr_addon_find(&output->addons, output,
+			&fbos_addon_impl);
+	if (!addon) {
+		goto create_new;
+	}
+
+	if (!(fbos = wl_container_of(addon, fbos, addon))) {
+		goto create_new;
+	}
+	return fbos;
+
+create_new:;
+	struct fx_renderer *renderer = fx_get_renderer(output->renderer);
+	if (!renderer) {
+		return NULL;
+	}
+
+	fbos = calloc(1, sizeof(*fbos));
+	if (!fbos) {
+		wlr_log(WLR_ERROR, "Could not allocate a fx_offscreen_buffers");
+		return NULL;
+	}
+
+	if (!fx_offscreen_buffers_assign(output, fbos)) {
+		wlr_log(WLR_ERROR, "Could not assign fx_offscreen_buffers to output: '%s'",
+				output->name);
+		free(fbos);
+		return NULL;
+	}
+	wl_list_insert(&renderer->offscreen_buffers, &fbos->link);
+	return fbos;
+}
diff --git a/scenefx/render/fx_renderer/fx_pass.c b/scenefx/render/fx_renderer/fx_pass.c
index 92df07d..e0a7774 100644
--- a/scenefx/render/fx_renderer/fx_pass.c
+++ b/scenefx/render/fx_renderer/fx_pass.c
@@ -1,3 +1,4 @@
+#include <math.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <assert.h>
@@ -14,9 +15,9 @@
 #include "render/fx_renderer/fx_renderer.h"
 #include "render/fx_renderer/shaders.h"
 #include "render/pass.h"
+#include "render/tracy.h"
+#include "scenefx/render/fx_renderer/fx_offscreen_buffers.h"
 #include "scenefx/render/fx_renderer/fx_renderer.h"
-#include "scenefx/render/fx_renderer/fx_effect_framebuffers.h"
-#include "scenefx/types/fx/corner_location.h"
 #include "scenefx/types/fx/blur_data.h"
 #include "util/matrix.h"
 
@@ -25,10 +26,10 @@
 struct fx_render_texture_options fx_render_texture_options_default(
 		const struct wlr_render_texture_options *base) {
 	struct fx_render_texture_options options = {
-		.corner_radius = 0,
-		.corners = CORNER_LOCATION_NONE,
+		.corners = {0},
 		.discard_transparent = false,
 		.clip_box = NULL,
+		.clipped_region = {0},
 	};
 	memcpy(&options.base, base, sizeof(*base));
 	return options;
@@ -40,16 +41,58 @@ struct fx_render_rect_options fx_render_rect_options_default(
 		.base = *base,
 		.clipped_region = {
 			.area = { .0, .0, .0, .0 },
-			.corner_radius = 0,
-			.corners = CORNER_LOCATION_NONE,
+			.corners = {0},
 		},
 	};
 	return options;
 }
 
-// Gets a non-transformed wlr_box
-static struct wlr_box get_monitor_box(struct wlr_output *output) {
-	return (struct wlr_box) { 0, 0, output->width, output->height };
+bool fx_render_pass_init_offscreen_buffers(struct wlr_render_pass *render_pass,
+		struct wlr_output *output) {
+	struct fx_gles_render_pass *pass = fx_get_render_pass(render_pass);
+	if (output == NULL) {
+		pass->fx_offscreen_buffers = NULL;
+		return false;
+	}
+	if (pass->fx_offscreen_buffers != NULL) {
+		wlr_log(WLR_ERROR, "Extra buffers called twice. Ignoring...");
+		return true;
+	}
+
+	// For per output framebuffers
+	pass->fx_offscreen_buffers = fx_offscreen_buffers_try_get(output);
+	if (pass->fx_offscreen_buffers == NULL) {
+		wlr_log(WLR_ERROR, "Failed to get/create effect framebuffers for output: %s",
+				output->name);
+		return false;
+	}
+
+	// Update the buffers if needed
+	struct fx_renderer *renderer = pass->buffer->renderer;
+	const int width = pass->buffer->buffer->width;
+	const int height = pass->buffer->buffer->height;
+	bool failed = false;
+	fx_framebuffer_get_or_create_custom(renderer, output->allocator, width, height, false,
+			&pass->fx_offscreen_buffers->blur_saved_pixels_buffer, &failed);
+	fx_framebuffer_get_or_create_custom(renderer, output->allocator, width, height, true,
+			&pass->fx_offscreen_buffers->effects_buffer, &failed);
+	fx_framebuffer_get_or_create_custom(renderer, output->allocator, width, height, true,
+			&pass->fx_offscreen_buffers->effects_buffer_swapped, &failed);
+	fx_framebuffer_get_or_create_custom(renderer, output->allocator, width, height, false,
+			&pass->fx_offscreen_buffers->optimized_blur_buffer, &failed);
+	fx_framebuffer_get_or_create_custom(renderer, output->allocator, width, height, false,
+			&pass->fx_offscreen_buffers->optimized_no_blur_buffer, &failed);
+
+	// Bind back to the default buffer
+	fx_framebuffer_bind(pass->buffer);
+
+	if (failed) {
+		fx_offscreen_buffers_destroy(pass->fx_offscreen_buffers);
+		pass->fx_offscreen_buffers = NULL;
+		wlr_log(WLR_ERROR, "Failed to create effect framebuffers");
+		return false;
+	}
+	return true;
 }
 
 ///
@@ -58,18 +101,19 @@ static struct wlr_box get_monitor_box(struct wlr_output *output) {
 
 static const struct wlr_render_pass_impl render_pass_impl;
 
-static struct fx_gles_render_pass *get_render_pass(struct wlr_render_pass *wlr_pass) {
-	assert(wlr_pass->impl == &render_pass_impl);
-	struct fx_gles_render_pass *pass = wl_container_of(wlr_pass, pass, base);
+struct fx_gles_render_pass *fx_get_render_pass(struct wlr_render_pass *render_pass) {
+	assert(render_pass->impl == &render_pass_impl);
+	struct fx_gles_render_pass *pass = wl_container_of(render_pass, pass, base);
 	return pass;
 }
 
 static bool render_pass_submit(struct wlr_render_pass *wlr_pass) {
-	struct fx_gles_render_pass *pass = get_render_pass(wlr_pass);
+	struct fx_gles_render_pass *pass = fx_get_render_pass(wlr_pass);
 	struct fx_renderer *renderer = pass->buffer->renderer;
 	struct fx_render_timer *timer = pass->timer;
 	bool ok = false;
 
+	TRACY_BOTH_ZONES_START(pass->buffer->renderer);
 	push_fx_debug(renderer);
 
 	if (timer) {
@@ -111,14 +155,17 @@ out:
 	glBindFramebuffer(GL_FRAMEBUFFER, 0);
 
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
+	TRACY_GPU_ZONE_COLLECT(renderer);
+
 	wlr_egl_restore_context(&pass->prev_ctx);
 
 	wlr_drm_syncobj_timeline_unref(pass->signal_timeline);
 	wlr_buffer_unlock(pass->buffer->buffer);
-	struct fx_effect_framebuffers *fbos = fx_effect_framebuffers_try_get(pass->output);
-	if (fbos) {
-		pixman_region32_fini(&fbos->blur_padding_region);
-	}
+
+	pass->fx_offscreen_buffers = NULL;
+	pixman_region32_fini(&pass->blur_padding_region);
+
 	free(pass);
 
 	return ok;
@@ -126,19 +173,21 @@ out:
 
 static void render_pass_add_texture(struct wlr_render_pass *wlr_pass,
 		const struct wlr_render_texture_options *options) {
-	struct fx_gles_render_pass *pass = get_render_pass(wlr_pass);
+	struct fx_gles_render_pass *pass = fx_get_render_pass(wlr_pass);
 	const struct fx_render_texture_options fx_options =
 		fx_render_texture_options_default(options);
 	// Re-use fx function but with default options
+	// TODO: Simplified version?
 	fx_render_pass_add_texture(pass, &fx_options);
 }
 
 static void render_pass_add_rect(struct wlr_render_pass *wlr_pass,
 		const struct wlr_render_rect_options *options) {
-	struct fx_gles_render_pass *pass = get_render_pass(wlr_pass);
+	struct fx_gles_render_pass *pass = fx_get_render_pass(wlr_pass);
 	const struct fx_render_rect_options fx_options =
 		fx_render_rect_options_default(options);
 	// Re-use fx function but with default options
+	// TODO: Simplified version?
 	fx_render_pass_add_rect(pass, &fx_options);
 }
 
@@ -275,6 +324,30 @@ static void setup_blending(enum wlr_render_blend_mode mode) {
 	}
 }
 
+static bool apply_clip_region(pixman_region32_t *clip_region,
+		const struct wlr_box *clipped_region_box, const struct fx_corner_fradii *corners) {
+	if (!wlr_box_empty(clipped_region_box)) {
+		float top = fmax(corners->top_left, corners->top_right);
+		float bottom = fmax(corners->bottom_left, corners->bottom_right);
+		float left = fmax(corners->top_left, corners->bottom_left);
+		float right = fmax(corners->top_right, corners->bottom_right);
+
+		pixman_region32_t user_clip_region;
+		pixman_region32_init_rect(
+			&user_clip_region,
+			clipped_region_box->x + (left * 0.3),
+			clipped_region_box->y + (top * 0.3),
+			fmax(clipped_region_box->width - (left + right) * 0.3, 0),
+			fmax(clipped_region_box->height - (top + bottom) * 0.3, 0)
+		);
+		pixman_region32_subtract(clip_region, clip_region, &user_clip_region);
+		pixman_region32_fini(&user_clip_region);
+		return true;
+	}
+
+	return false;
+}
+
 void fx_render_pass_add_texture(struct fx_gles_render_pass *pass,
 		const struct fx_render_texture_options *fx_options) {
 	const struct wlr_render_texture_options *options = &fx_options->base;
@@ -283,19 +356,27 @@ void fx_render_pass_add_texture(struct fx_gles_render_pass *pass,
 
 	struct tex_shader *shader = NULL;
 
+	bool use_effects = !fx_corner_fradii_is_empty(&fx_options->corners)
+		|| clipped_fregion_is_valid(&fx_options->clipped_region);
 	switch (texture->target) {
 	case GL_TEXTURE_2D:
 		if (texture->has_alpha) {
-			shader = &renderer->shaders.tex_rgba;
+			shader = use_effects
+				? &renderer->shaders.tex_effects_rgba
+				: &renderer->shaders.tex_rgba;
 		} else {
-			shader = &renderer->shaders.tex_rgbx;
+			shader = use_effects
+				? &renderer->shaders.tex_effects_rgbx
+				: &renderer->shaders.tex_rgbx;
 		}
 		break;
 	case GL_TEXTURE_EXTERNAL_OES:
 		// EGL_EXT_image_dma_buf_import_modifiers requires
 		// GL_OES_EGL_image_external
 		assert(renderer->exts.OES_egl_image_external);
-		shader = &renderer->shaders.tex_ext;
+		shader = use_effects
+			? &renderer->shaders.tex_effects_ext
+			: &renderer->shaders.tex_ext;
 		break;
 	default:
 		abort();
@@ -317,34 +398,64 @@ void fx_render_pass_add_texture(struct fx_gles_render_pass *pass,
 	src_fbox.width /= options->texture->width;
 	src_fbox.height /= options->texture->height;
 
+	TRACY_BOTH_ZONES_START(renderer);
+	TRACY_ZONE_TEXT_f("dst_box (WxH, X, Y): %dx%d, %d, %d",
+			dst_box.width, dst_box.height, dst_box.x, dst_box.y);
+	TRACY_ZONE_TEXT_f("clip_box (WxH, X, Y): %dx%d, %d, %d",
+			clip_box->width, clip_box->height, clip_box->x, clip_box->y);
+	TRACY_ZONE_TEXT_f("src_box (WxH, X, Y): %lfx%lf, %lf, %lf",
+			src_fbox.width, src_fbox.height, src_fbox.x, src_fbox.y);
+	TRACY_ZONE_TEXT_f("Shader Type: %s",
+			use_effects ? (
+			 shader == &renderer->shaders.tex_effects_rgba ? "Effects RGBA"
+			 : shader == &renderer->shaders.tex_effects_rgbx ? "Effects RGBX"
+			 : "Effects EXT"
+			) : (
+				shader == &renderer->shaders.tex_rgba ? "RGBA"
+				: shader == &renderer->shaders.tex_rgbx ? "RGBX"
+				: "EXT"
+			)
+		);
 	push_fx_debug(renderer);
 
 	if (options->wait_timeline != NULL) {
 		int sync_file_fd =
 			wlr_drm_syncobj_timeline_export_sync_file(options->wait_timeline, options->wait_point);
 		if (sync_file_fd < 0) {
+			TRACY_BOTH_ZONES_END_FAIL;
 			return;
 		}
 
 		EGLSyncKHR sync = wlr_egl_create_sync(renderer->egl, sync_file_fd);
 		close(sync_file_fd);
 		if (sync == EGL_NO_SYNC_KHR) {
+			TRACY_BOTH_ZONES_END_FAIL;
 			return;
 		}
 
 		bool ok = wlr_egl_wait_sync(renderer->egl, sync);
 		wlr_egl_destroy_sync(renderer->egl, sync);
 		if (!ok) {
+			TRACY_BOTH_ZONES_END_FAIL;
 			return;
 		}
 	}
 
-	bool has_alpha = texture->has_alpha
-		|| alpha < 1.0
-		|| fx_options->corner_radius > 0
-		|| fx_options->discard_transparent;
+	bool has_alpha = texture->has_alpha || alpha < 1.0 || use_effects;
+	TRACY_ZONE_TEXT_f("Has Alpha: %d", has_alpha);
 	setup_blending(!has_alpha ? WLR_RENDER_BLEND_MODE_NONE : options->blend_mode);
 
+	pixman_region32_t clip_region;
+	if (options->clip) {
+		pixman_region32_init(&clip_region);
+		pixman_region32_copy(&clip_region, options->clip);
+	} else {
+		pixman_region32_init_rect(&clip_region, dst_box.x, dst_box.y, dst_box.width, dst_box.height);
+	}
+	const struct wlr_box clipped_region_box = fx_options->clipped_region.area;
+	struct fx_corner_fradii clipped_region_corners = fx_options->clipped_region.corners;
+	apply_clip_region(&clip_region, &clipped_region_box, &clipped_region_corners);
+
 	glUseProgram(shader->program);
 
 	glActiveTexture(GL_TEXTURE0);
@@ -361,29 +472,33 @@ void fx_render_pass_add_texture(struct fx_gles_render_pass *pass,
 		break;
 	}
 
-	enum corner_location corners = fx_options->corners;
-
 	glUniform1i(shader->tex, 0);
 	glUniform1f(shader->alpha, alpha);
-	glUniform2f(shader->size, clip_box->width, clip_box->height);
-	glUniform2f(shader->position, clip_box->x, clip_box->y);
+
 	glUniform1f(shader->discard_transparent, fx_options->discard_transparent);
-	glUniform1f(shader->radius_top_left, (CORNER_LOCATION_TOP_LEFT & corners) == CORNER_LOCATION_TOP_LEFT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader->radius_top_right, (CORNER_LOCATION_TOP_RIGHT & corners) == CORNER_LOCATION_TOP_RIGHT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader->radius_bottom_left, (CORNER_LOCATION_BOTTOM_LEFT & corners) == CORNER_LOCATION_BOTTOM_LEFT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader->radius_bottom_right, (CORNER_LOCATION_BOTTOM_RIGHT & corners) == CORNER_LOCATION_BOTTOM_RIGHT ?
-			fx_options->corner_radius : 0);
+
+	if (use_effects) {
+		struct fx_corner_fradii corners = fx_options->corners;
+
+		glUniform2f(shader->effects.size, clip_box->width, clip_box->height);
+		glUniform2f(shader->effects.position, clip_box->x, clip_box->y);
+		uniform_corner_radii_set(&shader->effects.radius, &corners);
+
+		glUniform2f(shader->effects.clip_size, clipped_region_box.width, clipped_region_box.height);
+		glUniform2f(shader->effects.clip_position, clipped_region_box.x, clipped_region_box.y);
+		uniform_corner_radii_set(&shader->effects.clip_radius, &clipped_region_corners);
+	}
 
 	set_proj_matrix(shader->proj, pass->projection_matrix, &dst_box);
 	set_tex_matrix(shader->tex_proj, options->transform, &src_fbox);
 
-	render(&dst_box, options->clip, shader->pos_attrib);
+	render(&dst_box, &clip_region, shader->pos_attrib);
+	pixman_region32_fini(&clip_region);
 
 	glBindTexture(texture->target, 0);
+
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 }
 
 void fx_render_pass_add_rect(struct fx_gles_render_pass *pass,
@@ -394,57 +509,71 @@ void fx_render_pass_add_rect(struct fx_gles_render_pass *pass,
 
 	const struct wlr_render_color *color = &options->color;
 	struct wlr_box box;
-	wlr_render_rect_options_get_box(options, pass->buffer->buffer, &box);
+	struct wlr_buffer *wlr_buffer = pass->buffer->buffer;
+	wlr_render_rect_options_get_box(options, wlr_buffer, &box);
 
-	pixman_region32_t clip_region;
-	if (options->clip) {
-		pixman_region32_init(&clip_region);
-		pixman_region32_copy(&clip_region, options->clip);
-	} else {
-		pixman_region32_init_rect(&clip_region, box.x, box.y, box.width, box.height);
-	}
-	const struct wlr_box clipped_region_box = fx_options->clipped_region.area;
-	enum corner_location clipped_region_corners = fx_options->clipped_region.corners;
-	int clipped_region_corner_radius = clipped_region_corners != CORNER_LOCATION_NONE ?
-		fx_options->clipped_region.corner_radius : 0;
-	if (!wlr_box_empty(&clipped_region_box)) {
-		pixman_region32_t user_clip_region;
-		pixman_region32_init_rect(
-			&user_clip_region,
-			clipped_region_box.x + clipped_region_corner_radius * 0.3,
-			clipped_region_box.y + clipped_region_corner_radius * 0.3,
-			fmax(clipped_region_box.width - clipped_region_corner_radius * 0.6, 0),
-			fmax(clipped_region_box.height - clipped_region_corner_radius * 0.6, 0)
-		);
-		pixman_region32_subtract(&clip_region, &clip_region, &user_clip_region);
-		pixman_region32_fini(&user_clip_region);
+	const bool should_clip = clipped_fregion_is_valid(&fx_options->clipped_region);
+
+	enum wlr_render_blend_mode blend_mode =
+		(color->a == 1.0 && !should_clip) ? WLR_RENDER_BLEND_MODE_NONE : options->blend_mode;
+	const bool use_fast_clear =
+		blend_mode == WLR_RENDER_BLEND_MODE_NONE && // includes check for `should_clip`
+		options->clip == NULL &&
+		box.x == 0 && box.y == 0 &&
+		box.width == wlr_buffer->width &&
+		box.height == wlr_buffer->height;
 
-		push_fx_debug(renderer);
-		setup_blending(options->blend_mode);
+	TRACY_BOTH_ZONES_START(renderer);
+	TRACY_ZONE_TEXT_f("Box (WxH, X, Y): %dx%d, %d, %d", box.width, box.height, box.x, box.y);
+	TRACY_ZONE_TEXT_f("Color RGBA: %f, %f, %f, %f", color->r, color->g, color->b, color->a);
+	TRACY_ZONE_TEXT_f("Use fast clear optimization: %d", use_fast_clear);
+
+	push_fx_debug(renderer);
+	if (use_fast_clear) {
+		glClearColor(color->r, color->g, color->b, color->a);
+		glClear(GL_COLOR_BUFFER_BIT);
 	} else {
-		push_fx_debug(renderer);
-		setup_blending(color->a == 1.0 ? WLR_RENDER_BLEND_MODE_NONE : options->blend_mode);
-	}
+		const struct wlr_box *clipped_region_box = &fx_options->clipped_region.area;
+		const struct fx_corner_fradii *clipped_region_corners = &fx_options->clipped_region.corners;
+
+		TRACY_ZONE_TEXT_f("Clip Box (WxH, X, Y): %dx%d, %d, %d",
+				clipped_region_box->width, clipped_region_box->height,
+				clipped_region_box->x, clipped_region_box->y);
+		TRACY_ZONE_TEXT_f("Clip Box Corners (TL, TR, BL, BR): %f, %f, %f, %f",
+				clipped_region_corners->top_left,
+				clipped_region_corners->top_right,
+				clipped_region_corners->bottom_left,
+				clipped_region_corners->bottom_right);
+
+		pixman_region32_t clip_region;
+		if (options->clip) {
+			pixman_region32_init(&clip_region);
+			pixman_region32_copy(&clip_region, options->clip);
+		} else {
+			pixman_region32_init_rect(&clip_region, box.x, box.y, box.width, box.height);
+		}
 
-	struct quad_shader shader = renderer->shaders.quad;
-	glUseProgram(shader.program);
-	set_proj_matrix(shader.proj, pass->projection_matrix, &box);
-	glUniform4f(shader.color, color->r, color->g, color->b, color->a);
-	glUniform2f(shader.clip_size, clipped_region_box.width, clipped_region_box.height);
-	glUniform2f(shader.clip_position, clipped_region_box.x, clipped_region_box.y);
-	glUniform1f(shader.clip_radius_top_left, (CORNER_LOCATION_TOP_LEFT & clipped_region_corners) == CORNER_LOCATION_TOP_LEFT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(shader.clip_radius_top_right, (CORNER_LOCATION_TOP_RIGHT & clipped_region_corners) == CORNER_LOCATION_TOP_RIGHT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(shader.clip_radius_bottom_left, (CORNER_LOCATION_BOTTOM_LEFT & clipped_region_corners) == CORNER_LOCATION_BOTTOM_LEFT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(shader.clip_radius_bottom_right, (CORNER_LOCATION_BOTTOM_RIGHT & clipped_region_corners) == CORNER_LOCATION_BOTTOM_RIGHT ?
-			clipped_region_corner_radius : 0);
-
-	render(&box, &clip_region, renderer->shaders.quad.pos_attrib);
-	pixman_region32_fini(&clip_region);
+		apply_clip_region(&clip_region, clipped_region_box, clipped_region_corners);
+
+		setup_blending(blend_mode);
+		struct quad_shader *shader = should_clip
+			? &renderer->shaders.quad_clip
+			: &renderer->shaders.quad;
+		glUseProgram(shader->program);
+		set_proj_matrix(shader->proj, pass->projection_matrix, &box);
+		glUniform4f(shader->color, color->r, color->g, color->b, color->a);
+		if (should_clip) {
+			glUniform2f(shader->effects.clip_size, clipped_region_box->width, clipped_region_box->height);
+			glUniform2f(shader->effects.clip_position, clipped_region_box->x, clipped_region_box->y);
+			uniform_corner_radii_set(&shader->effects.clip_radius, clipped_region_corners);
+		}
+		render(&box, &clip_region, shader->pos_attrib);
+
+		pixman_region32_fini(&clip_region);
+	}
 
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 }
 
 void fx_render_pass_add_rect_grad(struct fx_gles_render_pass *pass,
@@ -454,51 +583,66 @@ void fx_render_pass_add_rect_grad(struct fx_gles_render_pass *pass,
 	struct fx_renderer *renderer = pass->buffer->renderer;
 
 	if (renderer->shaders.quad_grad.max_len <= fx_options->gradient.count) {
-		EGLint client_version;
-		eglQueryContext(renderer->egl->display, renderer->egl->context,
-			EGL_CONTEXT_CLIENT_VERSION, &client_version);
-
 		glDeleteProgram(renderer->shaders.quad_grad.program);
-		if (!link_quad_grad_program(&renderer->shaders.quad_grad, (GLint) client_version, fx_options->gradient.count + 1)) {
+		if (!link_quad_grad_program(&renderer->shaders.quad_grad, fx_options->gradient.count + 1)) {
 			wlr_log(WLR_ERROR, "Could not link quad shader after updating max_len to %d. Aborting renderer", fx_options->gradient.count + 1);
 			abort();
 		}
 	}
 
 	struct wlr_box box;
-	wlr_render_rect_options_get_box(options, pass->buffer->buffer, &box);
-
+	struct wlr_buffer *wlr_buffer = pass->buffer->buffer;
+	wlr_render_rect_options_get_box(options, wlr_buffer, &box);
+
+	TRACY_BOTH_ZONES_START(renderer);
+	TRACY_ZONE_TEXT_f("Box (WxH, X, Y): %dx%d, %d, %d", box.width, box.height, box.x, box.y);
+	TRACY_ZONE_TEXT_f("Gradient:");
+	TRACY_ZONE_TEXT_f("\tNum Colors: %d", fx_options->gradient.count);
+	TRACY_ZONE_TEXT_f("\tBlend: %d", fx_options->gradient.blend);
+	TRACY_ZONE_TEXT_f("\tDegree: %f", fx_options->gradient.degree);
+	TRACY_ZONE_TEXT_f("\tType: %s",
+			fx_options->gradient.linear == 1 ? "Linear"
+			: fx_options->gradient.linear == 2 ? "Conic"
+			: "Unknown");
+	TRACY_ZONE_TEXT_f("\tOrigin: %fx%f",
+			fx_options->gradient.origin[0], fx_options->gradient.origin[1]);
+	TRACY_ZONE_TEXT_f("\tRange (WxH, X, Y): %dx%d, %d, %d",
+			fx_options->gradient.range.width, fx_options->gradient.range.height,
+			fx_options->gradient.range.x, fx_options->gradient.range.y);
+	// TODO: Display Colors (not really sure how it works without a scene example...)
 	push_fx_debug(renderer);
+
 	setup_blending(options->blend_mode);
 
-	glUseProgram(renderer->shaders.quad_grad.program);
+	struct quad_grad_shader shader = renderer->shaders.quad_grad;
+	glUseProgram(shader.program);
 
-	set_proj_matrix(renderer->shaders.quad_grad.proj, pass->projection_matrix, &box);
-	glUniform4fv(renderer->shaders.quad_grad.colors, fx_options->gradient.count, (GLfloat*)fx_options->gradient.colors);
-	glUniform1i(renderer->shaders.quad_grad.count, fx_options->gradient.count);
-	glUniform2f(renderer->shaders.quad_grad.size, fx_options->gradient.range.width, fx_options->gradient.range.height);
-	glUniform1f(renderer->shaders.quad_grad.degree, fx_options->gradient.degree);
-	glUniform1f(renderer->shaders.quad_grad.linear, fx_options->gradient.linear);
-	glUniform1f(renderer->shaders.quad_grad.blend, fx_options->gradient.blend);
-	glUniform2f(renderer->shaders.quad_grad.grad_box, fx_options->gradient.range.x, fx_options->gradient.range.y);
-	glUniform2f(renderer->shaders.quad_grad.origin, fx_options->gradient.origin[0], fx_options->gradient.origin[1]);
+	set_proj_matrix(shader.proj, pass->projection_matrix, &box);
+	glUniform4fv(shader.colors, fx_options->gradient.count, (GLfloat*)fx_options->gradient.colors);
+	glUniform1i(shader.count, fx_options->gradient.count);
+	glUniform2f(shader.size, fx_options->gradient.range.width, fx_options->gradient.range.height);
+	glUniform1f(shader.degree, fx_options->gradient.degree);
+	glUniform1f(shader.linear, fx_options->gradient.linear);
+	glUniform1f(shader.blend, fx_options->gradient.blend);
+	glUniform2f(shader.grad_box, fx_options->gradient.range.x, fx_options->gradient.range.y);
+	glUniform2f(shader.origin, fx_options->gradient.origin[0], fx_options->gradient.origin[1]);
 
-	render(&box, options->clip, renderer->shaders.quad_grad.pos_attrib);
+	render(&box, options->clip, shader.pos_attrib);
 
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 }
 
 void fx_render_pass_add_rounded_rect(struct fx_gles_render_pass *pass,
 		const struct fx_render_rounded_rect_options *fx_options) {
 	const struct wlr_render_rect_options *options = &fx_options->base;
 
-	struct wlr_box box;
-	wlr_render_rect_options_get_box(options, pass->buffer->buffer, &box);
-	assert(box.width > 0 && box.height > 0);
-
 	struct fx_renderer *renderer = pass->buffer->renderer;
 
 	const struct wlr_render_color *color = &options->color;
+	struct wlr_box box;
+	struct wlr_buffer *wlr_buffer = pass->buffer->buffer;
+	wlr_render_rect_options_get_box(options, wlr_buffer, &box);
 
 	pixman_region32_t clip_region;
 	if (options->clip) {
@@ -507,24 +651,28 @@ void fx_render_pass_add_rounded_rect(struct fx_gles_render_pass *pass,
 	} else {
 		pixman_region32_init_rect(&clip_region, box.x, box.y, box.width, box.height);
 	}
-	const struct wlr_box clipped_region_box = fx_options->clipped_region.area;
-	enum corner_location clipped_region_corners = fx_options->clipped_region.corners;
-	int clipped_region_corner_radius = clipped_region_corners != CORNER_LOCATION_NONE ?
-		fx_options->clipped_region.corner_radius : 0;
-	if (!wlr_box_empty(&clipped_region_box)) {
-		pixman_region32_t user_clip_region;
-		pixman_region32_init_rect(
-			&user_clip_region,
-			clipped_region_box.x + clipped_region_corner_radius * 0.3,
-			clipped_region_box.y + clipped_region_corner_radius * 0.3,
-			fmax(clipped_region_box.width - clipped_region_corner_radius * 0.6, 0),
-			fmax(clipped_region_box.height - clipped_region_corner_radius * 0.6, 0)
-		);
-		pixman_region32_subtract(&clip_region, &clip_region, &user_clip_region);
-		pixman_region32_fini(&user_clip_region);
-	}
-
+	const struct wlr_box *clipped_region_box = &fx_options->clipped_region.area;
+	const struct fx_corner_fradii *clipped_region_corners = &fx_options->clipped_region.corners;
+	apply_clip_region(&clip_region, clipped_region_box, clipped_region_corners);
+
+	TRACY_BOTH_ZONES_START(renderer);
+	TRACY_ZONE_TEXT_f("Box (WxH, X, Y): %dx%d, %d, %d", box.width, box.height, box.x, box.y);
+	TRACY_ZONE_TEXT_f("Clip Box (WxH, X, Y): %dx%d, %d, %d",
+			clipped_region_box->width, clipped_region_box->height,
+			clipped_region_box->x, clipped_region_box->y);
+	TRACY_ZONE_TEXT_f("Clip Box Corners (TL, TR, BL, BR): %f, %f, %f, %f",
+			clipped_region_corners->top_left,
+			clipped_region_corners->top_right,
+			clipped_region_corners->bottom_left,
+			clipped_region_corners->bottom_right);
+	TRACY_ZONE_TEXT_f("Color RGBA: %f, %f, %f, %f", color->r, color->g, color->b, color->a);
+	TRACY_ZONE_TEXT_f("Corners (TL, TR, BL, BR): %f, %f, %f, %f",
+			clipped_region_corners->top_left,
+			clipped_region_corners->top_right,
+			clipped_region_corners->bottom_left,
+			clipped_region_corners->bottom_right);
 	push_fx_debug(renderer);
+
 	setup_blending(WLR_RENDER_BLEND_MODE_PREMULTIPLIED);
 
 	struct quad_round_shader shader = renderer->shaders.quad_round;
@@ -536,32 +684,19 @@ void fx_render_pass_add_rounded_rect(struct fx_gles_render_pass *pass,
 
 	glUniform2f(shader.size, box.width, box.height);
 	glUniform2f(shader.position, box.x, box.y);
-	glUniform2f(shader.clip_size, clipped_region_box.width, clipped_region_box.height);
-	glUniform2f(shader.clip_position, clipped_region_box.x, clipped_region_box.y);
-	glUniform1f(shader.clip_radius_top_left, (CORNER_LOCATION_TOP_LEFT & clipped_region_corners) == CORNER_LOCATION_TOP_LEFT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(shader.clip_radius_top_right, (CORNER_LOCATION_TOP_RIGHT & clipped_region_corners) == CORNER_LOCATION_TOP_RIGHT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(shader.clip_radius_bottom_left, (CORNER_LOCATION_BOTTOM_LEFT & clipped_region_corners) == CORNER_LOCATION_BOTTOM_LEFT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(shader.clip_radius_bottom_right, (CORNER_LOCATION_BOTTOM_RIGHT & clipped_region_corners) == CORNER_LOCATION_BOTTOM_RIGHT ?
-			clipped_region_corner_radius : 0);
-
-	enum corner_location corners = fx_options->corners;
-	glUniform1f(shader.radius_top_left, (CORNER_LOCATION_TOP_LEFT & corners) == CORNER_LOCATION_TOP_LEFT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader.radius_top_right, (CORNER_LOCATION_TOP_RIGHT & corners) == CORNER_LOCATION_TOP_RIGHT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader.radius_bottom_left, (CORNER_LOCATION_BOTTOM_LEFT & corners) == CORNER_LOCATION_BOTTOM_LEFT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader.radius_bottom_right, (CORNER_LOCATION_BOTTOM_RIGHT & corners) == CORNER_LOCATION_BOTTOM_RIGHT ?
-			fx_options->corner_radius : 0);
+	glUniform2f(shader.clip_size, clipped_region_box->width, clipped_region_box->height);
+	glUniform2f(shader.clip_position, clipped_region_box->x, clipped_region_box->y);
+	uniform_corner_radii_set(&shader.clip_radius, clipped_region_corners);
+
+	struct fx_corner_fradii corners = fx_options->corners;
+	uniform_corner_radii_set(&shader.radius, &corners);
 	glUniform1f(shader.fade_inset, fx_options->fade_inset);
 
 	render(&box, &clip_region, renderer->shaders.quad_round.pos_attrib);
 	pixman_region32_fini(&clip_region);
 
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 }
 
 void fx_render_pass_add_rounded_rect_grad(struct fx_gles_render_pass *pass,
@@ -571,21 +706,40 @@ void fx_render_pass_add_rounded_rect_grad(struct fx_gles_render_pass *pass,
 	struct fx_renderer *renderer = pass->buffer->renderer;
 
 	if (renderer->shaders.quad_grad_round.max_len <= fx_options->gradient.count) {
-		EGLint client_version;
-		eglQueryContext(renderer->egl->display, renderer->egl->context,
-			EGL_CONTEXT_CLIENT_VERSION, &client_version);
-
 		glDeleteProgram(renderer->shaders.quad_grad_round.program);
-		if (!link_quad_grad_round_program(&renderer->shaders.quad_grad_round, (GLint) client_version, fx_options->gradient.count + 1)) {
+		if (!link_quad_grad_round_program(&renderer->shaders.quad_grad_round, fx_options->gradient.count + 1)) {
 			wlr_log(WLR_ERROR, "Could not link quad shader after updating max_len to %d. Aborting renderer", fx_options->gradient.count + 1);
 			abort();
 		}
 	}
 
 	struct wlr_box box;
-	wlr_render_rect_options_get_box(options, pass->buffer->buffer, &box);
-
+	struct wlr_buffer *wlr_buffer = pass->buffer->buffer;
+	wlr_render_rect_options_get_box(options, wlr_buffer, &box);
+
+	TRACY_BOTH_ZONES_START(renderer);
+	TRACY_ZONE_TEXT_f("Box (WxH, X, Y): %dx%d, %d, %d", box.width, box.height, box.x, box.y);
+	TRACY_ZONE_TEXT_f("Corners (TL, TR, BL, BR): %f, %f, %f, %f",
+			fx_options->corners.top_left,
+			fx_options->corners.top_right,
+			fx_options->corners.bottom_left,
+			fx_options->corners.bottom_right);
+	TRACY_ZONE_TEXT_f("Gradient:");
+	TRACY_ZONE_TEXT_f("\tNum Colors: %d", fx_options->gradient.count);
+	TRACY_ZONE_TEXT_f("\tBlend: %d", fx_options->gradient.blend);
+	TRACY_ZONE_TEXT_f("\tDegree: %f", fx_options->gradient.degree);
+	TRACY_ZONE_TEXT_f("\tType: %s",
+			fx_options->gradient.linear == 1 ? "Linear"
+			: fx_options->gradient.linear == 2 ? "Conic"
+			: "Unknown");
+	TRACY_ZONE_TEXT_f("\tOrigin: %fx%f",
+			fx_options->gradient.origin[0], fx_options->gradient.origin[1]);
+	TRACY_ZONE_TEXT_f("\tRange (WxH, X, Y): %dx%d, %d, %d",
+			fx_options->gradient.range.width, fx_options->gradient.range.height,
+			fx_options->gradient.range.x, fx_options->gradient.range.y);
+	// TODO: Display Colors (not really sure how it works without a scene example...)
 	push_fx_debug(renderer);
+
 	setup_blending(WLR_RENDER_BLEND_MODE_PREMULTIPLIED);
 
 	struct quad_grad_round_shader shader = renderer->shaders.quad_grad_round;
@@ -605,19 +759,13 @@ void fx_render_pass_add_rounded_rect_grad(struct fx_gles_render_pass *pass,
 	glUniform2f(shader.grad_box, fx_options->gradient.range.x, fx_options->gradient.range.y);
 	glUniform2f(shader.origin, fx_options->gradient.origin[0], fx_options->gradient.origin[1]);
 
-	enum corner_location corners = fx_options->corners;
-	glUniform1f(shader.radius_top_left, (CORNER_LOCATION_TOP_LEFT & corners) == CORNER_LOCATION_TOP_LEFT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader.radius_top_right, (CORNER_LOCATION_TOP_RIGHT & corners) == CORNER_LOCATION_TOP_RIGHT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader.radius_bottom_left, (CORNER_LOCATION_BOTTOM_LEFT & corners) == CORNER_LOCATION_BOTTOM_LEFT ?
-			fx_options->corner_radius : 0);
-	glUniform1f(shader.radius_bottom_right, (CORNER_LOCATION_BOTTOM_RIGHT & corners) == CORNER_LOCATION_BOTTOM_RIGHT ?
-			fx_options->corner_radius : 0);
+	struct fx_corner_fradii corners = fx_options->corners;
+	uniform_corner_radii_set(&shader.radius, &corners);
 
 	render(&box, options->clip, shader.pos_attrib);
 
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 }
 
 void fx_render_pass_add_box_shadow(struct fx_gles_render_pass *pass,
@@ -635,23 +783,25 @@ void fx_render_pass_add_box_shadow(struct fx_gles_render_pass *pass,
 		pixman_region32_init_rect(&clip_region, box.x, box.y, box.width, box.height);
 	}
 	const struct wlr_box clipped_region_box = options->clipped_region.area;
-	enum corner_location clipped_region_corners = options->clipped_region.corners;
-	int clipped_region_corner_radius = clipped_region_corners != CORNER_LOCATION_NONE ?
-		options->clipped_region.corner_radius : 0;
-	if (!wlr_box_empty(&clipped_region_box)) {
-		pixman_region32_t user_clip_region;
-		pixman_region32_init_rect(
-			&user_clip_region,
-			clipped_region_box.x + clipped_region_corner_radius * 0.3,
-			clipped_region_box.y + clipped_region_corner_radius * 0.3,
-			fmax(clipped_region_box.width - clipped_region_corner_radius * 0.6, 0),
-			fmax(clipped_region_box.height - clipped_region_corner_radius * 0.6, 0)
-		);
-		pixman_region32_subtract(&clip_region, &clip_region, &user_clip_region);
-		pixman_region32_fini(&user_clip_region);
-	}
-
+	struct fx_corner_fradii clipped_region_corners = options->clipped_region.corners;
+	apply_clip_region(&clip_region, &clipped_region_box, &clipped_region_corners);
+
+	TRACY_BOTH_ZONES_START(renderer);
+	TRACY_ZONE_TEXT_f("Box (WxH, X, Y): %dx%d, %d, %d", box.width, box.height, box.x, box.y);
+	TRACY_ZONE_TEXT_f("Clip Box (WxH, X, Y): %dx%d, %d, %d",
+			clipped_region_box.width, clipped_region_box.height,
+			clipped_region_box.x, clipped_region_box.y);
+	TRACY_ZONE_TEXT_f("Clip Box Corners (TL, TR, BL, BR): %f, %f, %f, %f",
+			clipped_region_corners.top_left,
+			clipped_region_corners.top_right,
+			clipped_region_corners.bottom_left,
+			clipped_region_corners.bottom_right);
+	TRACY_ZONE_TEXT_f("Shadow Options:");
+	TRACY_ZONE_TEXT_f("\tColor RGBA: %f, %f, %f, %f",
+			options->color.r, options->color.g, options->color.b, options->color.a);
+	TRACY_ZONE_TEXT_f("\tBlur Sigma: %f", options->blur_sigma);
 	push_fx_debug(renderer);
+
 	// blending will practically always be needed (unless we have a madman
 	// who uses opaque shadows with zero sigma), so just enable it
 	setup_blending(WLR_RENDER_BLEND_MODE_PREMULTIPLIED);
@@ -667,18 +817,7 @@ void fx_render_pass_add_box_shadow(struct fx_gles_render_pass *pass,
 	glUniform2f(renderer->shaders.box_shadow.position, box.x, box.y);
 	glUniform1f(renderer->shaders.box_shadow.corner_radius, options->corner_radius);
 
-	glUniform1f(renderer->shaders.box_shadow.clip_radius_top_left,
-			(CORNER_LOCATION_TOP_LEFT & clipped_region_corners) == CORNER_LOCATION_TOP_LEFT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(renderer->shaders.box_shadow.clip_radius_top_right,
-			(CORNER_LOCATION_TOP_RIGHT & clipped_region_corners) == CORNER_LOCATION_TOP_RIGHT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(renderer->shaders.box_shadow.clip_radius_bottom_left,
-			(CORNER_LOCATION_BOTTOM_LEFT & clipped_region_corners) == CORNER_LOCATION_BOTTOM_LEFT ?
-			clipped_region_corner_radius : 0);
-	glUniform1f(renderer->shaders.box_shadow.clip_radius_bottom_right,
-			(CORNER_LOCATION_BOTTOM_RIGHT & clipped_region_corners) == CORNER_LOCATION_BOTTOM_RIGHT ?
-			clipped_region_corner_radius : 0);
+	uniform_corner_radii_set(&renderer->shaders.box_shadow.clip_radius, &clipped_region_corners);
 
 	glUniform2f(renderer->shaders.box_shadow.clip_position, clipped_region_box.x, clipped_region_box.y);
 	glUniform2f(renderer->shaders.box_shadow.clip_size, clipped_region_box.width, clipped_region_box.height);
@@ -689,6 +828,7 @@ void fx_render_pass_add_box_shadow(struct fx_gles_render_pass *pass,
 	glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
 
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 }
 
 // Renders the blur for each damaged rect and swaps the buffer
@@ -699,11 +839,14 @@ static void render_blur_segments(struct fx_gles_render_pass *pass,
 	struct fx_renderer *renderer = pass->buffer->renderer;
 	struct blur_data *blur_data = fx_options->blur_data;
 
+	TRACY_BOTH_ZONES_START(renderer);
+	push_fx_debug(renderer);
+
 	// Swap fbo
-	if (fx_options->current_buffer == pass->fx_effect_framebuffers->effects_buffer) {
-		fx_framebuffer_bind(pass->fx_effect_framebuffers->effects_buffer_swapped);
+	if (fx_options->current_buffer == pass->fx_offscreen_buffers->effects_buffer) {
+		fx_framebuffer_bind(pass->fx_offscreen_buffers->effects_buffer_swapped);
 	} else {
-		fx_framebuffer_bind(pass->fx_effect_framebuffers->effects_buffer);
+		fx_framebuffer_bind(pass->fx_offscreen_buffers->effects_buffer);
 	}
 
 	options->texture = fx_texture_from_buffer(&renderer->wlr_renderer,
@@ -726,8 +869,6 @@ static void render_blur_segments(struct fx_gles_render_pass *pass,
 	glDisable(GL_BLEND);
 	glDisable(GL_STENCIL_TEST);
 
-	push_fx_debug(renderer);
-
 	glUseProgram(shader->program);
 
 	glActiveTexture(GL_TEXTURE0);
@@ -762,14 +903,15 @@ static void render_blur_segments(struct fx_gles_render_pass *pass,
 
 	glBindTexture(texture->target, 0);
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 
 	wlr_texture_destroy(options->texture);
 
 	// Swap buffer. We don't want to draw to the same buffer
-	if (fx_options->current_buffer != pass->fx_effect_framebuffers->effects_buffer) {
-		fx_options->current_buffer = pass->fx_effect_framebuffers->effects_buffer;
+	if (fx_options->current_buffer != pass->fx_offscreen_buffers->effects_buffer) {
+		fx_options->current_buffer = pass->fx_offscreen_buffers->effects_buffer;
 	} else {
-		fx_options->current_buffer = pass->fx_effect_framebuffers->effects_buffer_swapped;
+		fx_options->current_buffer = pass->fx_offscreen_buffers->effects_buffer_swapped;
 	}
 }
 
@@ -796,6 +938,7 @@ static void render_blur_effects(struct fx_gles_render_pass *pass,
 	glDisable(GL_BLEND);
 	glDisable(GL_STENCIL_TEST);
 
+	TRACY_BOTH_ZONES_START(renderer);
 	push_fx_debug(renderer);
 
 	glUseProgram(shader.program);
@@ -824,50 +967,101 @@ static void render_blur_effects(struct fx_gles_render_pass *pass,
 	render(&dst_box, options->clip, shader.pos_attrib);
 
 	glBindTexture(texture->target, 0);
+
 	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 
 	wlr_texture_destroy(options->texture);
 }
 
-// Blurs the main_buffer content and returns the blurred framebuffer
+// Blurs the fx_options current_buffer content and returns the blurred framebuffer.
+// Returns NULL when the blur parameters reach 0.
 static struct fx_framebuffer *get_main_buffer_blur(struct fx_gles_render_pass *pass,
 		struct fx_render_blur_pass_options *fx_options) {
+	if (pass->fx_offscreen_buffers == NULL) {
+		wlr_log(WLR_ERROR, "FX Pass offscreen buffers not initialized. Skipping getting blur...");
+		return NULL;
+	}
+
 	struct fx_renderer *renderer = pass->buffer->renderer;
-	struct blur_data *blur_data = fx_options->blur_data;
-	struct wlr_box monitor_box = get_monitor_box(pass->output);
+	struct wlr_box buffer_bounds = {
+		0, 0,
+		fx_options->current_buffer->buffer->width, fx_options->current_buffer->buffer->height
+	};
+
+	// We don't want to affect the reference blur_data
+	struct blur_data blur_data = blur_data_apply_strength(fx_options->blur_data, fx_options->blur_strength);
+	if (fx_options->blur_strength <= 0 || !is_scene_blur_enabled(&blur_data)) {
+		return NULL;
+	}
+	fx_options->blur_data = &blur_data;
 
 	pixman_region32_t damage;
 	pixman_region32_init(&damage);
 	pixman_region32_copy(&damage, fx_options->tex_options.base.clip);
 	wlr_region_transform(&damage, &damage, fx_options->tex_options.base.transform,
-			monitor_box.width, monitor_box.height);
+			buffer_bounds.width, buffer_bounds.height);
 
-	wlr_region_expand(&damage, &damage, blur_data_calc_size(fx_options->blur_data));
+	wlr_region_expand(&damage, &damage, blur_data_calc_size(&blur_data));
+	// Make sure that the region doesn't expand past the buffer bounds
+	pixman_region32_intersect_rect(&damage, &damage,
+			0, 0, buffer_bounds.width, buffer_bounds.height);
 
 	// damage region will be scaled, make a temp
 	pixman_region32_t scaled_damage;
 	pixman_region32_init(&scaled_damage);
 
 	fx_options->tex_options.base.src_box = (struct wlr_fbox) {
-		monitor_box.x,
-		monitor_box.y,
-		monitor_box.width,
-		monitor_box.height,
+		0, 0,
+		buffer_bounds.width, buffer_bounds.height,
 	};
-	fx_options->tex_options.base.dst_box = monitor_box;
+	fx_options->tex_options.base.dst_box = buffer_bounds;
 	// Clip the blur to the damage
 	fx_options->tex_options.base.clip = &scaled_damage;
 	// Artifacts with NEAREST filter
 	fx_options->tex_options.base.filter_mode = WLR_SCALE_FILTER_BILINEAR;
 
+	TRACY_BOTH_ZONES_START(renderer);
+	TRACY_ZONE_TEXT_f("dst_box (WxH, X, Y): %dx%d, %d, %d",
+			fx_options->tex_options.base.dst_box.width,
+			fx_options->tex_options.base.dst_box.height,
+			fx_options->tex_options.base.dst_box.x,
+			fx_options->tex_options.base.dst_box.y);
+	TRACY_ZONE_TEXT_f("clip_box (WxH, X, Y): %dx%d, %d, %d",
+			fx_options->tex_options.clip_box->width,
+			fx_options->tex_options.clip_box->height,
+			fx_options->tex_options.clip_box->x,
+			fx_options->tex_options.clip_box->y);
+	TRACY_ZONE_TEXT_f("Corners (TL, TR, BL, BR): %f, %f, %f, %f",
+			fx_options->corners.top_left,
+			fx_options->corners.top_right,
+			fx_options->corners.bottom_left,
+			fx_options->corners.bottom_right);
+	TRACY_ZONE_TEXT_f("src_box (WxH, X, Y): %lfx%lf, %lf, %lf",
+			fx_options->tex_options.base.src_box.width,
+			fx_options->tex_options.base.src_box.height,
+			fx_options->tex_options.base.src_box.x,
+			fx_options->tex_options.base.src_box.y);
+	TRACY_ZONE_TEXT_f("Ignore Transparent: %d", fx_options->ignore_transparent);
+	TRACY_ZONE_TEXT_f("Discard Transparent: %d", fx_options->tex_options.discard_transparent);
+	TRACY_ZONE_TEXT_f("Use Optimized Blur: %d", fx_options->use_optimized_blur);
+	TRACY_ZONE_TEXT_f("Blur Options:");
+	TRACY_ZONE_TEXT_f("\tNum Blur Passes: %d", fx_options->blur_data->num_passes);
+	TRACY_ZONE_TEXT_f("\tBlur Radius: %f", fx_options->blur_data->radius);
+	TRACY_ZONE_TEXT_f("\tBrightness: %f", fx_options->blur_data->brightness);
+	TRACY_ZONE_TEXT_f("\tContrast: %f", fx_options->blur_data->contrast);
+	TRACY_ZONE_TEXT_f("\tNoise: %f", fx_options->blur_data->noise);
+	TRACY_ZONE_TEXT_f("\tSaturation: %f", fx_options->blur_data->saturation);
+	push_fx_debug(renderer);
+
 	// Downscale
-	for (int i = 0; i < blur_data->num_passes; ++i) {
+	for (int i = 0; i < blur_data.num_passes; ++i) {
 		wlr_region_scale(&scaled_damage, &damage, 1.0f / (1 << (i + 1)));
 		render_blur_segments(pass, fx_options, &renderer->shaders.blur1);
 	}
 
 	// Upscale
-	for (int i = blur_data->num_passes - 1; i >= 0; --i) {
+	for (int i = blur_data.num_passes - 1; i >= 0; --i) {
 		// when upsampling we make the region twice as big
 		wlr_region_scale(&scaled_damage, &damage, 1.0f / (1 << i));
 		render_blur_segments(pass, fx_options, &renderer->shaders.blur2);
@@ -876,21 +1070,21 @@ static struct fx_framebuffer *get_main_buffer_blur(struct fx_gles_render_pass *p
 	pixman_region32_fini(&scaled_damage);
 
 	// Render additional blur effects like saturation, noise, contrast, etc...
-	if (blur_data_should_parameters_blur_effects(blur_data)
+	if (blur_data_should_parameters_blur_effects(&blur_data)
 			&& pixman_region32_not_empty(&damage)) {
-		if (fx_options->current_buffer == pass->fx_effect_framebuffers->effects_buffer) {
-			fx_framebuffer_bind(pass->fx_effect_framebuffers->effects_buffer_swapped);
+		if (fx_options->current_buffer == pass->fx_offscreen_buffers->effects_buffer) {
+			fx_framebuffer_bind(pass->fx_offscreen_buffers->effects_buffer_swapped);
 		} else {
-			fx_framebuffer_bind(pass->fx_effect_framebuffers->effects_buffer);
+			fx_framebuffer_bind(pass->fx_offscreen_buffers->effects_buffer);
 		}
 		fx_options->tex_options.base.clip = &damage;
 		fx_options->tex_options.base.texture = fx_texture_from_buffer(
 				&renderer->wlr_renderer, fx_options->current_buffer->buffer);
 		render_blur_effects(pass, fx_options);
-		if (fx_options->current_buffer != pass->fx_effect_framebuffers->effects_buffer) {
-			fx_options->current_buffer = pass->fx_effect_framebuffers->effects_buffer;
+		if (fx_options->current_buffer != pass->fx_offscreen_buffers->effects_buffer) {
+			fx_options->current_buffer = pass->fx_offscreen_buffers->effects_buffer;
 		} else {
-			fx_options->current_buffer = pass->fx_effect_framebuffers->effects_buffer_swapped;
+			fx_options->current_buffer = pass->fx_offscreen_buffers->effects_buffer_swapped;
 		}
 	}
 
@@ -899,52 +1093,48 @@ static struct fx_framebuffer *get_main_buffer_blur(struct fx_gles_render_pass *p
 	// Bind back to the default buffer
 	fx_framebuffer_bind(pass->buffer);
 
+	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
+
 	return fx_options->current_buffer;
 }
 
 void fx_render_pass_add_blur(struct fx_gles_render_pass *pass,
 		struct fx_render_blur_pass_options *fx_options) {
-	if (pass->buffer->renderer->basic_renderer) {
-		wlr_log(WLR_ERROR, "Please use 'fx_renderer_begin_buffer_pass' instead of "
-				"'wlr_renderer_begin_buffer_pass' to use advanced effects");
+	if (pass->fx_offscreen_buffers == NULL) {
+		wlr_log(WLR_ERROR, "FX Pass offscreen buffers not initialized. Skipping blur...");
 		return;
 	}
+
 	struct fx_renderer *renderer = pass->buffer->renderer;
 	struct fx_render_texture_options *tex_options = &fx_options->tex_options;
-	const struct wlr_render_texture_options *options = &tex_options->base;
-
-	pixman_region32_t translucent_region;
-	pixman_region32_init(&translucent_region);
-
-	struct wlr_box dst_box;
-	wlr_render_texture_options_get_dst_box(options, &dst_box);
-
-	// Gets the translucent region
-	pixman_box32_t surface_box = { 0, 0, dst_box.width, dst_box.height };
-	pixman_region32_copy(&translucent_region, fx_options->opaque_region);
-	pixman_region32_inverse(&translucent_region, &translucent_region, &surface_box);
-	if (!pixman_region32_not_empty(&translucent_region)) {
-		goto damage_finish;
-	}
 
-	struct fx_framebuffer *buffer = pass->fx_effect_framebuffers->optimized_blur_buffer;
-	if (!buffer || !fx_options->use_optimized_blur) {
-		if (!buffer) {
-			wlr_log(WLR_ERROR, "Warning: Failed to use optimized blur");
-		}
-		pixman_region32_translate(&translucent_region, dst_box.x, dst_box.y);
-		pixman_region32_intersect(&translucent_region, &translucent_region, options->clip);
+	TRACY_BOTH_ZONES_START(renderer);
+	push_fx_debug(renderer);
 
+	const bool has_strength = fx_options->blur_strength < 1.0;
+	struct fx_framebuffer *buffer = pass->fx_offscreen_buffers->optimized_blur_buffer;
+	TRACY_ZONE_TEXT_f("Use Optimized Blur: %d", fx_options->use_optimized_blur);
+	TRACY_ZONE_TEXT_f("Optimized Blur Successfully Used: %d",
+			buffer && fx_options->use_optimized_blur);
+	if (!fx_options->use_optimized_blur || has_strength) {
 		// Render the blur into its own buffer
 		struct fx_render_blur_pass_options blur_options = *fx_options;
-		blur_options.tex_options.base.clip = &translucent_region;
-		blur_options.current_buffer = pass->buffer;
+		if (fx_options->use_optimized_blur && has_strength) {
+			// Re-blur the saved non-blurred version of the optimized blur.
+			// Isn't as efficient as just using the optimized blur buffer
+			blur_options.current_buffer = pass->fx_offscreen_buffers->optimized_no_blur_buffer;
+		} else {
+			blur_options.current_buffer = pass->buffer;
+		}
 		buffer = get_main_buffer_blur(pass, &blur_options);
 	}
+	if (!buffer) {
+		goto finish;
+	}
 	struct wlr_texture *wlr_texture =
 		fx_texture_from_buffer(&renderer->wlr_renderer, buffer->buffer);
 	struct fx_texture *blur_texture = fx_get_texture(wlr_texture);
-	blur_texture->has_alpha = true;
 
 	// Get a stencil of the window ignoring transparent regions
 	if (fx_options->ignore_transparent && fx_options->tex_options.base.texture) {
@@ -952,13 +1142,19 @@ void fx_render_pass_add_blur(struct fx_gles_render_pass *pass,
 
 		struct fx_render_texture_options tex_options = fx_options->tex_options;
 		tex_options.discard_transparent = true;
+		tex_options.clipped_region = fx_options->clipped_region;
 		fx_render_pass_add_texture(pass, &tex_options);
 
 		stencil_mask_close(true);
 	}
 
 	// Draw the blurred texture
-	tex_options->base.dst_box = get_monitor_box(pass->output);
+	tex_options->base.dst_box = (struct wlr_box) {
+		.x = 0,
+		.y = 0,
+		.width = buffer->buffer->width,
+		.height = buffer->buffer->height,
+	};
 	tex_options->base.src_box = (struct wlr_fbox) {
 		.x = 0,
 		.y = 0,
@@ -966,6 +1162,9 @@ void fx_render_pass_add_blur(struct fx_gles_render_pass *pass,
 		.height = buffer->buffer->height,
 	};
 	tex_options->base.texture = &blur_texture->wlr_texture;
+	// since we're capturing from the fbo, transform will always be normal
+	tex_options->base.transform = WL_OUTPUT_TRANSFORM_NORMAL;
+	tex_options->clipped_region = fx_options->clipped_region;
 	fx_render_pass_add_texture(pass, tex_options);
 
 	wlr_texture_destroy(&blur_texture->wlr_texture);
@@ -975,20 +1174,48 @@ void fx_render_pass_add_blur(struct fx_gles_render_pass *pass,
 		stencil_mask_fini();
 	}
 
-damage_finish:
-	pixman_region32_fini(&translucent_region);
+finish:
+	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
 }
 
 bool fx_render_pass_add_optimized_blur(struct fx_gles_render_pass *pass,
 		struct fx_render_blur_pass_options *fx_options) {
-	if (pass->buffer->renderer->basic_renderer) {
-		wlr_log(WLR_ERROR, "Please use 'fx_renderer_begin_buffer_pass' instead of "
-				"'wlr_renderer_begin_buffer_pass' to use advanced effects");
+	if (pass->fx_offscreen_buffers == NULL) {
+		wlr_log(WLR_ERROR, "FX Pass offscreen buffers not initialized. Skipping optimized blur...");
 		return false;
 	}
 	struct fx_renderer *renderer = pass->buffer->renderer;
 	struct wlr_box dst_box = fx_options->tex_options.base.dst_box;
 
+	TRACY_BOTH_ZONES_START(renderer);
+	TRACY_ZONE_TEXT_f("dst_box (WxH, X, Y): %dx%d, %d, %d",
+			fx_options->tex_options.base.dst_box.width,
+			fx_options->tex_options.base.dst_box.height,
+			fx_options->tex_options.base.dst_box.x,
+			fx_options->tex_options.base.dst_box.y);
+	TRACY_ZONE_TEXT_f("clip_box (WxH, X, Y): %dx%d, %d, %d",
+			fx_options->tex_options.clip_box->width,
+			fx_options->tex_options.clip_box->height,
+			fx_options->tex_options.clip_box->x,
+			fx_options->tex_options.clip_box->y);
+	TRACY_ZONE_TEXT_f("src_box (WxH, X, Y): %lfx%lf, %lf, %lf",
+			fx_options->tex_options.base.src_box.width,
+			fx_options->tex_options.base.src_box.height,
+			fx_options->tex_options.base.src_box.x,
+			fx_options->tex_options.base.src_box.y);
+	TRACY_ZONE_TEXT_f("Ignore Transparent: %d", fx_options->ignore_transparent);
+	TRACY_ZONE_TEXT_f("Discard Transparent: %d", fx_options->tex_options.discard_transparent);
+	TRACY_ZONE_TEXT_f("Use Optimized Blur: %d", fx_options->use_optimized_blur);
+	TRACY_ZONE_TEXT_f("Blur Options:");
+	TRACY_ZONE_TEXT_f("\tNum Blur Passes: %d", fx_options->blur_data->num_passes);
+	TRACY_ZONE_TEXT_f("\tBlur Radius: %f", fx_options->blur_data->radius);
+	TRACY_ZONE_TEXT_f("\tBrightness: %f", fx_options->blur_data->brightness);
+	TRACY_ZONE_TEXT_f("\tContrast: %f", fx_options->blur_data->contrast);
+	TRACY_ZONE_TEXT_f("\tNoise: %f", fx_options->blur_data->noise);
+	TRACY_ZONE_TEXT_f("\tSaturation: %f", fx_options->blur_data->saturation);
+	push_fx_debug(renderer);
+
 	pixman_region32_t clip;
 	pixman_region32_init_rect(&clip,
 			dst_box.x, dst_box.y, dst_box.width, dst_box.height);
@@ -997,32 +1224,31 @@ bool fx_render_pass_add_optimized_blur(struct fx_gles_render_pass *pass,
 	struct fx_render_blur_pass_options blur_options = *fx_options;
 	blur_options.current_buffer = pass->buffer;
 	blur_options.tex_options.base.clip = &clip;
-	struct fx_framebuffer *buffer = get_main_buffer_blur(pass, &blur_options);
-
-	// Update the optimized blur buffer if invalid
-	bool failed = false;
-	fx_framebuffer_get_or_create_custom(renderer, pass->output, NULL,
-			&pass->fx_effect_framebuffers->optimized_blur_buffer, &failed);
-	if (failed) {
-		goto finish;
+	struct fx_framebuffer *fx_buffer = get_main_buffer_blur(pass, &blur_options);
+	if (fx_buffer != NULL) {
+		// Render the newly blurred content into the blur_buffer
+		fx_render_pass_read_to_buffer(pass, &clip,
+				pass->fx_offscreen_buffers->optimized_blur_buffer, fx_buffer);
+
+		// Save the current scene pass state
+		fx_render_pass_read_to_buffer(pass, &clip,
+				pass->fx_offscreen_buffers->optimized_no_blur_buffer, pass->buffer);
 	}
 
-	// Render the newly blurred content into the blur_buffer
-	fx_renderer_read_to_buffer(pass, &clip,
-			pass->fx_effect_framebuffers->optimized_blur_buffer, buffer);
-
-finish:
 	pixman_region32_fini(&clip);
-	return !failed;
+
+	pop_fx_debug(renderer);
+	TRACY_BOTH_ZONES_END;
+	return fx_buffer != NULL;
 }
 
-// TODO: Use blitting for glesv3
-void fx_renderer_read_to_buffer(struct fx_gles_render_pass *pass,
+void fx_render_pass_read_to_buffer(struct fx_gles_render_pass *pass,
 		pixman_region32_t *_region, struct fx_framebuffer *dst_buffer,
 		struct fx_framebuffer *src_buffer) {
 	if (!_region || !pixman_region32_not_empty(_region)) {
 		return;
 	}
+	TRACY_BOTH_ZONES_START(pass->buffer->renderer);
 
 	pixman_region32_t region;
 	pixman_region32_init(&region);
@@ -1031,8 +1257,7 @@ void fx_renderer_read_to_buffer(struct fx_gles_render_pass *pass,
 	struct wlr_texture *src_tex =
 		fx_texture_from_buffer(&pass->buffer->renderer->wlr_renderer, src_buffer->buffer);
 	if (src_tex == NULL) {
-		pixman_region32_fini(&region);
-		return;
+		goto done;
 	}
 
 	// Draw onto the dst_buffer
@@ -1043,6 +1268,8 @@ void fx_renderer_read_to_buffer(struct fx_gles_render_pass *pass,
 		.transform = WL_OUTPUT_TRANSFORM_NORMAL,
 		.blend_mode = WLR_RENDER_BLEND_MODE_NONE,
 		.dst_box = (struct wlr_box){
+			.x = 0,
+			.y = 0,
 			.width = dst_buffer->buffer->width,
 			.height = dst_buffer->buffer->height,
 		},
@@ -1058,10 +1285,12 @@ void fx_renderer_read_to_buffer(struct fx_gles_render_pass *pass,
 	// Bind back to the main WLR buffer
 	fx_framebuffer_bind(pass->buffer);
 
+done:
+	TRACY_BOTH_ZONES_END;
+
 	pixman_region32_fini(&region);
 }
 
-
 static const char *reset_status_str(GLenum status) {
 	switch (status) {
 	case GL_GUILTY_CONTEXT_RESET_KHR:
@@ -1075,7 +1304,7 @@ static const char *reset_status_str(GLenum status) {
 	}
 }
 
-static struct fx_gles_render_pass *begin_buffer_pass(struct fx_framebuffer *buffer,
+struct fx_gles_render_pass *fx_begin_buffer_pass(struct fx_framebuffer *buffer,
 		struct wlr_egl_context *prev_ctx, struct fx_render_timer *timer,
 		struct wlr_drm_syncobj_timeline *signal_timeline, uint64_t signal_point) {
 	struct fx_renderer *renderer = buffer->renderer;
@@ -1110,6 +1339,10 @@ static struct fx_gles_render_pass *begin_buffer_pass(struct fx_framebuffer *buff
 		pass->signal_point = signal_point;
 	}
 
+	pass->fx_offscreen_buffers = NULL;
+	pixman_region32_init(&pass->blur_padding_region);
+	pass->has_blur = false;
+
 	matrix_projection(pass->projection_matrix, wlr_buffer->width, wlr_buffer->height,
 		WL_OUTPUT_TRANSFORM_FLIPPED_180);
 
@@ -1119,66 +1352,7 @@ static struct fx_gles_render_pass *begin_buffer_pass(struct fx_framebuffer *buff
 	glViewport(0, 0, wlr_buffer->width, wlr_buffer->height);
 	glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
 	glDisable(GL_SCISSOR_TEST);
-	pop_fx_debug(renderer);
 
-	return pass;
-}
-
-struct fx_gles_render_pass *fx_renderer_begin_buffer_pass(
-		struct wlr_renderer *wlr_renderer, struct wlr_buffer *wlr_buffer,
-		struct wlr_output *output, const struct fx_buffer_pass_options *fx_options) {
-	const struct wlr_buffer_pass_options *options = fx_options->base;
-	struct fx_renderer *renderer = fx_get_renderer(wlr_renderer);
-
-	renderer->basic_renderer = (output == NULL);
-
-	struct wlr_egl_context prev_ctx = {0};
-	if (!wlr_egl_make_current(renderer->egl, &prev_ctx)) {
-		return NULL;
-	}
-
-	struct fx_render_timer *timer = NULL;
-	if (options->timer) {
-		timer = fx_get_render_timer(options->timer);
-		clock_gettime(CLOCK_MONOTONIC, &timer->cpu_start);
-	}
-
-	struct fx_framebuffer *buffer = fx_framebuffer_get_or_create(renderer, wlr_buffer);
-	if (!buffer) {
-		return NULL;
-	}
-
-	// For per output framebuffers
-	struct fx_effect_framebuffers *fbos = NULL;
-	// Update the buffers if needed
-	if (!renderer->basic_renderer) {
-		bool failed = false;
-
-		fbos = fx_effect_framebuffers_try_get(output);
-		failed |= fbos == NULL;
-		if (fbos) {
-			pixman_region32_init(&fbos->blur_padding_region);
-
-			fx_framebuffer_get_or_create_custom(renderer, output, fx_options->swapchain,
-					&fbos->blur_saved_pixels_buffer, &failed);
-			fx_framebuffer_get_or_create_custom(renderer, output, fx_options->swapchain,
-					&fbos->effects_buffer, &failed);
-			fx_framebuffer_get_or_create_custom(renderer, output, fx_options->swapchain,
-					&fbos->effects_buffer_swapped, &failed);
-		}
-
-		if (failed) {
-			// Revert back to not running advanced effects
-			renderer->basic_renderer = true;
-		}
-	}
-
-	struct fx_gles_render_pass *pass = begin_buffer_pass(buffer,
-			&prev_ctx, timer, options->signal_timeline, options->signal_point);
-	if (!pass) {
-		return NULL;
-	}
-	pass->fx_effect_framebuffers = fbos;
-	pass->output = output;
+	pop_fx_debug(renderer);
 	return pass;
 }
diff --git a/scenefx/render/fx_renderer/fx_renderer.c b/scenefx/render/fx_renderer/fx_renderer.c
index ff42020..8d0a7b2 100644
--- a/scenefx/render/fx_renderer/fx_renderer.c
+++ b/scenefx/render/fx_renderer/fx_renderer.c
@@ -21,6 +21,9 @@
 #include "render/fx_renderer/shaders.h"
 #include "render/fx_renderer/fx_renderer.h"
 #include "render/fx_renderer/util.h"
+#include "render/pass.h"
+#include "render/tracy.h"
+#include "scenefx/render/fx_renderer/fx_offscreen_buffers.h"
 #include "scenefx/render/fx_renderer/fx_renderer.h"
 #include "scenefx/render/pass.h"
 #include "util/time.h"
@@ -78,9 +81,31 @@ static int fx_get_drm_fd(struct wlr_renderer *wlr_renderer) {
 	return renderer->drm_fd;
 }
 
+static inline void free_shaders(struct fx_renderer *renderer) {
+	push_fx_debug(renderer);
+	glDeleteProgram(renderer->shaders.quad.program);
+	glDeleteProgram(renderer->shaders.quad_clip.program);
+	glDeleteProgram(renderer->shaders.quad_round.program);
+	glDeleteProgram(renderer->shaders.quad_grad.program);
+	glDeleteProgram(renderer->shaders.quad_grad_round.program);
+	glDeleteProgram(renderer->shaders.tex_rgba.program);
+	glDeleteProgram(renderer->shaders.tex_rgbx.program);
+	glDeleteProgram(renderer->shaders.tex_ext.program);
+	glDeleteProgram(renderer->shaders.tex_effects_rgba.program);
+	glDeleteProgram(renderer->shaders.tex_effects_rgbx.program);
+	glDeleteProgram(renderer->shaders.tex_effects_ext.program);
+	glDeleteProgram(renderer->shaders.box_shadow.program);
+	glDeleteProgram(renderer->shaders.blur1.program);
+	glDeleteProgram(renderer->shaders.blur2.program);
+	glDeleteProgram(renderer->shaders.blur_effects.program);
+	pop_fx_debug(renderer);
+}
+
 static void fx_renderer_destroy(struct wlr_renderer *wlr_renderer) {
 	struct fx_renderer *renderer = fx_get_renderer(wlr_renderer);
 
+	TRACY_GPU_CONTEXT_DESTROY(renderer->tracy_data);
+
 	wlr_egl_make_current(renderer->egl, NULL);
 
 	struct fx_texture *tex, *tex_tmp;
@@ -88,17 +113,17 @@ static void fx_renderer_destroy(struct wlr_renderer *wlr_renderer) {
 		fx_texture_destroy(tex);
 	}
 
+	struct fx_offscreen_buffers *fbos, *fbos_tmp;
+	wl_list_for_each_safe(fbos, fbos_tmp, &renderer->offscreen_buffers, link) {
+		fx_offscreen_buffers_destroy(fbos);
+	}
+
 	struct fx_framebuffer *buffer, *buffer_tmp;
 	wl_list_for_each_safe(buffer, buffer_tmp, &renderer->buffers, link) {
 		fx_framebuffer_destroy(buffer);
 	}
 
-	push_fx_debug(renderer);
-	glDeleteProgram(renderer->shaders.quad.program);
-	glDeleteProgram(renderer->shaders.tex_rgba.program);
-	glDeleteProgram(renderer->shaders.tex_rgbx.program);
-	glDeleteProgram(renderer->shaders.tex_ext.program);
-	pop_fx_debug(renderer);
+	free_shaders(renderer);
 
 	if (renderer->exts.KHR_debug) {
 		glDisable(GL_DEBUG_OUTPUT_KHR);
@@ -119,14 +144,36 @@ static void fx_renderer_destroy(struct wlr_renderer *wlr_renderer) {
 
 static struct wlr_render_pass *begin_buffer_pass(struct wlr_renderer *wlr_renderer,
 		struct wlr_buffer *wlr_buffer, const struct wlr_buffer_pass_options *options) {
-	struct fx_gles_render_pass *pass =
-		fx_renderer_begin_buffer_pass(wlr_renderer, wlr_buffer, NULL, &(struct fx_buffer_pass_options) {
-					.base = options,
-					.swapchain = NULL,
-				});
+	struct fx_renderer *renderer = fx_get_renderer(wlr_renderer);
+
+	struct wlr_egl_context prev_ctx = {0};
+	if (!wlr_egl_make_current(renderer->egl, &prev_ctx)) {
+		return NULL;
+	}
+
+	TRACY_BOTH_ZONES_START(renderer);
+
+	struct fx_render_timer *timer = NULL;
+	if (options->timer) {
+		timer = fx_get_render_timer(options->timer);
+		clock_gettime(CLOCK_MONOTONIC, &timer->cpu_start);
+	}
+
+	struct fx_framebuffer *buffer = fx_framebuffer_get_or_create(renderer, wlr_buffer);
+	if (!buffer) {
+		TRACY_BOTH_ZONES_END_FAIL;
+		return NULL;
+	}
+
+	struct fx_gles_render_pass *pass = fx_begin_buffer_pass(buffer,
+			&prev_ctx, timer, options->signal_timeline, options->signal_point);
 	if (!pass) {
+		TRACY_BOTH_ZONES_END_FAIL;
 		return NULL;
 	}
+
+	TRACY_BOTH_ZONES_END;
+
 	return &pass->base;
 }
 
@@ -308,62 +355,84 @@ struct wlr_renderer *fx_renderer_create(struct wlr_backend *backend) {
 }
 
 static bool link_shaders(struct fx_renderer *renderer) {
-	EGLint client_version;
-	eglQueryContext(renderer->egl->display, renderer->egl->context,
-		EGL_CONTEXT_CLIENT_VERSION, &client_version);
-
 	// quad fragment shader
-	if (!link_quad_program(&renderer->shaders.quad, (GLint) client_version)) {
+	if (!link_quad_program(&renderer->shaders.quad, false)) {
 		wlr_log(WLR_ERROR, "Could not link quad shader");
 		goto error;
 	}
 
+	// quad clip fragment shader
+	if (!link_quad_program(&renderer->shaders.quad_clip, true)) {
+		wlr_log(WLR_ERROR, "Could not link quad clip shader");
+		goto error;
+	}
+
 	// quad fragment shader with gradients
-	if (!link_quad_grad_program(&renderer->shaders.quad_grad, (GLint) client_version, 16)) {
+	if (!link_quad_grad_program(&renderer->shaders.quad_grad, 16)) {
 		wlr_log(WLR_ERROR, "Could not link quad grad shader");
 		goto error;
 	}
 
-	if (!link_quad_grad_round_program(&renderer->shaders.quad_grad_round, (GLint) client_version, 16)) {
+	if (!link_quad_grad_round_program(&renderer->shaders.quad_grad_round, 16)) {
 		wlr_log(WLR_ERROR, "Could not link quad grad round shader");
 		goto error;
 	}
 
-	if (!link_quad_round_program(&renderer->shaders.quad_round, (GLint) client_version)) {
+	if (!link_quad_round_program(&renderer->shaders.quad_round)) {
 		wlr_log(WLR_ERROR, "Could not link quad round shader");
 		goto error;
 	}
 
-	// fragment shaders
-	if (!link_tex_program(&renderer->shaders.tex_rgba, (GLint) client_version, SHADER_SOURCE_TEXTURE_RGBA)) {
+	// Basic fragment shaders
+	if (!link_tex_program(&renderer->shaders.tex_rgba,
+				SHADER_SOURCE_TEXTURE_RGBA, false)) {
 		wlr_log(WLR_ERROR, "Could not link tex_RGBA shader");
 		goto error;
 	}
-	if (!link_tex_program(&renderer->shaders.tex_rgbx, (GLint) client_version, SHADER_SOURCE_TEXTURE_RGBX)) {
+	if (!link_tex_program(&renderer->shaders.tex_rgbx,
+				SHADER_SOURCE_TEXTURE_RGBX, false)) {
 		wlr_log(WLR_ERROR, "Could not link tex_RGBX shader");
 		goto error;
 	}
-	if (!link_tex_program(&renderer->shaders.tex_ext, (GLint) client_version, SHADER_SOURCE_TEXTURE_EXTERNAL)) {
+	if (!link_tex_program(&renderer->shaders.tex_ext,
+				SHADER_SOURCE_TEXTURE_EXTERNAL, false)) {
 		wlr_log(WLR_ERROR, "Could not link tex_EXTERNAL shader");
 		goto error;
 	}
 
+	// Effects fragment shaders
+	if (!link_tex_program(&renderer->shaders.tex_effects_rgba,
+				SHADER_SOURCE_TEXTURE_RGBA, true)) {
+		wlr_log(WLR_ERROR, "Could not link tex_effects_RGBA shader");
+		goto error;
+	}
+	if (!link_tex_program(&renderer->shaders.tex_effects_rgbx,
+				SHADER_SOURCE_TEXTURE_RGBX, true)) {
+		wlr_log(WLR_ERROR, "Could not link tex_effects_RGBX shader");
+		goto error;
+	}
+	if (!link_tex_program(&renderer->shaders.tex_effects_ext,
+				SHADER_SOURCE_TEXTURE_EXTERNAL, true)) {
+		wlr_log(WLR_ERROR, "Could not link tex_effects_EXTERNAL shader");
+		goto error;
+	}
+
 	// box shadow shader
-	if (!link_box_shadow_program(&renderer->shaders.box_shadow, (GLint) client_version)) {
+	if (!link_box_shadow_program(&renderer->shaders.box_shadow)) {
 		wlr_log(WLR_ERROR, "Could not link box shadow shader");
 		goto error;
 	}
 
 	// Blur shaders
-	if (!link_blur1_program(&renderer->shaders.blur1, (GLint) client_version)) {
+	if (!link_blur1_program(&renderer->shaders.blur1)) {
 		wlr_log(WLR_ERROR, "Could not link blur1 shader");
 		goto error;
 	}
-	if (!link_blur2_program(&renderer->shaders.blur2, (GLint) client_version)) {
+	if (!link_blur2_program(&renderer->shaders.blur2)) {
 		wlr_log(WLR_ERROR, "Could not link blur2 shader");
 		goto error;
 	}
-	if (!link_blur_effects_program(&renderer->shaders.blur_effects, (GLint) client_version)) {
+	if (!link_blur_effects_program(&renderer->shaders.blur_effects)) {
 		wlr_log(WLR_ERROR, "Could not link blur_effects shader");
 		goto error;
 	}
@@ -371,18 +440,7 @@ static bool link_shaders(struct fx_renderer *renderer) {
 	return true;
 
 error:
-	glDeleteProgram(renderer->shaders.quad.program);
-	glDeleteProgram(renderer->shaders.quad_round.program);
-	glDeleteProgram(renderer->shaders.quad_grad.program);
-	glDeleteProgram(renderer->shaders.quad_grad_round.program);
-	glDeleteProgram(renderer->shaders.tex_rgba.program);
-	glDeleteProgram(renderer->shaders.tex_rgbx.program);
-	glDeleteProgram(renderer->shaders.tex_ext.program);
-	glDeleteProgram(renderer->shaders.box_shadow.program);
-	glDeleteProgram(renderer->shaders.blur1.program);
-	glDeleteProgram(renderer->shaders.blur2.program);
-	glDeleteProgram(renderer->shaders.blur_effects.program);
-
+	free_shaders(renderer);
 	return false;
 }
 
@@ -406,6 +464,7 @@ struct wlr_renderer *fx_renderer_create_egl(struct wlr_egl *egl) {
 
 	wl_list_init(&renderer->buffers);
 	wl_list_init(&renderer->textures);
+	wl_list_init(&renderer->offscreen_buffers);
 
 	renderer->egl = egl;
 	renderer->exts_str = exts_str;
@@ -493,6 +552,9 @@ struct wlr_renderer *fx_renderer_create_egl(struct wlr_egl *egl) {
 		} else {
 			load_gl_proc(&renderer->procs.glGetInteger64vEXT, "glGetInteger64v");
 		}
+		TRACY_FN(
+			load_gl_proc(&renderer->procs.glGetQueryivEXT, "glGetQueryivEXT");
+		)
 	}
 
 	if (renderer->exts.KHR_debug) {
@@ -509,6 +571,10 @@ struct wlr_renderer *fx_renderer_create_egl(struct wlr_egl *egl) {
 
 	push_fx_debug(renderer);
 
+	TRACY_FN(
+		renderer->tracy_data = TRACY_GPU_CONTEXT_NEW(renderer);
+	)
+
 	// Link all shaders
 	if (!link_shaders(renderer)) {
 		goto error;
diff --git a/scenefx/render/fx_renderer/gles2/meson.build b/scenefx/render/fx_renderer/gles2/meson.build
deleted file mode 100644
index 70cde2c..0000000
--- a/scenefx/render/fx_renderer/gles2/meson.build
+++ /dev/null
@@ -1,2 +0,0 @@
-subdir('shaders')
-
diff --git a/scenefx/render/fx_renderer/gles2/shaders/corner_alpha.frag b/scenefx/render/fx_renderer/gles2/shaders/corner_alpha.frag
deleted file mode 100644
index e560f6c..0000000
--- a/scenefx/render/fx_renderer/gles2/shaders/corner_alpha.frag
+++ /dev/null
@@ -1,35 +0,0 @@
-float get_dist(vec2 q, float radius) {
-	return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
-}
-
-float corner_alpha(vec2 size, vec2 position, float radius_tl, float radius_tr, float radius_bl, float radius_br) {
-	vec2 relative_pos = (gl_FragCoord.xy - position);
-
-	vec2 top_left = abs(relative_pos - size) - size + radius_tl;
-	vec2 top_right = abs(relative_pos - vec2(0, size.y)) - size + radius_tr;
-	vec2 bottom_left = abs(relative_pos - vec2(size.x, 0)) - size + radius_bl;
-	vec2 bottom_right = abs(relative_pos) - size + radius_br;
-
-	float dist = max(
-		max(get_dist(top_left, radius_tl), get_dist(top_right, radius_tr)),
-		max(get_dist(bottom_left, radius_bl), get_dist(bottom_right, radius_br))
-	);
-
-	return smoothstep(0.0, 1.0, dist);
-}
-
-float corner_dist(vec2 size, vec2 position, float radius_tl, float radius_tr, float radius_bl, float radius_br) {
-	vec2 relative_pos = (gl_FragCoord.xy - position);
-
-	vec2 top_left = abs(relative_pos - size) - size + radius_tl;
-	vec2 top_right = abs(relative_pos - vec2(0, size.y)) - size + radius_tr;
-	vec2 bottom_left = abs(relative_pos - vec2(size.x, 0)) - size + radius_bl;
-	vec2 bottom_right = abs(relative_pos) - size + radius_br;
-
-	float dist = max(
-		max(get_dist(top_left, radius_tl), get_dist(top_right, radius_tr)),
-		max(get_dist(bottom_left, radius_bl), get_dist(bottom_right, radius_br))
-	);
-
-	return dist;
-}
diff --git a/scenefx/render/fx_renderer/gles2/shaders/meson.build b/scenefx/render/fx_renderer/gles2/shaders/meson.build
deleted file mode 100644
index 260e07d..0000000
--- a/scenefx/render/fx_renderer/gles2/shaders/meson.build
+++ /dev/null
@@ -1,30 +0,0 @@
-embed = find_program('./embed.sh', native: true)
-
-shaders = [
-    'common.vert',
-	'gradient.frag',
-    'corner_alpha.frag',
-    'quad.frag',
-    'quad_grad.frag',
-    'quad_round.frag',
-    'quad_grad_round.frag',
-    'tex.frag',
-    'box_shadow.frag',
-    'blur1.frag',
-    'blur2.frag',
-    'blur_effects.frag',
-]
-
-foreach name : shaders
-    output = name.underscorify() + '_gles2_src.h'
-    var = name.underscorify() + '_gles2_src'
-    scenefx_files += custom_target(
-        output,
-        command: [embed, var],
-        input: name,
-        output: output,
-        feed: true,
-        capture: true,
-    )
-endforeach
-
diff --git a/scenefx/render/fx_renderer/gles2/shaders/quad.frag b/scenefx/render/fx_renderer/gles2/shaders/quad.frag
deleted file mode 100644
index 3144f93..0000000
--- a/scenefx/render/fx_renderer/gles2/shaders/quad.frag
+++ /dev/null
@@ -1,31 +0,0 @@
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-
-varying vec4 v_color;
-varying vec2 v_texcoord;
-
-uniform vec2 clip_size;
-uniform vec2 clip_position;
-uniform float clip_radius_top_left;
-uniform float clip_radius_top_right;
-uniform float clip_radius_bottom_left;
-uniform float clip_radius_bottom_right;
-
-float corner_alpha(vec2 size, vec2 position, float radius_tl, float radius_tr, float radius_bl, float radius_br);
-
-void main() {
-    // Clipping
-    float clip_corner_alpha = corner_alpha(
-        clip_size - 1.0,
-        clip_position + 0.5,
-        clip_radius_top_left,
-        clip_radius_top_right,
-        clip_radius_bottom_left,
-        clip_radius_bottom_right
-    );
-
-    gl_FragColor = v_color * clip_corner_alpha;
-}
diff --git a/scenefx/render/fx_renderer/gles2/shaders/quad_round.frag b/scenefx/render/fx_renderer/gles2/shaders/quad_round.frag
deleted file mode 100644
index 7efced1..0000000
--- a/scenefx/render/fx_renderer/gles2/shaders/quad_round.frag
+++ /dev/null
@@ -1,64 +0,0 @@
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-
-varying vec4 v_color;
-varying vec2 v_texcoord;
-
-uniform vec2 size;
-uniform vec2 position;
-uniform float radius_top_left;
-uniform float radius_top_right;
-uniform float radius_bottom_left;
-uniform float radius_bottom_right;
-uniform float fade_inset;
-
-uniform vec2 clip_size;
-uniform vec2 clip_position;
-uniform float clip_radius_top_left;
-uniform float clip_radius_top_right;
-uniform float clip_radius_bottom_left;
-uniform float clip_radius_bottom_right;
-
-float corner_alpha(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
-float corner_dist(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
-
-void main() {
-    float quad_corner_alpha = corner_alpha(
-        size - 1.0,
-        position + 0.5,
-        radius_top_left,
-        radius_top_right,
-        radius_bottom_left,
-        radius_bottom_right
-    );
-
-    // Clipping
-    float clip_corner_alpha = corner_alpha(
-        clip_size - 1.0,
-        clip_position + 0.5,
-        clip_radius_top_left,
-        clip_radius_top_right,
-        clip_radius_bottom_left,
-        clip_radius_bottom_right
-    );
-
-    vec4 final_color = v_color;
-    if (fade_inset > 0.0) {
-        float d = corner_dist(
-            size - 1.0,
-            position + 0.5,
-            radius_top_left,
-            radius_top_right,
-            radius_bottom_left,
-            radius_bottom_right
-        );
-        float inside_dist = -d;
-        float fade_factor = clamp(inside_dist / fade_inset, 0.0, 1.0);
-        final_color *= fade_factor;
-    }
-
-    gl_FragColor = mix(final_color, vec4(0.0), quad_corner_alpha) * clip_corner_alpha;
-}
diff --git a/scenefx/render/fx_renderer/gles3/meson.build b/scenefx/render/fx_renderer/gles3/meson.build
deleted file mode 100644
index 40da7c3..0000000
--- a/scenefx/render/fx_renderer/gles3/meson.build
+++ /dev/null
@@ -1 +0,0 @@
-subdir('shaders')
\ No newline at end of file
diff --git a/scenefx/render/fx_renderer/gles3/shaders/blur1.frag b/scenefx/render/fx_renderer/gles3/shaders/blur1.frag
deleted file mode 100644
index 55504a2..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/blur1.frag
+++ /dev/null
@@ -1,23 +0,0 @@
-#version 300 es
-
-precision mediump float;
-
-in mediump vec2 v_texcoord;
-uniform sampler2D tex;
-
-uniform float radius;
-uniform vec2 halfpixel;
-
-out vec4 fragColor;
-
-void main() {
-    vec2 uv = v_texcoord * 2.0;
-
-    vec4 sum = texture2D(tex, uv) * 4.0;
-    sum += texture2D(tex, uv - halfpixel.xy * radius);
-    sum += texture2D(tex, uv + halfpixel.xy * radius);
-    sum += texture2D(tex, uv + vec2(halfpixel.x, -halfpixel.y) * radius);
-    sum += texture2D(tex, uv - vec2(halfpixel.x, -halfpixel.y) * radius);
-
-    fragColor = sum / 8.0;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/blur2.frag b/scenefx/render/fx_renderer/gles3/shaders/blur2.frag
deleted file mode 100644
index 6fc90b6..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/blur2.frag
+++ /dev/null
@@ -1,27 +0,0 @@
-#version 300 es
-
-precision mediump float;
-
-in mediump vec2 v_texcoord;
-uniform sampler2D tex;
-
-uniform float radius;
-uniform vec2 halfpixel;
-
-out vec4 fragColor;
-
-void main() {
-    vec2 uv = v_texcoord / 2.0;
-
-    vec4 sum = texture2D(tex, uv + vec2(-halfpixel.x * 2.0, 0.0) * radius);
-
-    sum += texture2D(tex, uv + vec2(-halfpixel.x, halfpixel.y) * radius) * 2.0;
-    sum += texture2D(tex, uv + vec2(0.0, halfpixel.y * 2.0) * radius);
-    sum += texture2D(tex, uv + vec2(halfpixel.x, halfpixel.y) * radius) * 2.0;
-    sum += texture2D(tex, uv + vec2(halfpixel.x * 2.0, 0.0) * radius);
-    sum += texture2D(tex, uv + vec2(halfpixel.x, -halfpixel.y) * radius) * 2.0;
-    sum += texture2D(tex, uv + vec2(0.0, -halfpixel.y * 2.0) * radius);
-    sum += texture2D(tex, uv + vec2(-halfpixel.x, -halfpixel.y) * radius) * 2.0;
-
-    fragColor = sum / 12.0;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/blur_effects.frag b/scenefx/render/fx_renderer/gles3/shaders/blur_effects.frag
deleted file mode 100644
index 901f2a8..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/blur_effects.frag
+++ /dev/null
@@ -1,58 +0,0 @@
-#version 300 es
-
-precision highp float;
-
-in vec2 v_texcoord;
-uniform sampler2D tex;
-
-uniform float brightness;
-uniform float contrast;
-uniform float saturation;
-uniform float noise;
-
-out vec4 fragColor;
-
-mat4 brightnessMatrix() {
-	float b = brightness - 1.0;
-	return mat4(1, 0, 0, 0,
-				0, 1, 0, 0,
-				0, 0, 1, 0,
-				b, b, b, 1);
-}
-
-mat4 contrastMatrix() {
-	float t = (1.0 - contrast) / 2.0;
-	return mat4(contrast, 0, 0, 0,
-				0, contrast, 0, 0,
-				0, 0, contrast, 0,
-				t, t, t, 1);
-}
-
-mat4 saturationMatrix() {
-	vec3 luminance = vec3(0.3086, 0.6094, 0.0820) * (1.0 - saturation);
-	vec3 red = vec3(luminance.x);
-	red.x += saturation;
-	vec3 green = vec3(luminance.y);
-	green.y += saturation;
-	vec3 blue = vec3(luminance.z);
-	blue.z += saturation;
-	return mat4(red, 0,
-				green, 0,
-				blue, 0,
-				0, 0, 0, 1);
-}
-
-float noiseAmount(vec2 p) {
-	vec3 p3 = fract(vec3(p.xyx) * 1689.1984);
-	p3 += dot(p3, p3.yzx + 33.33);
-	float hash = fract((p3.x + p3.y) * p3.z);
-	return (mod(hash, 1.0) - 0.5) * noise;
-};
-
-void main() {
-	vec4 color = texture2D(tex, v_texcoord);
-	// Do *not* transpose the combined matrix when multiplying
-	color = brightnessMatrix() * contrastMatrix() * saturationMatrix() * color;
-	color.xyz += noiseAmount(v_texcoord);
-	fragColor = color;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/box_shadow.frag b/scenefx/render/fx_renderer/gles3/shaders/box_shadow.frag
deleted file mode 100644
index 2f6e961..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/box_shadow.frag
+++ /dev/null
@@ -1,96 +0,0 @@
-// Writeup: https://madebyevan.com/shaders/fast-rounded-rectangle-shadows/
-
-#version 300 es
-
-precision highp float;
-
-in vec4 v_color;
-in vec2 v_texcoord;
-
-uniform vec2 position;
-uniform vec2 size;
-uniform float blur_sigma;
-uniform float corner_radius;
-uniform vec2 clip_position;
-uniform vec2 clip_size;
-uniform float clip_radius_top_left;
-uniform float clip_radius_top_right;
-uniform float clip_radius_bottom_left;
-uniform float clip_radius_bottom_right;
-
-out vec4 fragColor;
-
-float gaussian(float x, float sigma) {
-    const float pi = 3.141592653589793;
-    return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma);
-}
-
-// approximates the error function, needed for the gaussian integral
-vec2 erf(vec2 x) {
-    vec2 s = sign(x), a = abs(x);
-    x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
-    x *= x;
-    return s - s / (x * x);
-}
-
-// return the blurred mask along the x dimension
-float roundedBoxShadowX(float x, float y, float sigma, float corner, vec2 halfSize) {
-    float delta = min(halfSize.y - corner - abs(y), 0.0);
-    float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
-    vec2 integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma));
-    return integral.y - integral.x;
-}
-
-// return the mask for the shadow of a box from lower to upper
-float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float corner_radius) {
-    // Center everything to make the math easier
-    vec2 center = (lower + upper) * 0.5;
-    vec2 halfSize = (upper - lower) * 0.5;
-    point -= center;
-
-    // The signal is only non-zero in a limited range, so don't waste samples
-    float low = point.y - halfSize.y;
-    float high = point.y + halfSize.y;
-    float start = clamp(-3.0 * sigma, low, high);
-    float end = clamp(3.0 * sigma, low, high);
-
-    // Accumulate samples (we can get away with surprisingly few samples)
-    float step = (end - start) / 4.0;
-    float y = start + step * 0.5;
-    float value = 0.0;
-    for (int i = 0; i < 4; i++) {
-        value += roundedBoxShadowX(point.x, point.y - y, sigma, corner_radius, halfSize) * gaussian(y, sigma) * step;
-        y += step;
-    }
-
-    return value;
-}
-
-// per-pixel "random" number between 0 and 1
-float random() {
-    return fract(sin(dot(vec2(12.9898, 78.233), gl_FragCoord.xy)) * 43758.5453);
-}
-
-float corner_alpha(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
-
-void main() {
-    float shadow_alpha = v_color.a * roundedBoxShadow(
-            position + blur_sigma,
-            position + size - blur_sigma,
-            gl_FragCoord.xy, blur_sigma * 0.5,
-            corner_radius);
-    // dither the alpha to break up color bands
-    shadow_alpha += (random() - 0.5) / 128.0;
-
-    // Clipping
-    float clip_corner_alpha = corner_alpha(
-        clip_size - 1.5,
-        clip_position + 0.75,
-        clip_radius_top_left,
-        clip_radius_top_right,
-        clip_radius_bottom_left,
-        clip_radius_bottom_right
-    );
-
-    fragColor = vec4(v_color.rgb, shadow_alpha) * clip_corner_alpha;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/common.vert b/scenefx/render/fx_renderer/gles3/shaders/common.vert
deleted file mode 100644
index b420686..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/common.vert
+++ /dev/null
@@ -1,15 +0,0 @@
-#version 300 es
-
-uniform mat3 proj;
-uniform vec4 color;
-uniform mat3 tex_proj;
-in vec2 pos;
-out vec4 v_color;
-out vec2 v_texcoord;
-
-void main() {
-	vec3 pos3 = vec3(pos, 1.0);
-	gl_Position = vec4(pos3 * proj, 1.0);
-	v_color = color;
-	v_texcoord = (pos3 * tex_proj).xy;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/corner_alpha.frag b/scenefx/render/fx_renderer/gles3/shaders/corner_alpha.frag
deleted file mode 100644
index e560f6c..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/corner_alpha.frag
+++ /dev/null
@@ -1,35 +0,0 @@
-float get_dist(vec2 q, float radius) {
-	return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
-}
-
-float corner_alpha(vec2 size, vec2 position, float radius_tl, float radius_tr, float radius_bl, float radius_br) {
-	vec2 relative_pos = (gl_FragCoord.xy - position);
-
-	vec2 top_left = abs(relative_pos - size) - size + radius_tl;
-	vec2 top_right = abs(relative_pos - vec2(0, size.y)) - size + radius_tr;
-	vec2 bottom_left = abs(relative_pos - vec2(size.x, 0)) - size + radius_bl;
-	vec2 bottom_right = abs(relative_pos) - size + radius_br;
-
-	float dist = max(
-		max(get_dist(top_left, radius_tl), get_dist(top_right, radius_tr)),
-		max(get_dist(bottom_left, radius_bl), get_dist(bottom_right, radius_br))
-	);
-
-	return smoothstep(0.0, 1.0, dist);
-}
-
-float corner_dist(vec2 size, vec2 position, float radius_tl, float radius_tr, float radius_bl, float radius_br) {
-	vec2 relative_pos = (gl_FragCoord.xy - position);
-
-	vec2 top_left = abs(relative_pos - size) - size + radius_tl;
-	vec2 top_right = abs(relative_pos - vec2(0, size.y)) - size + radius_tr;
-	vec2 bottom_left = abs(relative_pos - vec2(size.x, 0)) - size + radius_bl;
-	vec2 bottom_right = abs(relative_pos) - size + radius_br;
-
-	float dist = max(
-		max(get_dist(top_left, radius_tl), get_dist(top_right, radius_tr)),
-		max(get_dist(bottom_left, radius_bl), get_dist(bottom_right, radius_br))
-	);
-
-	return dist;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/embed.sh b/scenefx/render/fx_renderer/gles3/shaders/embed.sh
deleted file mode 100644
index 47f0789..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/embed.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh -eu
-
-var=${1:-data}
-hex="$(od -A n -t x1 -v)"
-
-echo "static const char $var[] = {"
-for byte in $hex; do
-    echo "	0x$byte,"
-done
-echo "	0x00,"
-echo "};"
diff --git a/scenefx/render/fx_renderer/gles3/shaders/gradient.frag b/scenefx/render/fx_renderer/gles3/shaders/gradient.frag
deleted file mode 100644
index f5522ac..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/gradient.frag
+++ /dev/null
@@ -1,41 +0,0 @@
-vec4 gradient(vec4 colors[LEN], int count, vec2 size, vec2 grad_box, vec2 origin, float degree, bool linear, bool blend) {
-	float step;
-
-	vec2 normal = (gl_FragCoord.xy - grad_box)/size;
-	vec2 uv = normal - origin;
-
-	float rad = radians(degree);
-
-	if (linear) {
-		uv *= vec2(1.0)/vec2(abs(cos(rad)) + abs(sin(rad)));
-
-		vec2 rotated = vec2(uv.x * cos(rad) - uv.y * sin(rad) + origin.x,
-						uv.x * sin(rad) + uv.y * cos(rad) + origin.y);
-
-		step = rotated.x;
-	} else {
-		vec2 uv = normal - origin;
-		uv = vec2(uv.x * cos(rad) - uv.y * sin(rad),
-				uv.x * sin(rad) + uv.y * cos(rad));
-
-		uv = vec2(-atan(uv.y, uv.x)/3.14159265 * 0.5 + 0.5, 0.0);
-		step = uv.x;
-	}
-
-	if (!blend) {
-		float smooth_fac = 1.0/float(count);
-		int ind = int(step/smooth_fac);
-
-		return colors[ind];
-	}
-
-	float smooth_fac = 1.0/float(count - 1);
-    int ind = int(step/smooth_fac);
-    float at = float(ind)*smooth_fac;
-
-    vec4 color = colors[ind];
-    if(ind > 0) color = mix(colors[ind - 1], color, smoothstep(at - smooth_fac, at, step));
-    if(ind <= count - 1) color = mix(color, colors[ind + 1], smoothstep(at, at + smooth_fac, step));
-
-	return color;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/meson.build b/scenefx/render/fx_renderer/gles3/shaders/meson.build
deleted file mode 100644
index e7cd6fd..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/meson.build
+++ /dev/null
@@ -1,30 +0,0 @@
-embed = find_program('./embed.sh', native: true)
-
-shaders = [
-    'common.vert',
-	'gradient.frag',
-    'corner_alpha.frag',
-    'quad.frag',
-    'quad_grad.frag',
-    'quad_round.frag',
-    'quad_grad_round.frag',
-    'tex.frag',
-    'box_shadow.frag',
-    'blur1.frag',
-    'blur2.frag',
-    'blur_effects.frag',
-]
-
-foreach name : shaders
-    output = name.underscorify() + '_gles3_src.h'
-    var = name.underscorify() + '_gles3_src'
-    scenefx_files += custom_target(
-        output,
-        command: [embed, var],
-        input: name,
-        output: output,
-        feed: true,
-        capture: true,
-    )
-endforeach
-
diff --git a/scenefx/render/fx_renderer/gles3/shaders/quad.frag b/scenefx/render/fx_renderer/gles3/shaders/quad.frag
deleted file mode 100644
index 63eeac4..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/quad.frag
+++ /dev/null
@@ -1,31 +0,0 @@
-#version 300 es
-
-precision highp float;
-
-in vec4 v_color;
-in vec2 v_texcoord;
-
-uniform vec2 clip_size;
-uniform vec2 clip_position;
-uniform float clip_radius_top_left;
-uniform float clip_radius_top_right;
-uniform float clip_radius_bottom_left;
-uniform float clip_radius_bottom_right;
-
-out vec4 fragColor;
-
-float corner_alpha(vec2 size, vec2 position, float radius_tl, float radius_tr, float radius_bl, float radius_br);
-
-void main() {
-    // Clipping
-    float clip_corner_alpha = corner_alpha(
-        clip_size - 1.0,
-        clip_position + 0.5,
-        clip_radius_top_left,
-        clip_radius_top_right,
-        clip_radius_bottom_left,
-        clip_radius_bottom_right
-    );
-
-    fragColor = v_color * clip_corner_alpha;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/quad_grad.frag b/scenefx/render/fx_renderer/gles3/shaders/quad_grad.frag
deleted file mode 100644
index 25bd978..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/quad_grad.frag
+++ /dev/null
@@ -1,25 +0,0 @@
-#version 300 es
-
-#define LEN %d
-
-precision highp float;
-
-in vec4 v_color;
-in vec2 v_texcoord;
-
-uniform vec4 colors[LEN];
-uniform vec2 size;
-uniform float degree;
-uniform vec2 grad_box;
-uniform vec2 origin;
-uniform bool linear;
-uniform bool blend;
-uniform int count;
-
-out vec4 fragColor;
-
-vec4 gradient(vec4 colors[LEN], int count, vec2 size, vec2 grad_box, vec2 origin, float degree, bool linear, bool blend);
-
-void main(){
-	fragColor = gradient(colors, count, size, grad_box, origin, degree, linear, blend);
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/quad_grad_round.frag b/scenefx/render/fx_renderer/gles3/shaders/quad_grad_round.frag
deleted file mode 100644
index 0882cc8..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/quad_grad_round.frag
+++ /dev/null
@@ -1,46 +0,0 @@
-#version 300 es
-
-#define LEN %d
-
-precision highp float;
-
-in vec4 v_color;
-in vec2 v_texcoord;
-
-uniform vec2 size;
-uniform vec2 position;
-
-uniform float radius_top_left;
-uniform float radius_top_right;
-uniform float radius_bottom_left;
-uniform float radius_bottom_right;
-
-uniform vec4 colors[LEN];
-uniform vec2 grad_size;
-uniform float degree;
-uniform vec2 grad_box;
-uniform vec2 origin;
-uniform bool linear;
-uniform bool blend;
-uniform int count;
-
-out vec4 fragColor;
-
-vec4 gradient(vec4 colors[LEN], int count, vec2 size, vec2 grad_box, vec2 origin, float degree, bool linear, bool blend);
-
-float corner_alpha(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
-
-// TODO:
-void main() {
-    float quad_corner_alpha = corner_alpha(
-        size - 1.0,
-        position + 0.5,
-        radius_top_left,
-        radius_top_right,
-        radius_bottom_left,
-        radius_bottom_right
-    );
-    float rect_alpha = v_color.a * quad_corner_alpha;
-
-    fragColor = mix(vec4(0), gradient(colors, count, size, grad_box, origin, degree, linear, blend), rect_alpha);
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/quad_round.frag b/scenefx/render/fx_renderer/gles3/shaders/quad_round.frag
deleted file mode 100644
index aabf7d3..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/quad_round.frag
+++ /dev/null
@@ -1,64 +0,0 @@
-#version 300 es
-
-precision highp float;
-
-in vec4 v_color;
-in vec2 v_texcoord;
-
-uniform vec2 size;
-uniform vec2 position;
-uniform float radius_top_left;
-uniform float radius_top_right;
-uniform float radius_bottom_left;
-uniform float radius_bottom_right;
-uniform float fade_inset;
-
-uniform vec2 clip_size;
-uniform vec2 clip_position;
-uniform float clip_radius_top_left;
-uniform float clip_radius_top_right;
-uniform float clip_radius_bottom_left;
-uniform float clip_radius_bottom_right;
-
-out vec4 fragColor;
-
-float corner_alpha(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
-float corner_dist(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
-
-void main() {
-    float quad_corner_alpha = corner_alpha(
-        size - 1.0,
-        position + 0.5,
-        radius_top_left,
-        radius_top_right,
-        radius_bottom_left,
-        radius_bottom_right
-    );
-
-    // Clipping
-    float clip_corner_alpha = corner_alpha(
-        clip_size - 1.0,
-        clip_position + 0.5,
-        clip_radius_top_left,
-        clip_radius_top_right,
-        clip_radius_bottom_left,
-        clip_radius_bottom_right
-    );
-
-    vec4 final_color = v_color;
-    if (fade_inset > 0.0) {
-        float d = corner_dist(
-            size - 1.0,
-            position + 0.5,
-            radius_top_left,
-            radius_top_right,
-            radius_bottom_left,
-            radius_bottom_right
-        );
-        float inside_dist = -d;
-        float fade_factor = clamp(inside_dist / fade_inset, 0.0, 1.0);
-        final_color *= fade_factor;
-    }
-
-    fragColor = mix(final_color, vec4(0.0), quad_corner_alpha) * clip_corner_alpha;
-}
diff --git a/scenefx/render/fx_renderer/gles3/shaders/tex.frag b/scenefx/render/fx_renderer/gles3/shaders/tex.frag
deleted file mode 100644
index f8f7edf..0000000
--- a/scenefx/render/fx_renderer/gles3/shaders/tex.frag
+++ /dev/null
@@ -1,65 +0,0 @@
-#version 300 es
-
-#define SOURCE %d
-
-#define SOURCE_TEXTURE_RGBA 1
-#define SOURCE_TEXTURE_RGBX 2
-#define SOURCE_TEXTURE_EXTERNAL 3
-
-#if !defined(SOURCE)
-#error "Missing shader preamble"
-#endif
-
-#if SOURCE == SOURCE_TEXTURE_EXTERNAL
-#extension GL_OES_EGL_image_external : require
-#endif
-
-precision highp float;
-
-in vec2 v_texcoord;
-
-#if SOURCE == SOURCE_TEXTURE_EXTERNAL
-uniform samplerExternalOES tex;
-#elif SOURCE == SOURCE_TEXTURE_RGBA || SOURCE == SOURCE_TEXTURE_RGBX
-uniform sampler2D tex;
-#endif
-
-uniform float alpha;
-
-uniform vec2 size;
-uniform vec2 position;
-uniform float radius_top_left;
-uniform float radius_top_right;
-uniform float radius_bottom_left;
-uniform float radius_bottom_right;
-
-uniform bool discard_transparent;
-
-out vec4 fragColor;
-
-vec4 sample_texture() {
-#if SOURCE == SOURCE_TEXTURE_RGBA || SOURCE == SOURCE_TEXTURE_EXTERNAL
-	return texture2D(tex, v_texcoord);
-#elif SOURCE == SOURCE_TEXTURE_RGBX
-	return vec4(texture2D(tex, v_texcoord).rgb, 1.0);
-#endif
-}
-
-float corner_alpha(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
-
-void main() {
-    float corner_alpha = corner_alpha(
-        size - 0.5,
-        position + 0.25,
-        radius_top_left,
-        radius_top_right,
-        radius_bottom_left,
-        radius_bottom_right
-    );
-	fragColor = mix(sample_texture() * alpha, vec4(0.0), corner_alpha);
-
-	if (discard_transparent && fragColor.a == 0.0) {
-		discard;
-		return;
-	}
-}
diff --git a/scenefx/render/fx_renderer/meson.build b/scenefx/render/fx_renderer/meson.build
index 1d584bd..cb42b24 100644
--- a/scenefx/render/fx_renderer/meson.build
+++ b/scenefx/render/fx_renderer/meson.build
@@ -7,11 +7,12 @@ endif
 
 scenefx_files += files(
 	'util.c',
+	'tracy.c',
 	'shaders.c',
 	'pixel_format.c',
 	'fx_pass.c',
 	'fx_framebuffer.c',
-	'fx_effect_framebuffers.c',
+	'fx_offscreen_buffers.c',
 	'fx_texture.c',
 	'fx_renderer.c',
 )
@@ -35,12 +36,12 @@ if 'gles2' in renderers or 'auto' in renderers
 	gbm = dependency('gbm', required: 'gles2' in renderers)
 	glesv2 = dependency('glesv2', required: 'gles2' in renderers)
 
-	glslang = find_program('glslang', 'glslangValidator', native: true, required: false, disabler: true)
-
 	if egl.found() and gbm.found() and glesv2.found()
 		scenefx_deps += [egl, gbm, glesv2]
+		features += {'gles2-renderer': true}
+		internal_features += {'egl': true}
 	endif
-	subdir('gles3')
-	subdir('gles2')
-endif
 
+	subdir('shaders')
+
+endif
diff --git a/scenefx/render/fx_renderer/pixel_format.c b/scenefx/render/fx_renderer/pixel_format.c
index 9fff2ea..4403ddc 100644
--- a/scenefx/render/fx_renderer/pixel_format.c
+++ b/scenefx/render/fx_renderer/pixel_format.c
@@ -71,6 +71,11 @@ static const struct fx_pixel_format formats[] = {
 		.gl_format = GL_RGBA,
 		.gl_type = GL_UNSIGNED_INT_2_10_10_10_REV_EXT,
 	},
+	{
+		.drm_format = DRM_FORMAT_BGR161616F,
+		.gl_format = GL_RGB,
+		.gl_type = GL_HALF_FLOAT_OES,
+	},
 	{
 		.drm_format = DRM_FORMAT_XBGR16161616F,
 		.gl_format = GL_RGBA,
@@ -81,6 +86,12 @@ static const struct fx_pixel_format formats[] = {
 		.gl_format = GL_RGBA,
 		.gl_type = GL_HALF_FLOAT_OES,
 	},
+	{
+		.drm_format = DRM_FORMAT_BGR161616,
+		.gl_internalformat = GL_RGB16_EXT,
+		.gl_format = GL_RGB,
+		.gl_type = GL_UNSIGNED_SHORT,
+	},
 	{
 		.drm_format = DRM_FORMAT_XBGR16161616,
 		.gl_internalformat = GL_RGBA16_EXT,
diff --git a/scenefx/render/fx_renderer/shaders.c b/scenefx/render/fx_renderer/shaders.c
index b740c5f..7727376 100644
--- a/scenefx/render/fx_renderer/shaders.c
+++ b/scenefx/render/fx_renderer/shaders.c
@@ -2,37 +2,24 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <wlr/util/log.h>
+#include <scenefx/types/fx/clipped_region.h>
 
 #include "render/fx_renderer/shaders.h"
 
 // shaders
 #include "GLES2/gl2.h"
-// gles3
-#include "common_vert_gles3_src.h"
-#include "gradient_frag_gles3_src.h"
-#include "corner_alpha_frag_gles3_src.h"
-#include "quad_frag_gles3_src.h"
-#include "quad_grad_frag_gles3_src.h"
-#include "quad_round_frag_gles3_src.h"
-#include "quad_grad_round_frag_gles3_src.h"
-#include "tex_frag_gles3_src.h"
-#include "box_shadow_frag_gles3_src.h"
-#include "blur1_frag_gles3_src.h"
-#include "blur2_frag_gles3_src.h"
-#include "blur_effects_frag_gles3_src.h"
-// gles2
-#include "common_vert_gles2_src.h"
-#include "gradient_frag_gles2_src.h"
-#include "corner_alpha_frag_gles2_src.h"
-#include "quad_frag_gles2_src.h"
-#include "quad_grad_frag_gles2_src.h"
-#include "quad_round_frag_gles2_src.h"
-#include "quad_grad_round_frag_gles2_src.h"
-#include "tex_frag_gles2_src.h"
-#include "box_shadow_frag_gles2_src.h"
-#include "blur1_frag_gles2_src.h"
-#include "blur2_frag_gles2_src.h"
-#include "blur_effects_frag_gles2_src.h"
+#include "common_vert_src.h"
+#include "gradient_frag_src.h"
+#include "corner_alpha_frag_src.h"
+#include "quad_frag_src.h"
+#include "quad_grad_frag_src.h"
+#include "quad_round_frag_src.h"
+#include "quad_grad_round_frag_src.h"
+#include "tex_frag_src.h"
+#include "box_shadow_frag_src.h"
+#include "blur1_frag_src.h"
+#include "blur2_frag_src.h"
+#include "blur_effects_frag_src.h"
 
 GLuint compile_shader(GLuint type, const GLchar *src) {
 	GLuint shader = glCreateShader(type);
@@ -50,9 +37,8 @@ GLuint compile_shader(GLuint type, const GLchar *src) {
 	return shader;
 }
 
-GLuint link_program(const GLchar *frag_src, GLint client_version) {
-	const GLchar *vert_src = client_version > 2 ? common_vert_gles3_src : common_vert_gles2_src;
-	GLuint vert = compile_shader(GL_VERTEX_SHADER, vert_src);
+GLuint link_program(const GLchar *frag_src) {
+	GLuint vert = compile_shader(GL_VERTEX_SHADER, common_vert_src);
 	if (!vert) {
 		goto error;
 	}
@@ -115,20 +101,25 @@ void load_gl_proc(void *proc_ptr, const char *name) {
 	*(void **)proc_ptr = proc;
 }
 
+void uniform_corner_radii_set(const struct shader_corner_radii *uniform,
+		const struct fx_corner_fradii *corners) {
+	glUniform1f(uniform->top_left, corners->top_left);
+	glUniform1f(uniform->top_right, corners->top_right);
+	glUniform1f(uniform->bottom_left, corners->bottom_left);
+	glUniform1f(uniform->bottom_right, corners->bottom_right);
+}
 // Shaders
 
-bool link_quad_program(struct quad_shader *shader, GLint client_version) {
-	GLchar quad_src[4096];
-	if (client_version > 2) {
-		snprintf(quad_src, sizeof(quad_src), "%s\n%s", quad_frag_gles3_src,
-			corner_alpha_frag_gles3_src);
-	} else {
-		snprintf(quad_src, sizeof(quad_src), "%s\n%s", quad_frag_gles2_src,
-			corner_alpha_frag_gles2_src);
-	}
+bool link_quad_program(struct quad_shader *shader, bool clip) {
+	GLchar quad_src_part[2048];
+	GLchar quad_src[8192];
+	snprintf(quad_src_part, sizeof(quad_src_part),
+		quad_frag_src, clip);
+	snprintf(quad_src, sizeof(quad_src),
+		"%s\n%s\n", quad_src_part, clip ? corner_alpha_frag_src : "");
 
 	GLuint prog;
-	shader->program = prog = link_program(quad_src, client_version);
+	shader->program = prog = link_program(quad_src);
 	if (!shader->program) {
 		return false;
 	}
@@ -136,33 +127,30 @@ bool link_quad_program(struct quad_shader *shader, GLint client_version) {
 	shader->proj = glGetUniformLocation(prog, "proj");
 	shader->color = glGetUniformLocation(prog, "color");
 	shader->pos_attrib = glGetAttribLocation(prog, "pos");
-	shader->clip_size = glGetUniformLocation(prog, "clip_size");
-	shader->clip_position = glGetUniformLocation(prog, "clip_position");
-	shader->clip_radius_top_left = glGetUniformLocation(prog, "clip_radius_top_left");
-	shader->clip_radius_top_right = glGetUniformLocation(prog, "clip_radius_top_right");
-	shader->clip_radius_bottom_left = glGetUniformLocation(prog, "clip_radius_bottom_left");
-	shader->clip_radius_bottom_right = glGetUniformLocation(prog, "clip_radius_bottom_right");
+
+	if (!clip) {
+		return true;
+	}
+	shader->effects.clip_size = glGetUniformLocation(prog, "clip_size");
+	shader->effects.clip_position = glGetUniformLocation(prog, "clip_position");
+	shader->effects.clip_radius.top_left = glGetUniformLocation(prog, "clip_radius_top_left");
+	shader->effects.clip_radius.top_right = glGetUniformLocation(prog, "clip_radius_top_right");
+	shader->effects.clip_radius.bottom_left = glGetUniformLocation(prog, "clip_radius_bottom_left");
+	shader->effects.clip_radius.bottom_right = glGetUniformLocation(prog, "clip_radius_bottom_right");
 
 	return true;
 }
 
-bool link_quad_grad_program(struct quad_grad_shader *shader, GLint client_version, int max_len) {
+bool link_quad_grad_program(struct quad_grad_shader *shader, int max_len) {
 	GLchar quad_src_part[2048];
 	GLchar quad_src[4096];
-	if (client_version > 2) {
-		snprintf(quad_src_part, sizeof(quad_src_part),
-			quad_grad_frag_gles3_src, max_len);
-		snprintf(quad_src, sizeof(quad_src),
-			"%s\n%s", quad_src_part, gradient_frag_gles3_src);
-	} else {
-		snprintf(quad_src_part, sizeof(quad_src_part),
-			quad_grad_frag_gles2_src, max_len);
-		snprintf(quad_src, sizeof(quad_src),
-			"%s\n%s", quad_src_part, gradient_frag_gles2_src);
-	}
+	snprintf(quad_src_part, sizeof(quad_src_part),
+		quad_grad_frag_src, max_len);
+	snprintf(quad_src, sizeof(quad_src),
+		"%s\n%s", quad_src_part, gradient_frag_src);
 
 	GLuint prog;
-	shader->program = prog = link_program(quad_src, client_version);
+	shader->program = prog = link_program(quad_src);
 	if (!shader->program) {
 		return false;
 	}
@@ -183,18 +171,13 @@ bool link_quad_grad_program(struct quad_grad_shader *shader, GLint client_versio
 	return true;
 }
 
-bool link_quad_round_program(struct quad_round_shader *shader, GLint client_version) {
+bool link_quad_round_program(struct quad_round_shader *shader) {
 	GLchar quad_src[8192];
-	if (client_version > 2) {
-		snprintf(quad_src, sizeof(quad_src), "%s\n%s", quad_round_frag_gles3_src,
-			corner_alpha_frag_gles3_src);
-	} else {
-		snprintf(quad_src, sizeof(quad_src), "%s\n%s", quad_round_frag_gles2_src,
-			corner_alpha_frag_gles2_src);
-	}
+	snprintf(quad_src, sizeof(quad_src), "%s\n%s", quad_round_frag_src,
+		corner_alpha_frag_src);
 
 	GLuint prog;
-	shader->program = prog = link_program(quad_src, client_version);
+	shader->program = prog = link_program(quad_src);
 	if (!shader->program) {
 		return false;
 	}
@@ -204,39 +187,32 @@ bool link_quad_round_program(struct quad_round_shader *shader, GLint client_vers
 	shader->pos_attrib = glGetAttribLocation(prog, "pos");
 	shader->size = glGetUniformLocation(prog, "size");
 	shader->position = glGetUniformLocation(prog, "position");
-	shader->radius_top_left = glGetUniformLocation(prog, "radius_top_left");
-	shader->radius_top_right = glGetUniformLocation(prog, "radius_top_right");
-	shader->radius_bottom_left = glGetUniformLocation(prog, "radius_bottom_left");
-	shader->radius_bottom_right = glGetUniformLocation(prog, "radius_bottom_right");
+	shader->radius.top_left = glGetUniformLocation(prog, "radius_top_left");
+	shader->radius.top_right = glGetUniformLocation(prog, "radius_top_right");
+	shader->radius.bottom_left = glGetUniformLocation(prog, "radius_bottom_left");
+	shader->radius.bottom_right = glGetUniformLocation(prog, "radius_bottom_right");
 
 	shader->clip_size = glGetUniformLocation(prog, "clip_size");
 	shader->clip_position = glGetUniformLocation(prog, "clip_position");
-	shader->clip_radius_top_left = glGetUniformLocation(prog, "clip_radius_top_left");
-	shader->clip_radius_top_right = glGetUniformLocation(prog, "clip_radius_top_right");
-	shader->clip_radius_bottom_left = glGetUniformLocation(prog, "clip_radius_bottom_left");
-	shader->clip_radius_bottom_right = glGetUniformLocation(prog, "clip_radius_bottom_right");
+	shader->clip_radius.top_left = glGetUniformLocation(prog, "clip_radius_top_left");
+	shader->clip_radius.top_right = glGetUniformLocation(prog, "clip_radius_top_right");
+	shader->clip_radius.bottom_left = glGetUniformLocation(prog, "clip_radius_bottom_left");
+	shader->clip_radius.bottom_right = glGetUniformLocation(prog, "clip_radius_bottom_right");
 	shader->fade_inset = glGetUniformLocation(prog, "fade_inset");
 
 	return true;
 }
 
-bool link_quad_grad_round_program(struct quad_grad_round_shader *shader, GLint client_version, int max_len) {
+bool link_quad_grad_round_program(struct quad_grad_round_shader *shader, int max_len) {
 	GLchar quad_src_part[2048];
 	GLchar quad_src[8192];
-	if (client_version > 2) {
-		snprintf(quad_src_part, sizeof(quad_src_part),
-			quad_grad_round_frag_gles3_src, max_len);
-		snprintf(quad_src, sizeof(quad_src),
-			"%s\n%s\n%s", quad_src_part, gradient_frag_gles3_src, corner_alpha_frag_gles3_src);
-	} else {
-		snprintf(quad_src_part, sizeof(quad_src_part),
-			quad_grad_round_frag_gles2_src, max_len);
-		snprintf(quad_src, sizeof(quad_src),
-			"%s\n%s\n%s", quad_src_part, gradient_frag_gles2_src, corner_alpha_frag_gles2_src);
-	}
+	snprintf(quad_src_part, sizeof(quad_src_part),
+		quad_grad_round_frag_src, max_len);
+	snprintf(quad_src, sizeof(quad_src),
+		"%s\n%s\n%s", quad_src_part, gradient_frag_src, corner_alpha_frag_src);
 
 	GLuint prog;
-	shader->program = prog = link_program(quad_src, client_version);
+	shader->program = prog = link_program(quad_src);
 	if (!shader->program) {
 		return false;
 	}
@@ -246,10 +222,10 @@ bool link_quad_grad_round_program(struct quad_grad_round_shader *shader, GLint c
 	shader->pos_attrib = glGetAttribLocation(prog, "pos");
 	shader->size = glGetUniformLocation(prog, "size");
 	shader->position = glGetUniformLocation(prog, "position");
-	shader->radius_top_left = glGetUniformLocation(prog, "radius_top_left");
-	shader->radius_top_right = glGetUniformLocation(prog, "radius_top_right");
-	shader->radius_bottom_left = glGetUniformLocation(prog, "radius_bottom_left");
-	shader->radius_bottom_right = glGetUniformLocation(prog, "radius_bottom_right");
+	shader->radius.top_left = glGetUniformLocation(prog, "radius_top_left");
+	shader->radius.top_right = glGetUniformLocation(prog, "radius_top_right");
+	shader->radius.bottom_left = glGetUniformLocation(prog, "radius_bottom_left");
+	shader->radius.bottom_right = glGetUniformLocation(prog, "radius_bottom_right");
 
 	shader->grad_size = glGetUniformLocation(prog, "grad_size");
 	shader->colors = glGetUniformLocation(prog, "colors");
@@ -265,23 +241,17 @@ bool link_quad_grad_round_program(struct quad_grad_round_shader *shader, GLint c
 	return true;
 }
 
-bool link_tex_program(struct tex_shader *shader, GLint client_version, enum fx_tex_shader_source source) {
-	GLchar frag_src_part[2048];
-	GLchar frag_src[4096];
-	if (client_version > 2) {
-		snprintf(frag_src_part, sizeof(frag_src_part),
-			tex_frag_gles3_src, source);
-		snprintf(frag_src, sizeof(frag_src),
-			"%s\n%s\n", frag_src_part, corner_alpha_frag_gles3_src);
-	} else {
-		snprintf(frag_src_part, sizeof(frag_src_part),
-			tex_frag_gles2_src, source);
-		snprintf(frag_src, sizeof(frag_src),
-			"%s\n%s\n", frag_src_part, corner_alpha_frag_gles2_src);
-	}
+bool link_tex_program(struct tex_shader *shader, enum fx_tex_shader_source source,
+		bool effects) {
+	GLchar frag_src_part[4096];
+	GLchar frag_src[8192];
+	snprintf(frag_src_part, sizeof(frag_src_part),
+		tex_frag_src, source, effects);
+	snprintf(frag_src, sizeof(frag_src),
+		"%s\n%s\n", frag_src_part, effects ? corner_alpha_frag_src : "");
 
 	GLuint prog;
-	shader->program = prog = link_program(frag_src, client_version);
+	shader->program = prog = link_program(frag_src);
 	if (!shader->program) {
 		return false;
 	}
@@ -291,29 +261,36 @@ bool link_tex_program(struct tex_shader *shader, GLint client_version, enum fx_t
 	shader->alpha = glGetUniformLocation(prog, "alpha");
 	shader->pos_attrib = glGetAttribLocation(prog, "pos");
 	shader->tex_proj = glGetUniformLocation(prog, "tex_proj");
-	shader->size = glGetUniformLocation(prog, "size");
-	shader->position = glGetUniformLocation(prog, "position");
-	shader->radius_top_left = glGetUniformLocation(prog, "radius_top_left");
-	shader->radius_top_right = glGetUniformLocation(prog, "radius_top_right");
-	shader->radius_bottom_left = glGetUniformLocation(prog, "radius_bottom_left");
-	shader->radius_bottom_right = glGetUniformLocation(prog, "radius_bottom_right");
+
 	shader->discard_transparent = glGetUniformLocation(prog, "discard_transparent");
 
+	if (!effects) {
+		return true;
+	}
+	shader->effects.size = glGetUniformLocation(prog, "size");
+	shader->effects.position = glGetUniformLocation(prog, "position");
+	shader->effects.radius.top_left = glGetUniformLocation(prog, "radius_top_left");
+	shader->effects.radius.top_right = glGetUniformLocation(prog, "radius_top_right");
+	shader->effects.radius.bottom_left = glGetUniformLocation(prog, "radius_bottom_left");
+	shader->effects.radius.bottom_right = glGetUniformLocation(prog, "radius_bottom_right");
+
+	shader->effects.clip_size = glGetUniformLocation(prog, "clip_size");
+	shader->effects.clip_position = glGetUniformLocation(prog, "clip_position");
+	shader->effects.clip_radius.top_left = glGetUniformLocation(prog, "clip_radius_top_left");
+	shader->effects.clip_radius.top_right = glGetUniformLocation(prog, "clip_radius_top_right");
+	shader->effects.clip_radius.bottom_left = glGetUniformLocation(prog, "clip_radius_bottom_left");
+	shader->effects.clip_radius.bottom_right = glGetUniformLocation(prog, "clip_radius_bottom_right");
+
 	return true;
 }
 
-bool link_box_shadow_program(struct box_shadow_shader *shader, GLint client_version) {
+bool link_box_shadow_program(struct box_shadow_shader *shader) {
 	GLchar shadow_src[8192];
-	if (client_version > 2) {
-		snprintf(shadow_src, sizeof(shadow_src), "%s\n%s", box_shadow_frag_gles3_src,
-			corner_alpha_frag_gles3_src);
-	} else {
-		snprintf(shadow_src, sizeof(shadow_src), "%s\n%s", box_shadow_frag_gles2_src,
-			corner_alpha_frag_gles2_src);
-	}
+	snprintf(shadow_src, sizeof(shadow_src), "%s\n%s", box_shadow_frag_src,
+		corner_alpha_frag_src);
 
 	GLuint prog;
-	shader->program = prog = link_program(shadow_src, client_version);
+	shader->program = prog = link_program(shadow_src);
 	if (!shader->program) {
 		return false;
 	}
@@ -326,18 +303,17 @@ bool link_box_shadow_program(struct box_shadow_shader *shader, GLint client_vers
 	shader->corner_radius = glGetUniformLocation(prog, "corner_radius");
 	shader->clip_position = glGetUniformLocation(prog, "clip_position");
 	shader->clip_size = glGetUniformLocation(prog, "clip_size");
-	shader->clip_radius_top_left = glGetUniformLocation(prog, "clip_radius_top_left");
-	shader->clip_radius_top_right = glGetUniformLocation(prog, "clip_radius_top_right");
-	shader->clip_radius_bottom_left = glGetUniformLocation(prog, "clip_radius_bottom_left");
-	shader->clip_radius_bottom_right = glGetUniformLocation(prog, "clip_radius_bottom_right");
+	shader->clip_radius.top_left = glGetUniformLocation(prog, "clip_radius_top_left");
+	shader->clip_radius.top_right = glGetUniformLocation(prog, "clip_radius_top_right");
+	shader->clip_radius.bottom_left = glGetUniformLocation(prog, "clip_radius_bottom_left");
+	shader->clip_radius.bottom_right = glGetUniformLocation(prog, "clip_radius_bottom_right");
 
 	return true;
 }
 
-bool link_blur1_program(struct blur_shader *shader, GLint client_version) {
+bool link_blur1_program(struct blur_shader *shader) {
 	GLuint prog;
-	shader->program = prog = client_version > 2 ? link_program(blur1_frag_gles3_src, client_version)
-		: link_program(blur1_frag_gles2_src, client_version);
+	shader->program = prog = link_program(blur1_frag_src);
 	if (!shader->program) {
 		return false;
 	}
@@ -351,10 +327,9 @@ bool link_blur1_program(struct blur_shader *shader, GLint client_version) {
 	return true;
 }
 
-bool link_blur2_program(struct blur_shader *shader, GLint client_version) {
+bool link_blur2_program(struct blur_shader *shader) {
 	GLuint prog;
-	shader->program = prog = client_version > 2 ? link_program(blur2_frag_gles3_src, client_version)
-		: link_program(blur2_frag_gles2_src, client_version);
+	shader->program = prog = link_program(blur2_frag_src);
 	if (!shader->program) {
 		return false;
 	}
@@ -368,10 +343,9 @@ bool link_blur2_program(struct blur_shader *shader, GLint client_version) {
 	return true;
 }
 
-bool link_blur_effects_program(struct blur_effects_shader *shader, GLint client_version) {
+bool link_blur_effects_program(struct blur_effects_shader *shader) {
 	GLuint prog;
-	shader->program = prog = client_version > 2 ? link_program(blur_effects_frag_gles3_src, client_version)
-		: link_program(blur_effects_frag_gles2_src, client_version);
+	shader->program = prog = link_program(blur_effects_frag_src);
 	if (!shader->program) {
 		return false;
 	}
diff --git a/scenefx/render/fx_renderer/gles2/shaders/blur1.frag b/scenefx/render/fx_renderer/shaders/blur1.frag
similarity index 100%
rename from scenefx/render/fx_renderer/gles2/shaders/blur1.frag
rename to scenefx/render/fx_renderer/shaders/blur1.frag
diff --git a/scenefx/render/fx_renderer/gles2/shaders/blur2.frag b/scenefx/render/fx_renderer/shaders/blur2.frag
similarity index 100%
rename from scenefx/render/fx_renderer/gles2/shaders/blur2.frag
rename to scenefx/render/fx_renderer/shaders/blur2.frag
diff --git a/scenefx/render/fx_renderer/gles2/shaders/blur_effects.frag b/scenefx/render/fx_renderer/shaders/blur_effects.frag
similarity index 99%
rename from scenefx/render/fx_renderer/gles2/shaders/blur_effects.frag
rename to scenefx/render/fx_renderer/shaders/blur_effects.frag
index 6f7411f..c3d3246 100644
--- a/scenefx/render/fx_renderer/gles2/shaders/blur_effects.frag
+++ b/scenefx/render/fx_renderer/shaders/blur_effects.frag
@@ -47,7 +47,7 @@ float noiseAmount(vec2 p) {
 	p3 += dot(p3, p3.yzx + 33.33);
 	float hash = fract((p3.x + p3.y) * p3.z);
 	return (mod(hash, 1.0) - 0.5) * noise;
-};
+}
 
 void main() {
 	vec4 color = texture2D(tex, v_texcoord);
diff --git a/scenefx/render/fx_renderer/gles2/shaders/box_shadow.frag b/scenefx/render/fx_renderer/shaders/box_shadow.frag
similarity index 88%
rename from scenefx/render/fx_renderer/gles2/shaders/box_shadow.frag
rename to scenefx/render/fx_renderer/shaders/box_shadow.frag
index 6a40109..1cab7b5 100644
--- a/scenefx/render/fx_renderer/gles2/shaders/box_shadow.frag
+++ b/scenefx/render/fx_renderer/shaders/box_shadow.frag
@@ -66,12 +66,8 @@ float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float co
     return value;
 }
 
-// per-pixel "random" number between 0 and 1
-float random() {
-    return fract(sin(dot(vec2(12.9898, 78.233), gl_FragCoord.xy)) * 43758.5453);
-}
-
-float corner_alpha(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
+float corner_alpha(vec2 size, vec2 position, bool is_cutout,
+        float radius_tl, float radius_tr, float radius_bl, float radius_br);
 
 void main() {
     float shadow_alpha = v_color.a * roundedBoxShadow(
@@ -79,13 +75,12 @@ void main() {
             position + size - blur_sigma,
             gl_FragCoord.xy, blur_sigma * 0.5,
             corner_radius);
-    // dither the alpha to break up color bands
-    shadow_alpha += (random() - 0.5) / 128.0;
 
     // Clipping
     float clip_corner_alpha = corner_alpha(
         clip_size - 1.5,
         clip_position + 0.75,
+        true,
         clip_radius_top_left,
         clip_radius_top_right,
         clip_radius_bottom_left,
diff --git a/scenefx/render/fx_renderer/gles2/shaders/common.vert b/scenefx/render/fx_renderer/shaders/common.vert
similarity index 100%
rename from scenefx/render/fx_renderer/gles2/shaders/common.vert
rename to scenefx/render/fx_renderer/shaders/common.vert
diff --git a/scenefx/render/fx_renderer/shaders/corner_alpha.frag b/scenefx/render/fx_renderer/shaders/corner_alpha.frag
new file mode 100644
index 0000000..42c1413
--- /dev/null
+++ b/scenefx/render/fx_renderer/shaders/corner_alpha.frag
@@ -0,0 +1,74 @@
+float get_dist(vec2 q, float radius) {
+	return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
+}
+
+// Note: Returns 0.0 if outside, 1.0 if inside the bounds. The is_cutout parameter
+// reverses this, 0.0 inside cutout and 1.0 for outside cutout.
+float corner_alpha(vec2 size, vec2 position, bool is_cutout,
+		float radius_tl, float radius_tr, float radius_bl, float radius_br) {
+	if (radius_tl <= 0.0
+			&& radius_tr <= 0.0
+			&& radius_bl <= 0.0
+			&& radius_br <= 0.0) {
+		return 1.0;
+	}
+
+	vec2 relative_pos = (gl_FragCoord.xy - position);
+
+	if (relative_pos.x < 0.0 || relative_pos.y < 0.0
+			|| relative_pos.x > size.x || relative_pos.y > size.y) {
+		if (is_cutout) {
+			return 1.0;
+		}
+		discard;
+	}
+
+	bool is_top_left = radius_tl > 0.0
+		&& relative_pos.x <= radius_tl
+		&& relative_pos.y <= radius_tl;
+	bool is_top_right = radius_tr > 0.0
+		&& relative_pos.x >= size.x - radius_tr
+		&& relative_pos.y <= radius_tr;
+	bool is_bottom_left = radius_bl > 0.0
+		&& relative_pos.x <= radius_bl
+		&& relative_pos.y >= size.y - radius_bl;
+	bool is_bottom_right = radius_br > 0.0
+		&& relative_pos.x >= size.x - radius_br
+		&& relative_pos.y >= size.y - radius_br;
+	if (!is_top_left && !is_top_right && !is_bottom_left && !is_bottom_right) {
+		if (is_cutout) {
+			discard;
+		}
+		return 1.0;
+	}
+
+	vec2 top_left = abs(relative_pos - size) - size + radius_tl;
+	vec2 top_right = abs(relative_pos - vec2(0, size.y)) - size + radius_tr;
+	vec2 bottom_left = abs(relative_pos - vec2(size.x, 0)) - size + radius_bl;
+	vec2 bottom_right = abs(relative_pos) - size + radius_br;
+
+	float dist = max(
+		max(get_dist(top_left, radius_tl), get_dist(top_right, radius_tr)),
+		max(get_dist(bottom_left, radius_bl), get_dist(bottom_right, radius_br))
+	);
+
+	float result = smoothstep(0.0, 1.0, dist);
+	return is_cutout ? result : 1.0 - result;
+}
+
+float corner_dist(vec2 size, vec2 position,
+		float radius_tl, float radius_tr, float radius_bl, float radius_br) {
+	vec2 relative_pos = (gl_FragCoord.xy - position);
+
+	vec2 top_left = abs(relative_pos - size) - size + radius_tl;
+	vec2 top_right = abs(relative_pos - vec2(0, size.y)) - size + radius_tr;
+	vec2 bottom_left = abs(relative_pos - vec2(size.x, 0)) - size + radius_bl;
+	vec2 bottom_right = abs(relative_pos) - size + radius_br;
+
+	float dist = max(
+		max(get_dist(top_left, radius_tl), get_dist(top_right, radius_tr)),
+		max(get_dist(bottom_left, radius_bl), get_dist(bottom_right, radius_br))
+	);
+
+	return dist;
+}
diff --git a/scenefx/render/fx_renderer/gles2/shaders/embed.sh b/scenefx/render/fx_renderer/shaders/embed.sh
similarity index 100%
rename from scenefx/render/fx_renderer/gles2/shaders/embed.sh
rename to scenefx/render/fx_renderer/shaders/embed.sh
diff --git a/scenefx/render/fx_renderer/gles2/shaders/gradient.frag b/scenefx/render/fx_renderer/shaders/gradient.frag
similarity index 100%
rename from scenefx/render/fx_renderer/gles2/shaders/gradient.frag
rename to scenefx/render/fx_renderer/shaders/gradient.frag
diff --git a/scenefx/render/fx_renderer/shaders/meson.build b/scenefx/render/fx_renderer/shaders/meson.build
new file mode 100644
index 0000000..1bafd4f
--- /dev/null
+++ b/scenefx/render/fx_renderer/shaders/meson.build
@@ -0,0 +1,31 @@
+embed = find_program('./embed.sh', native: true)
+
+# TODO: Validate each fragment shader just like upstream
+shaders = [
+	'common.vert',
+	'gradient.frag',
+	'corner_alpha.frag',
+	'quad.frag',
+	'quad_grad.frag',
+	'quad_round.frag',
+	'quad_grad_round.frag',
+	'tex.frag',
+	'box_shadow.frag',
+	'blur1.frag',
+	'blur2.frag',
+	'blur_effects.frag',
+]
+
+foreach name : shaders
+	output = name.underscorify() + '_src.h'
+	var = name.underscorify() + '_src'
+	scenefx_files += custom_target(
+		output,
+		command: [embed, var],
+		input: name,
+		output: output,
+		feed: true,
+		capture: true,
+	)
+endforeach
+
diff --git a/scenefx/render/fx_renderer/shaders/quad.frag b/scenefx/render/fx_renderer/shaders/quad.frag
new file mode 100644
index 0000000..93c9fe6
--- /dev/null
+++ b/scenefx/render/fx_renderer/shaders/quad.frag
@@ -0,0 +1,45 @@
+#define EFFECTS %d
+
+#if !defined(EFFECTS)
+#error "Missing shader preamble"
+#endif
+
+#ifdef GL_FRAGMENT_PRECISION_HIGH
+precision highp float;
+#else
+precision mediump float;
+#endif
+
+varying vec4 v_color;
+varying vec2 v_texcoord;
+
+#if EFFECTS
+uniform vec2 clip_size;
+uniform vec2 clip_position;
+uniform float clip_radius_top_left;
+uniform float clip_radius_top_right;
+uniform float clip_radius_bottom_left;
+uniform float clip_radius_bottom_right;
+#endif
+
+float corner_alpha(vec2 size, vec2 position, bool is_cutout,
+		float radius_tl, float radius_tr, float radius_bl, float radius_br);
+
+void main() {
+#if EFFECTS
+	// Clipping
+	float clip_corner_alpha = corner_alpha(
+		clip_size - 1.0,
+		clip_position + 0.5,
+		true,
+		clip_radius_top_left,
+		clip_radius_top_right,
+		clip_radius_bottom_left,
+		clip_radius_bottom_right
+	);
+
+	gl_FragColor = v_color * clip_corner_alpha;
+#else
+	gl_FragColor = v_color;
+#endif
+}
diff --git a/scenefx/render/fx_renderer/gles2/shaders/quad_grad.frag b/scenefx/render/fx_renderer/shaders/quad_grad.frag
similarity index 100%
rename from scenefx/render/fx_renderer/gles2/shaders/quad_grad.frag
rename to scenefx/render/fx_renderer/shaders/quad_grad.frag
diff --git a/scenefx/render/fx_renderer/gles2/shaders/quad_grad_round.frag b/scenefx/render/fx_renderer/shaders/quad_grad_round.frag
similarity index 57%
rename from scenefx/render/fx_renderer/gles2/shaders/quad_grad_round.frag
rename to scenefx/render/fx_renderer/shaders/quad_grad_round.frag
index bbf7400..87d48d7 100644
--- a/scenefx/render/fx_renderer/gles2/shaders/quad_grad_round.frag
+++ b/scenefx/render/fx_renderer/shaders/quad_grad_round.frag
@@ -28,19 +28,21 @@ uniform int count;
 
 vec4 gradient(vec4 colors[LEN], int count, vec2 size, vec2 grad_box, vec2 origin, float degree, bool linear, bool blend);
 
-float corner_alpha(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
+float corner_alpha(vec2 size, vec2 position, bool is_cutout,
+		float radius_tl, float radius_tr, float radius_bl, float radius_br);
 
 // TODO:
 void main() {
-    float quad_corner_alpha = corner_alpha(
-        size - 1.0,
-        position + 0.5,
-        radius_top_left,
-        radius_top_right,
-        radius_bottom_left,
-        radius_bottom_right
-    );
-    float rect_alpha = v_color.a * quad_corner_alpha;
-
-    gl_FragColor = mix(vec4(0), gradient(colors, count, size, grad_box, origin, degree, linear, blend), rect_alpha);
+	float quad_corner_alpha = corner_alpha(
+		size - 1.0,
+		position + 0.5,
+		false,
+		radius_top_left,
+		radius_top_right,
+		radius_bottom_left,
+		radius_bottom_right
+	);
+	float rect_alpha = v_color.a * quad_corner_alpha;
+
+	gl_FragColor = mix(vec4(0.0), gradient(colors, count, size, grad_box, origin, degree, linear, blend), rect_alpha);
 }
diff --git a/scenefx/render/fx_renderer/shaders/quad_round.frag b/scenefx/render/fx_renderer/shaders/quad_round.frag
new file mode 100644
index 0000000..3e80e26
--- /dev/null
+++ b/scenefx/render/fx_renderer/shaders/quad_round.frag
@@ -0,0 +1,72 @@
+#ifdef GL_FRAGMENT_PRECISION_HIGH
+precision highp float;
+#else
+precision mediump float;
+#endif
+
+varying vec4 v_color;
+varying vec2 v_texcoord;
+
+uniform vec2 size;
+uniform vec2 position;
+uniform float radius_top_left;
+uniform float radius_top_right;
+uniform float radius_bottom_left;
+uniform float radius_bottom_right;
+uniform float fade_inset;
+
+uniform vec2 clip_size;
+uniform vec2 clip_position;
+uniform float clip_radius_top_left;
+uniform float clip_radius_top_right;
+uniform float clip_radius_bottom_left;
+uniform float clip_radius_bottom_right;
+
+float corner_alpha(vec2 size, vec2 position, bool is_cutout,
+		float radius_tl, float radius_tr, float radius_bl, float radius_br);
+float corner_dist(vec2 size, vec2 position,
+		float radius_tl, float radius_tr, float radius_bl, float radius_br);
+
+void main() {
+	float quad_corner_alpha = corner_alpha(
+		size - 1.0,
+		position + 0.5,
+		false,
+		radius_top_left,
+		radius_top_right,
+		radius_bottom_left,
+		radius_bottom_right
+	);
+
+	// Clipping
+	float clip_corner_alpha = corner_alpha(
+		clip_size - 1.0,
+		clip_position + 0.5,
+		true,
+		clip_radius_top_left,
+		clip_radius_top_right,
+		clip_radius_bottom_left,
+		clip_radius_bottom_right
+	);
+
+	vec4 final_color = v_color;
+	if (fade_inset > 0.0) {
+		float r_tl = radius_top_left > 0.0 ? radius_top_left + fade_inset : 0.0;
+		float r_tr = radius_top_right > 0.0 ? radius_top_right + fade_inset : 0.0;
+		float r_bl = radius_bottom_left > 0.0 ? radius_bottom_left + fade_inset : 0.0;
+		float r_br = radius_bottom_right > 0.0 ? radius_bottom_right + fade_inset : 0.0;
+		float d = corner_dist(
+			size - 1.0,
+			position + 0.5,
+			r_tl,
+			r_tr,
+			r_bl,
+			r_br
+		);
+		float inside_dist = -d;
+		float fade_factor = clamp(inside_dist / fade_inset, 0.0, 1.0);
+		final_color *= fade_factor;
+	}
+
+	gl_FragColor = final_color * quad_corner_alpha * clip_corner_alpha;
+}
diff --git a/scenefx/render/fx_renderer/gles2/shaders/tex.frag b/scenefx/render/fx_renderer/shaders/tex.frag
similarity index 52%
rename from scenefx/render/fx_renderer/gles2/shaders/tex.frag
rename to scenefx/render/fx_renderer/shaders/tex.frag
index 08ad3a1..11e3102 100644
--- a/scenefx/render/fx_renderer/gles2/shaders/tex.frag
+++ b/scenefx/render/fx_renderer/shaders/tex.frag
@@ -1,10 +1,11 @@
 #define SOURCE %d
+#define EFFECTS %d
 
 #define SOURCE_TEXTURE_RGBA 1
 #define SOURCE_TEXTURE_RGBX 2
 #define SOURCE_TEXTURE_EXTERNAL 3
 
-#if !defined(SOURCE)
+#if !defined(SOURCE) || !defined(EFFECTS)
 #error "Missing shader preamble"
 #endif
 
@@ -28,6 +29,7 @@ uniform sampler2D tex;
 
 uniform float alpha;
 
+#if EFFECTS
 uniform vec2 size;
 uniform vec2 position;
 uniform float radius_top_left;
@@ -35,6 +37,14 @@ uniform float radius_top_right;
 uniform float radius_bottom_left;
 uniform float radius_bottom_right;
 
+uniform vec2 clip_size;
+uniform vec2 clip_position;
+uniform float clip_radius_top_left;
+uniform float clip_radius_top_right;
+uniform float clip_radius_bottom_left;
+uniform float clip_radius_bottom_right;
+#endif
+
 uniform bool discard_transparent;
 
 vec4 sample_texture() {
@@ -45,21 +55,40 @@ vec4 sample_texture() {
 #endif
 }
 
-float corner_alpha(vec2 size, vec2 position, float round_tl, float round_tr, float round_bl, float round_br);
+#if EFFECTS
+float corner_alpha(vec2 size, vec2 position, bool is_cutout,
+		float radius_tl, float radius_tr, float radius_bl, float radius_br);
+#endif
 
 void main() {
-    float corner_alpha = corner_alpha(
-        size - 0.5,
-        position + 0.25,
-        radius_top_left,
-        radius_top_right,
-        radius_bottom_left,
-        radius_bottom_right
-    );
-	gl_FragColor = mix(sample_texture() * alpha, vec4(0.0), corner_alpha);
+#if EFFECTS
+	float quad_corner_alpha = corner_alpha(
+		size - 0.5,
+		position + 0.25,
+		false,
+		radius_top_left,
+		radius_top_right,
+		radius_bottom_left,
+		radius_bottom_right
+	);
+
+	// Clipping
+	float clip_corner_alpha = corner_alpha(
+		clip_size - 1.0,
+		clip_position + 0.5,
+		true,
+		clip_radius_top_left,
+		clip_radius_top_right,
+		clip_radius_bottom_left,
+		clip_radius_bottom_right
+	);
+
+	gl_FragColor = sample_texture() * alpha * quad_corner_alpha * clip_corner_alpha;
+#else
+	gl_FragColor = sample_texture() * alpha;
+#endif
 
 	if (discard_transparent && gl_FragColor.a == 0.0) {
 		discard;
-		return;
 	}
 }
diff --git a/scenefx/render/fx_renderer/tracy.c b/scenefx/render/fx_renderer/tracy.c
new file mode 100644
index 0000000..e8c773e
--- /dev/null
+++ b/scenefx/render/fx_renderer/tracy.c
@@ -0,0 +1,212 @@
+#ifdef TRACY_ENABLE
+#include <GLES2/gl2.h>
+#include <assert.h>
+#include <stdatomic.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <tracy/TracyC.h>
+#include <wayland-util.h>
+#include <wlr/util/log.h>
+
+#include "render/fx_renderer/fx_renderer.h"
+#include "render/tracy.h"
+
+// Docs used in the implementation:
+// - https://github.com/wolfpld/tracy/releases/latest/download/tracy.pdf
+// - https://registry.khronos.org/OpenGL/extensions/EXT/EXT_disjoint_timer_query.txt
+// - https://github.com/wolfpld/tracy/blob/7388a476587c93f4e8e3e50d64c1400fc8c5e47e/public/tracy/TracyOpenGL.hpp
+
+#define QUERY_QUEUE_LEN (64 * 1024)
+
+// Colors:
+//	From https://github.com/wolfpld/tracy/blob/7388a476587c93f4e8e3e50d64c1400fc8c5e47e/public/common/TracyColor.hpp
+#define RED4 0x8b0000
+
+static atomic_int id_counter = 0;
+
+struct tracy_data {
+	struct fx_renderer *renderer;
+
+	uint8_t context_id;
+
+	struct {
+		uint32_t queries[QUERY_QUEUE_LEN];
+		uint32_t head;
+		uint32_t tail;
+	} queue;
+};
+
+/*
+ * GPU Zone
+ */
+
+static inline unsigned int get_next_query_index(struct tracy_data *tracy_data) {
+	const unsigned int id = tracy_data->queue.head;
+	tracy_data->queue.head = (tracy_data->queue.head + 1) % QUERY_QUEUE_LEN;
+	assert(tracy_data->queue.head != tracy_data->queue.tail);
+	return id;
+}
+
+void tracy_gpu_zone_begin(struct tracy_data *tracy_data, struct tracy_gpu_zone_context *out_ctx,
+		const int line, const char *source, const char *func, const char *name) {
+	if (out_ctx == NULL || tracy_data == NULL) {
+		return;
+	}
+
+	out_ctx->tracy_data = tracy_data;
+#ifdef TRACY_ON_DEMAND
+	out_ctx->is_active = TracyCIsConnected;
+#else
+	out_ctx->is_active = true;
+#endif
+
+	if (!out_ctx->is_active) {
+		return;
+	}
+
+	// Create the query object
+	const unsigned int query = get_next_query_index(tracy_data);
+	tracy_data->renderer->procs.glQueryCounterEXT(tracy_data->queue.queries[query], GL_TIMESTAMP_EXT);
+
+	// Label the zone
+	uint64_t srcloc = ___tracy_alloc_srcloc_name(line,
+			source, strlen(source),
+			func, strlen(func),
+			name, strlen(name),
+			0);
+
+	// Begin the Tracy GPU zone
+	const struct ___tracy_gpu_zone_begin_data data = {
+		.context = tracy_data->context_id,
+		.queryId = (uint16_t) query,
+		.srcloc = srcloc,
+	};
+	___tracy_emit_gpu_zone_begin_alloc(data);
+}
+
+void tracy_gpu_zone_end(struct tracy_gpu_zone_context *ctx) {
+	if (ctx == NULL || ctx->tracy_data == NULL || !ctx->is_active) {
+		return;
+	}
+	struct tracy_data *tracy_data = ctx->tracy_data;
+
+	// Create the query object
+	const unsigned int query = get_next_query_index(tracy_data);
+	tracy_data->renderer->procs.glQueryCounterEXT(tracy_data->queue.queries[query], GL_TIMESTAMP_EXT);
+
+	// End the Tracy GPU zone
+	const struct ___tracy_gpu_zone_end_data data = {
+		.context = tracy_data->context_id,
+		.queryId = (uint16_t) query,
+	};
+	___tracy_emit_gpu_zone_end(data);
+}
+
+void tracy_gpu_context_collect(struct tracy_data *tracy_data) {
+	if (tracy_data == NULL) {
+		TracyCMessageL("tracy_data == NULL");
+		return;
+	}
+
+	TracyCZoneC(ctx, RED4, true);
+
+	if (tracy_data->queue.tail == tracy_data->queue.head) {
+		goto done;
+	}
+
+#ifdef TRACY_ON_DEMAND
+	if (!TracyCIsConnected) {
+		tracy_data->queue.head = 0;
+		tracy_data->queue.tail = 0;
+		goto done;
+	}
+#endif
+
+	while (tracy_data->queue.tail != tracy_data->queue.head) {
+		GLint available;
+		tracy_data->renderer->procs.glGetQueryObjectivEXT(
+				tracy_data->queue.queries[tracy_data->queue.tail],
+				GL_QUERY_RESULT_AVAILABLE_EXT, &available);
+		if (!available) {
+			goto done;
+		}
+
+		GLuint64 time;
+		tracy_data->renderer->procs.glGetQueryObjectui64vEXT(
+				tracy_data->queue.queries[tracy_data->queue.tail],
+				GL_QUERY_RESULT_EXT, &time);
+
+		const struct ___tracy_gpu_time_data data = {
+			.context = tracy_data->context_id,
+			.gpuTime = time,
+			.queryId = (uint16_t) tracy_data->queue.tail,
+		};
+		___tracy_emit_gpu_time(data);
+
+		tracy_data->queue.tail = (tracy_data->queue.tail + 1) % QUERY_QUEUE_LEN;
+	}
+
+done:
+	TracyCZoneEnd(ctx);
+}
+
+void tracy_gpu_context_destroy(struct tracy_data *tracy_data) {
+	id_counter--;
+	if (tracy_data == NULL) {
+		return;
+	}
+
+	tracy_data->renderer->procs.glDeleteQueriesEXT(QUERY_QUEUE_LEN, tracy_data->queue.queries);
+	free(tracy_data);
+}
+
+struct tracy_data *tracy_gpu_context_new(struct fx_renderer *renderer) {
+	assert(renderer);
+
+	struct tracy_data *tracy_data = calloc(1, sizeof(*tracy_data));
+	if (tracy_data == NULL) {
+		wlr_log_errno(WLR_ERROR, "Allocation failed");
+		return NULL;
+	}
+
+	tracy_data->renderer = renderer;
+	tracy_data->context_id = ++id_counter;
+	tracy_data->queue.head = 0;
+	tracy_data->queue.tail = 0;
+
+	// Create the query objects
+	renderer->procs.glGenQueriesEXT(QUERY_QUEUE_LEN, tracy_data->queue.queries);
+
+	// Get the current GL time
+	GLint64 gl_gpu_time;
+	renderer->procs.glGetInteger64vEXT(GL_TIMESTAMP_EXT, &gl_gpu_time);
+
+	// Create the query objects
+	GLint bits;
+	renderer->procs.glGetQueryivEXT(GL_TIMESTAMP_EXT, GL_QUERY_COUNTER_BITS_EXT, &bits);
+
+	const struct ___tracy_gpu_new_context_data data = {
+		.context = tracy_data->context_id,
+		.gpuTime = gl_gpu_time,
+		.period = 1.0f,
+		.flags = 0,
+		.type = 1, // `enum class GpuContextType` in TracyQueue.hpp
+	};
+	___tracy_emit_gpu_new_context(data);
+
+	// Set the custom name
+	int len = snprintf(NULL, 0, "FX Renderer (GLESV2): %s",
+		 glGetString(GL_RENDERER)) + 1; \
+	char ctx_name[len]; \
+	snprintf(ctx_name, len, "FX Renderer (GLESV2): %s",
+		glGetString(GL_RENDERER)); \
+	const struct ___tracy_gpu_context_name_data name_data = {
+		.context = tracy_data->context_id,
+		.name = ctx_name,
+		.len = strlen(ctx_name),
+	};
+	___tracy_emit_gpu_context_name(name_data);
+	return tracy_data;
+}
+#endif /* ifdef TRACY_ENABLE */
diff --git a/scenefx/render/meson.build b/scenefx/render/meson.build
index c5dd423..97b97d6 100644
--- a/scenefx/render/meson.build
+++ b/scenefx/render/meson.build
@@ -1,6 +1,16 @@
 scenefx_files += files(
-	'pixel_format.c',
+	'color.c',
 	'egl.c',
+	'pixel_format.c',
 )
 
 subdir('fx_renderer')
+
+lcms2 = dependency('lcms2', required: get_option('color-management'))
+if lcms2.found()
+	scenefx_deps += lcms2
+	scenefx_files += files('color_lcms2.c')
+	features += {'color-management': true}
+else
+	scenefx_files += files('color_fallback.c')
+endif
diff --git a/scenefx/render/pixel_format.c b/scenefx/render/pixel_format.c
index 7befbf0..c60dd9d 100644
--- a/scenefx/render/pixel_format.c
+++ b/scenefx/render/pixel_format.c
@@ -44,10 +44,26 @@ static const struct wlr_pixel_format_info pixel_format_info[] = {
 		.drm_format = DRM_FORMAT_R8,
 		.bytes_per_block = 1,
 	},
+	{
+		.drm_format = DRM_FORMAT_R16F,
+		.bytes_per_block = 2,
+	},
+	{
+		.drm_format = DRM_FORMAT_R32F,
+		.bytes_per_block = 4,
+	},
 	{
 		.drm_format = DRM_FORMAT_GR88,
 		.bytes_per_block = 2,
 	},
+	{
+		.drm_format = DRM_FORMAT_GR1616F,
+		.bytes_per_block = 4,
+	},
+	{
+		.drm_format = DRM_FORMAT_GR3232F,
+		.bytes_per_block = 8,
+	},
 	{
 		.drm_format = DRM_FORMAT_RGB888,
 		.bytes_per_block = 3,
@@ -56,6 +72,18 @@ static const struct wlr_pixel_format_info pixel_format_info[] = {
 		.drm_format = DRM_FORMAT_BGR888,
 		.bytes_per_block = 3,
 	},
+	{
+		.drm_format = DRM_FORMAT_BGR161616,
+		.bytes_per_block = 6,
+	},
+	{
+		.drm_format = DRM_FORMAT_BGR161616F,
+		.bytes_per_block = 6,
+	},
+	{
+		.drm_format = DRM_FORMAT_BGR323232F,
+		.bytes_per_block = 12,
+	},
 	{
 		.drm_format = DRM_FORMAT_RGBX4444,
 		.bytes_per_block = 2,
@@ -136,6 +164,10 @@ static const struct wlr_pixel_format_info pixel_format_info[] = {
 		.opaque_substitute = DRM_FORMAT_XBGR16161616F,
 		.bytes_per_block = 8,
 	},
+	{
+		.drm_format = DRM_FORMAT_ABGR32323232F,
+		.bytes_per_block = 16,
+	},
 	{
 		.drm_format = DRM_FORMAT_XBGR16161616,
 		.bytes_per_block = 8,
@@ -275,3 +307,68 @@ bool pixel_format_has_alpha(uint32_t fmt) {
 	}
 	return true;
 }
+
+bool pixel_format_is_ycbcr(uint32_t format) {
+	switch (format) {
+	case DRM_FORMAT_AYUV:
+	case DRM_FORMAT_NV12:
+	case DRM_FORMAT_NV15:
+	case DRM_FORMAT_NV16:
+	case DRM_FORMAT_NV20:
+	case DRM_FORMAT_NV21:
+	case DRM_FORMAT_NV24:
+	case DRM_FORMAT_NV30:
+	case DRM_FORMAT_NV42:
+	case DRM_FORMAT_NV61:
+	case DRM_FORMAT_P010:
+	case DRM_FORMAT_P012:
+	case DRM_FORMAT_P016:
+	case DRM_FORMAT_P030:
+	case DRM_FORMAT_P210:
+	case DRM_FORMAT_Q401:
+	case DRM_FORMAT_Q410:
+	case DRM_FORMAT_S010:
+	case DRM_FORMAT_S012:
+	case DRM_FORMAT_S016:
+	case DRM_FORMAT_S210:
+	case DRM_FORMAT_S212:
+	case DRM_FORMAT_S216:
+	case DRM_FORMAT_S410:
+	case DRM_FORMAT_S412:
+	case DRM_FORMAT_S416:
+	case DRM_FORMAT_UYVY:
+	case DRM_FORMAT_VUY101010:
+	case DRM_FORMAT_VUY888:
+	case DRM_FORMAT_VYUY:
+	case DRM_FORMAT_X0L0:
+	case DRM_FORMAT_X0L2:
+	case DRM_FORMAT_XVYU12_16161616:
+	case DRM_FORMAT_XVYU16161616:
+	case DRM_FORMAT_XVYU2101010:
+	case DRM_FORMAT_XYUV8888:
+	case DRM_FORMAT_Y0L0:
+	case DRM_FORMAT_Y0L2:
+	case DRM_FORMAT_Y210:
+	case DRM_FORMAT_Y212:
+	case DRM_FORMAT_Y216:
+	case DRM_FORMAT_Y410:
+	case DRM_FORMAT_Y412:
+	case DRM_FORMAT_Y416:
+	case DRM_FORMAT_YUV410:
+	case DRM_FORMAT_YUV411:
+	case DRM_FORMAT_YUV420:
+	case DRM_FORMAT_YUV420_10BIT:
+	case DRM_FORMAT_YUV420_8BIT:
+	case DRM_FORMAT_YUV422:
+	case DRM_FORMAT_YUV444:
+	case DRM_FORMAT_YUYV:
+	case DRM_FORMAT_YVU410:
+	case DRM_FORMAT_YVU411:
+	case DRM_FORMAT_YVU420:
+	case DRM_FORMAT_YVU422:
+	case DRM_FORMAT_YVU444:
+	case DRM_FORMAT_YVYU:
+		return true;
+	}
+	return false;
+}
diff --git a/scenefx/subprojects/packagefiles/tracy_v0_13_1_build_fix.patch b/scenefx/subprojects/packagefiles/tracy_v0_13_1_build_fix.patch
new file mode 100644
index 0000000..d85b014
--- /dev/null
+++ b/scenefx/subprojects/packagefiles/tracy_v0_13_1_build_fix.patch
@@ -0,0 +1,22 @@
+diff --git a/public/client/TracyProfiler.cpp b/public/client/TracyProfiler.cpp
+index 0691f78..20a3f02 100644
+--- a/public/client/TracyProfiler.cpp
++++ b/public/client/TracyProfiler.cpp
+@@ -933,7 +933,7 @@ static Thread* s_symbolThread;
+ std::atomic<bool> s_symbolThreadGone { false };
+ #endif
+ #ifdef TRACY_HAS_SYSTEM_TRACING
+-static std::atomic<Thread*> s_sysTraceThread = nullptr;
++static std::atomic<Thread*> s_sysTraceThread(nullptr);
+ #endif
+ 
+ #if defined __linux__ && !defined TRACY_NO_CRASH_HANDLER
+@@ -2154,7 +2154,7 @@ void Profiler::Worker()
+     // takes care of calling StopSystemTracing(). However, a client may decide to
+     // manually RequestShutdown(), in which case ~Profile() may not execute before
+     // this Worker() thread goes through its teardown stages and reaches this point.
+-    // To ensure that system tracing does not keep pushing data to the worker queue 
++    // To ensure that system tracing does not keep pushing data to the worker queue
+     // indefinitely (thus preventing this worker from terminating), we have to call
+     // StopSystemTracing() here as well to be safe:
+     StopSystemTracing();
diff --git a/scenefx/subprojects/tracy.wrap b/scenefx/subprojects/tracy.wrap
new file mode 100644
index 0000000..728ed1a
--- /dev/null
+++ b/scenefx/subprojects/tracy.wrap
@@ -0,0 +1,8 @@
+[wrap-git]
+url=https://github.com/wolfpld/tracy.git
+revision=v0.13.1
+depth=1
+
+# Fixes build errors for v0.13.1.
+# Should be removed when the next version gets released!
+diff_files=tracy_v0_13_1_build_fix.patch
diff --git a/scenefx/tinywl/Makefile b/scenefx/tinywl/Makefile
index fb5f315..a99934c 100644
--- a/scenefx/tinywl/Makefile
+++ b/scenefx/tinywl/Makefile
@@ -1,27 +1,18 @@
 PKG_CONFIG?=pkg-config
-WAYLAND_PROTOCOLS!=$(PKG_CONFIG) --variable=pkgdatadir wayland-protocols
-WAYLAND_SCANNER!=$(PKG_CONFIG) --variable=wayland_scanner wayland-scanner
 
-PKGS="scenefx-0.2" "wlroots-0.19" wayland-server xkbcommon
+PKGS="scenefx-0.5" "wlroots-0.20" wayland-server xkbcommon
 CFLAGS_PKG_CONFIG!=$(PKG_CONFIG) --cflags $(PKGS)
 CFLAGS+=$(CFLAGS_PKG_CONFIG)
 LIBS!=$(PKG_CONFIG) --libs $(PKGS)
 
 all: tinywl
 
-# wayland-scanner is a tool which generates C headers and rigging for Wayland
-# protocols, which are specified in XML. wlroots requires you to rig these up
-# to your build system yourself and provide them in the include path.
-xdg-shell-protocol.h:
-	$(WAYLAND_SCANNER) server-header \
-		$(WAYLAND_PROTOCOLS)/stable/xdg-shell/xdg-shell.xml $@
-
-tinywl.o: tinywl.c xdg-shell-protocol.h
+tinywl.o: tinywl.c
 	$(CC) -c $< -g -Werror $(CFLAGS) -I. -DWLR_USE_UNSTABLE -o $@
 tinywl: tinywl.o
-	$(CC) $^ $> -g -Werror $(CFLAGS) $(LDFLAGS) $(LIBS) -o $@
+	$(CC) $^ -g -Werror $(CFLAGS) $(LDFLAGS) $(LIBS) -o $@
 
 clean:
-	rm -f tinywl tinywl.o xdg-shell-protocol.h
+	rm -f tinywl tinywl.o
 
 .PHONY: all clean
diff --git a/scenefx/tinywl/meson.build b/scenefx/tinywl/meson.build
index 4c3a1d0..295390f 100644
--- a/scenefx/tinywl/meson.build
+++ b/scenefx/tinywl/meson.build
@@ -1,5 +1,5 @@
 executable(
 	'tinywl',
-	['tinywl.c', protocols_client_header['xdg-shell']],
+	'tinywl.c',
 	dependencies: scenefx,
 )
diff --git a/scenefx/tinywl/tinywl.c b/scenefx/tinywl/tinywl.c
index c6e2a5d..2f29028 100644
--- a/scenefx/tinywl/tinywl.c
+++ b/scenefx/tinywl/tinywl.c
@@ -6,7 +6,6 @@
 #include <time.h>
 #include <scenefx/render/fx_renderer/fx_renderer.h>
 #include <scenefx/types/fx/clipped_region.h>
-#include <scenefx/types/fx/corner_location.h>
 #include <scenefx/types/wlr_scene.h>
 #include <unistd.h>
 #include <wayland-server-core.h>
@@ -68,6 +67,7 @@ struct tinywl_server {
 	struct wlr_seat *seat;
 	struct wl_listener new_input;
 	struct wl_listener request_cursor;
+	struct wl_listener pointer_focus_change;
 	struct wl_listener request_set_selection;
 	struct wl_list keyboards;
 	enum tinywl_cursor_mode cursor_mode;
@@ -107,6 +107,7 @@ struct tinywl_toplevel {
 
 	float opacity;
 	int corner_radius;
+	struct wlr_scene_blur *blur;
 	struct wlr_scene_shadow *shadow;
 	struct wlr_scene_rect *border;
 };
@@ -351,6 +352,18 @@ static void seat_request_cursor(struct wl_listener *listener, void *data) {
 	}
 }
 
+static void seat_pointer_focus_change(struct wl_listener *listener, void *data) {
+	struct tinywl_server *server = wl_container_of(
+			listener, server, pointer_focus_change);
+	/* This event is raised when the pointer focus is changed, including when the
+	 * client is closed. We set the cursor image to its default if target surface
+	 * is NULL */
+	struct wlr_seat_pointer_focus_change_event *event = data;
+	if (event->new_surface == NULL) {
+		wlr_cursor_set_xcursor(server->cursor, server->cursor_mgr, "default");
+	}
+}
+
 static void seat_request_set_selection(struct wl_listener *listener, void *data) {
 	/* This event is raised by the seat when a client wants to set the selection,
 	 * usually when the user copies something. wlroots allows compositors to
@@ -599,6 +612,7 @@ static void xdg_toplevel_commit(struct wl_listener *listener, void *data) {
 
 	struct wlr_box *geometry = &toplevel->xdg_toplevel->base->geometry;
 	wlr_scene_subsurface_tree_set_clip(&toplevel->xdg_scene_tree->node, geometry);
+	wlr_scene_blur_set_size(toplevel->blur, geometry->width, geometry->height);
 
 	int border_width = geometry->width + (BORDER_THICKNESS * 2);
 	int border_height = geometry->height + (BORDER_THICKNESS * 2);
@@ -608,8 +622,7 @@ static void xdg_toplevel_commit(struct wl_listener *listener, void *data) {
 
 	wlr_scene_rect_set_size(toplevel->border, border_width, border_height);
 	wlr_scene_rect_set_clipped_region(toplevel->border, (struct clipped_region) {
-			.corner_radius = toplevel->corner_radius,
-			.corners = CORNER_LOCATION_ALL,
+			.corners = corner_radii_all(toplevel->corner_radius),
 			.area = { BORDER_THICKNESS, BORDER_THICKNESS, geometry->width, geometry->height }
 	});
 
@@ -618,8 +631,7 @@ static void xdg_toplevel_commit(struct wl_listener *listener, void *data) {
 			border_width + (blur_sigma * 2),
 			border_height + (blur_sigma * 2));
 	wlr_scene_shadow_set_clipped_region(toplevel->shadow, (struct clipped_region) {
-			.corner_radius = toplevel->corner_radius + BORDER_THICKNESS,
-			.corners = CORNER_LOCATION_ALL,
+			.corners = corner_radii_all(toplevel->corner_radius + BORDER_THICKNESS),
 			.area = { blur_sigma, blur_sigma, border_width, border_height }
 	});
 }
@@ -653,8 +665,8 @@ static void output_configure_scene(struct wlr_scene_node *node,
 			wlr_scene_buffer_set_opacity(buffer, toplevel->opacity);
 
 			if (!wlr_subsurface_try_from_wlr_surface(xdg_surface->surface)) {
-				wlr_scene_buffer_set_corner_radius(
-						buffer, toplevel->corner_radius, CORNER_LOCATION_BOTTOM);
+				wlr_scene_buffer_set_corner_radii(
+						buffer, corner_radii_bottom(toplevel->corner_radius));
 			}
 		}
 	} else if (node->type == WLR_SCENE_NODE_TREE) {
@@ -720,7 +732,7 @@ static void server_new_output(struct wl_listener *listener, void *data) {
 	struct wlr_output *wlr_output = data;
 
 	/* Configures the output created by the backend to use our allocator
-	 * and our renderer. Must be done once, before commiting the output */
+	 * and our renderer. Must be done once, before committing the output */
 	wlr_output_init_render(wlr_output, server->allocator, server->renderer);
 
 	/* The output may be disabled, switch it on. */
@@ -803,12 +815,9 @@ static void iter_xdg_scene_buffers(struct wlr_scene_buffer *buffer, int sx,
 		wlr_scene_buffer_set_opacity(buffer, toplevel->opacity);
 
 		if (!wlr_subsurface_try_from_wlr_surface(xdg_surface->surface)) {
-			wlr_scene_buffer_set_corner_radius(buffer, toplevel->corner_radius,
-					CORNER_LOCATION_BOTTOM);
+			wlr_scene_buffer_set_corner_radii(buffer, corner_radii_bottom(toplevel->corner_radius));
 
-			wlr_scene_buffer_set_backdrop_blur(buffer, true);
-			wlr_scene_buffer_set_backdrop_blur_optimized(buffer, true);
-			wlr_scene_buffer_set_backdrop_blur_ignore_transparent(buffer, true);
+			wlr_scene_blur_set_transparency_mask_source(toplevel->blur, buffer);
 		}
 	}
 }
@@ -958,10 +967,13 @@ static void server_new_xdg_toplevel(struct wl_listener *listener, void *data) {
 	toplevel->opacity = 1;
 	toplevel->corner_radius = 20;
 
+	toplevel->blur = wlr_scene_blur_create(toplevel->scene_tree, 0, 0);
+	wlr_scene_blur_set_should_only_blur_bottom_layer(toplevel->blur, true);
+
 	toplevel->border = wlr_scene_rect_create(toplevel->scene_tree, 0, 0,
 			(float[4]){ 1.0f, 0.f, 0.f, 1.0f });
-	wlr_scene_rect_set_corner_radius(toplevel->border,
-			toplevel->corner_radius + BORDER_THICKNESS, CORNER_LOCATION_BOTTOM);
+	wlr_scene_rect_set_corner_radii(toplevel->border,
+			corner_radii_bottom(toplevel->corner_radius + BORDER_THICKNESS));
 	wlr_scene_node_set_position(&toplevel->border->node, -BORDER_THICKNESS, -BORDER_THICKNESS);
 
 	float blur_sigma = 20.0f;
@@ -974,6 +986,8 @@ static void server_new_xdg_toplevel(struct wl_listener *listener, void *data) {
 	wlr_scene_node_lower_to_bottom(&toplevel->border->node);
 	// Lower the shadow below the border
 	wlr_scene_node_lower_to_bottom(&toplevel->shadow->node);
+	// Lower the blur below the shadow
+	wlr_scene_node_lower_to_bottom(&toplevel->blur->node);
 
 	/* Listen to the various events it can emit */
 	toplevel->map.notify = xdg_toplevel_map;
@@ -1066,7 +1080,7 @@ int main(int argc, char *argv[]) {
 
 	struct tinywl_server server = {0};
 	/* The Wayland display is managed by libwayland. It handles accepting
-	 * clients from the Unix socket, manging Wayland globals, and so on. */
+	 * clients from the Unix socket, managing Wayland globals, and so on. */
 	server.wl_display = wl_display_create();
 	/* The backend is a wlroots feature which abstracts the underlying input and
 	 * output hardware. The autocreate option will choose the most suitable
@@ -1144,8 +1158,7 @@ int main(int argc, char *argv[]) {
 	struct wlr_scene_rect *rect = wlr_scene_rect_create(server.layers.toplevel_layer,
 			200, 200, top_rect_color);
 	wlr_scene_rect_set_clipped_region(rect, (struct clipped_region) {
-			.corner_radius = 12,
-			.corners = CORNER_LOCATION_TOP,
+			.corners = {12, 12, 0, 0},
 			.area = {
 				.x = 50,
 				.y = 50,
@@ -1215,6 +1228,9 @@ int main(int argc, char *argv[]) {
 	server.request_cursor.notify = seat_request_cursor;
 	wl_signal_add(&server.seat->events.request_set_cursor,
 			&server.request_cursor);
+	server.pointer_focus_change.notify = seat_pointer_focus_change;
+	wl_signal_add(&server.seat->pointer_state.events.focus_change,
+			&server.pointer_focus_change);
 	server.request_set_selection.notify = seat_request_set_selection;
 	wl_signal_add(&server.seat->events.request_set_selection,
 			&server.request_set_selection);
@@ -1266,6 +1282,7 @@ int main(int argc, char *argv[]) {
 
 	wl_list_remove(&server.new_input.link);
 	wl_list_remove(&server.request_cursor.link);
+	wl_list_remove(&server.pointer_focus_change.link);
 	wl_list_remove(&server.request_set_selection.link);
 
 	wl_list_remove(&server.new_output.link);
diff --git a/scenefx/types/fx/blur_data.c b/scenefx/types/fx/blur_data.c
index 423c02a..fcc8c74 100644
--- a/scenefx/types/fx/blur_data.c
+++ b/scenefx/types/fx/blur_data.c
@@ -25,3 +25,39 @@ bool blur_data_should_parameters_blur_effects(struct blur_data *blur_data) {
 int blur_data_calc_size(struct blur_data *blur_data) {
 	return pow(2, blur_data->num_passes + 1) * blur_data->radius;
 }
+
+static float lerp(float a, float b, float f) {
+	return (a * (1.0 - f)) + (b * f);
+}
+
+static void adjust_post_effects(struct blur_data *blur_data, float strength) {
+	blur_data->brightness = lerp(1.0, blur_data->brightness, strength);
+	blur_data->contrast = lerp(1.0, blur_data->contrast, strength);
+	blur_data->noise = lerp(0.0, blur_data->noise, strength);
+	blur_data->saturation = lerp(1.0, blur_data->saturation, strength);
+}
+
+struct blur_data blur_data_apply_strength(struct blur_data *ref_blur_data, float strength) {
+	struct blur_data blur_data = *ref_blur_data;
+
+	if (strength >= 1.0f) {
+		adjust_post_effects(&blur_data, 1.0f);
+		return blur_data;
+	} else if (strength <= 0.0f) {
+		adjust_post_effects(&blur_data, 0.0f);
+		blur_data.num_passes = 0;
+		blur_data.radius = 0.0f;
+		return blur_data;
+	}
+
+	// Calculate the new blur strength from the new alpha multiplied blur size
+	// Also make sure that the values are never larger than the initial values
+	int new_size = blur_data_calc_size(ref_blur_data) * strength;
+	blur_data.num_passes = fmax(0, fmin(ref_blur_data->num_passes, ceilf(log2(new_size / ref_blur_data->radius))));
+	blur_data.radius = fmax(0, fmin(ref_blur_data->radius, new_size / pow(2, blur_data.num_passes + 1)));
+
+	adjust_post_effects(&blur_data, strength);
+
+	return blur_data;
+}
+
diff --git a/scenefx/types/fx/clipped_region.c b/scenefx/types/fx/clipped_region.c
index b12b0d1..bc46a84 100644
--- a/scenefx/types/fx/clipped_region.c
+++ b/scenefx/types/fx/clipped_region.c
@@ -1,10 +1,71 @@
 #include "scenefx/types/fx/clipped_region.h"
-#include "scenefx/types/fx/corner_location.h"
+#include "types/fx/clipped_region.h"
+
+struct fx_corner_radii fx_corner_radii_extend(struct fx_corner_radii corners, int extend) {
+	return (struct fx_corner_radii) {
+		.top_left =  corners.top_left == 0 ? 0 : (corners.top_left + extend),
+		.top_right = corners.top_right == 0 ? 0 : (corners.top_right + extend),
+		.bottom_left = corners.bottom_left == 0 ? 0 : (corners.bottom_left + extend),
+		.bottom_right = corners.bottom_right == 0 ? 0 : (corners.bottom_right + extend),
+	};
+}
+
+void fx_corner_radii_transform(enum wl_output_transform transform,
+		struct fx_corner_radii *corners) {
+	if (transform & WL_OUTPUT_TRANSFORM_FLIPPED) {
+		*corners = (struct fx_corner_radii){
+			corners->top_right,
+			corners->top_left,
+			corners->bottom_left,
+			corners->bottom_right
+		};
+	}
+
+	unsigned int turns = transform & (WL_OUTPUT_TRANSFORM_90 | WL_OUTPUT_TRANSFORM_180 | WL_OUTPUT_TRANSFORM_270);
+	if (turns > 0) {
+		uint16_t points[4] = {corners->top_left, corners->top_right, corners->bottom_right, corners->bottom_left};
+		*corners = (struct fx_corner_radii){
+			points[turns % 4],
+			points[(turns + 1) % 4],
+			points[(turns + 2) % 4],
+			points[(turns + 3) % 4],
+		};
+	}
+}
+
+struct fx_corner_fradii fx_corner_radii_scale(struct fx_corner_radii corners, float scale) {
+	return (struct fx_corner_fradii) {
+	(float)corners.top_left * scale,
+	(float)corners.top_right * scale,
+	(float)corners.bottom_right * scale,
+	(float)corners.bottom_left * scale,
+	};
+}
+
+bool fx_corner_radii_eq(const struct fx_corner_radii lhs, const struct fx_corner_radii rhs) {
+	return lhs.top_left == rhs.top_left
+		&& lhs.top_right == rhs.top_right
+		&& lhs.bottom_right == rhs.bottom_right
+		&& lhs.bottom_left == rhs.bottom_left;
+}
+
+bool fx_corner_radii_is_empty(const struct fx_corner_radii* corners) {
+	return corners->top_left == 0
+		&& corners->top_right == 0
+		&& corners->bottom_right == 0
+		&& corners->bottom_left == 0;
+}
+
+bool fx_corner_fradii_is_empty(const struct fx_corner_fradii* corners) {
+	return corners->top_left == 0.0
+		&& corners->top_right == 0.0
+		&& corners->bottom_right == 0.0
+		&& corners->bottom_left == 0.0;
+}
 
 struct clipped_region clipped_region_get_default(void) {
 	return (struct clipped_region) {
-		.corner_radius = 0,
-		.corners = CORNER_LOCATION_NONE,
+		.corners = {0},
 		.area = (struct wlr_box) {0},
 	};
 }
diff --git a/scenefx/types/fx/corner_location.c b/scenefx/types/fx/corner_location.c
deleted file mode 100644
index e7069f2..0000000
--- a/scenefx/types/fx/corner_location.c
+++ /dev/null
@@ -1,76 +0,0 @@
-#include "scenefx/types/fx/corner_location.h"
-
-// Maps the corners to its correct transformed corner
-static const enum corner_location corner_transform[8][CORNER_LOCATION_LAST] = {
-	[WL_OUTPUT_TRANSFORM_NORMAL] = {
-		[CORNER_LOCATION_TOP_LEFT] = CORNER_LOCATION_TOP_LEFT,
-		[CORNER_LOCATION_TOP_RIGHT] = CORNER_LOCATION_TOP_RIGHT,
-		[CORNER_LOCATION_BOTTOM_RIGHT] = CORNER_LOCATION_BOTTOM_RIGHT,
-		[CORNER_LOCATION_BOTTOM_LEFT] = CORNER_LOCATION_BOTTOM_LEFT,
-	},
-	[WL_OUTPUT_TRANSFORM_90] = {
-		[CORNER_LOCATION_TOP_LEFT] = CORNER_LOCATION_BOTTOM_LEFT,
-		[CORNER_LOCATION_TOP_RIGHT] = CORNER_LOCATION_TOP_LEFT,
-		[CORNER_LOCATION_BOTTOM_RIGHT] = CORNER_LOCATION_TOP_RIGHT,
-		[CORNER_LOCATION_BOTTOM_LEFT] = CORNER_LOCATION_BOTTOM_RIGHT,
-	},
-	[WL_OUTPUT_TRANSFORM_180] = {
-		[CORNER_LOCATION_TOP_LEFT] = CORNER_LOCATION_BOTTOM_RIGHT,
-		[CORNER_LOCATION_TOP_RIGHT] = CORNER_LOCATION_BOTTOM_LEFT,
-		[CORNER_LOCATION_BOTTOM_RIGHT] = CORNER_LOCATION_TOP_LEFT,
-		[CORNER_LOCATION_BOTTOM_LEFT] = CORNER_LOCATION_TOP_RIGHT,
-	},
-	[WL_OUTPUT_TRANSFORM_270] = {
-		[CORNER_LOCATION_TOP_LEFT] = CORNER_LOCATION_TOP_RIGHT,
-		[CORNER_LOCATION_TOP_RIGHT] = CORNER_LOCATION_BOTTOM_RIGHT,
-		[CORNER_LOCATION_BOTTOM_RIGHT] = CORNER_LOCATION_BOTTOM_LEFT,
-		[CORNER_LOCATION_BOTTOM_LEFT] = CORNER_LOCATION_TOP_LEFT,
-	},
-	[WL_OUTPUT_TRANSFORM_FLIPPED] = {
-		[CORNER_LOCATION_TOP_LEFT] = CORNER_LOCATION_TOP_RIGHT,
-		[CORNER_LOCATION_TOP_RIGHT] = CORNER_LOCATION_TOP_LEFT,
-		[CORNER_LOCATION_BOTTOM_RIGHT] = CORNER_LOCATION_BOTTOM_LEFT,
-		[CORNER_LOCATION_BOTTOM_LEFT] = CORNER_LOCATION_BOTTOM_RIGHT,
-	},
-	[WL_OUTPUT_TRANSFORM_FLIPPED_90] = {
-		[CORNER_LOCATION_TOP_LEFT] = CORNER_LOCATION_TOP_LEFT,
-		[CORNER_LOCATION_TOP_RIGHT] = CORNER_LOCATION_BOTTOM_LEFT,
-		[CORNER_LOCATION_BOTTOM_RIGHT] = CORNER_LOCATION_BOTTOM_RIGHT,
-		[CORNER_LOCATION_BOTTOM_LEFT] = CORNER_LOCATION_TOP_RIGHT,
-	},
-	[WL_OUTPUT_TRANSFORM_FLIPPED_180] = {
-		[CORNER_LOCATION_TOP_LEFT] = CORNER_LOCATION_BOTTOM_LEFT,
-		[CORNER_LOCATION_TOP_RIGHT] = CORNER_LOCATION_BOTTOM_RIGHT,
-		[CORNER_LOCATION_BOTTOM_RIGHT] = CORNER_LOCATION_TOP_RIGHT,
-		[CORNER_LOCATION_BOTTOM_LEFT] = CORNER_LOCATION_TOP_LEFT,
-	},
-	[WL_OUTPUT_TRANSFORM_FLIPPED_270] = {
-		[CORNER_LOCATION_TOP_LEFT] = CORNER_LOCATION_BOTTOM_RIGHT,
-		[CORNER_LOCATION_TOP_RIGHT] = CORNER_LOCATION_TOP_RIGHT,
-		[CORNER_LOCATION_BOTTOM_RIGHT] = CORNER_LOCATION_TOP_LEFT,
-		[CORNER_LOCATION_BOTTOM_LEFT] = CORNER_LOCATION_BOTTOM_LEFT,
-	},
-};
-
-void corner_location_transform(enum wl_output_transform transform,
-		enum corner_location *corners) {
-	enum corner_location result = CORNER_LOCATION_NONE;
-
-	int bitmask = 1 << (CORNER_LOCATION_COUNT - 1);
-	int mask = 1;
-	while (bitmask) {
-		switch (*corners & mask) {
-		case CORNER_LOCATION_NONE:
-		case CORNER_LOCATION_ALL:
-			break;
-		default:
-			result |= corner_transform[transform][*corners & mask];
-			break;
-		}
-
-		bitmask &= ~mask;
-		mask <<= 1;
-	}
-
-	*corners = result;
-}
diff --git a/scenefx/types/fx/meson.build b/scenefx/types/fx/meson.build
index 749d01d..e22e4a8 100644
--- a/scenefx/types/fx/meson.build
+++ b/scenefx/types/fx/meson.build
@@ -1,5 +1,4 @@
 scenefx_files += files(
 	'blur_data.c',
-	'corner_location.c',
 	'clipped_region.c',
 )
diff --git a/scenefx/types/meson.build b/scenefx/types/meson.build
index a7e855b..96aa83d 100644
--- a/scenefx/types/meson.build
+++ b/scenefx/types/meson.build
@@ -1,6 +1,7 @@
 scenefx_files += files(
 	'scene/wlr_scene.c',
-	'output/wlr_output.c',
+	'scene/linked_node.c',
+	'output/output.c',
 )
 
 subdir('fx')
diff --git a/scenefx/types/output/wlr_output.c b/scenefx/types/output/output.c
similarity index 67%
rename from scenefx/types/output/wlr_output.c
rename to scenefx/types/output/output.c
index 59c9b31..17a7e3b 100644
--- a/scenefx/types/output/wlr_output.c
+++ b/scenefx/types/output/output.c
@@ -21,3 +21,11 @@ void output_pending_resolution(struct wlr_output *output,
 		*height = output->height;
 	}
 }
+
+const struct wlr_output_image_description *output_pending_image_description(
+		struct wlr_output *output, const struct wlr_output_state *state) {
+	if (state->committed & WLR_OUTPUT_STATE_IMAGE_DESCRIPTION) {
+		return state->image_description;
+	}
+	return output->image_description;
+}
diff --git a/scenefx/types/scene/linked_node.c b/scenefx/types/scene/linked_node.c
new file mode 100644
index 0000000..225d549
--- /dev/null
+++ b/scenefx/types/scene/linked_node.c
@@ -0,0 +1,78 @@
+#include <assert.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <wlr/util/log.h>
+
+#include "scenefx/types/linked_node.h"
+
+struct link {
+    struct linked_node *node_1;
+    struct linked_node *node_2;
+};
+
+static struct link *link_init(struct linked_node *node_1,
+        struct linked_node *node_2) {
+    struct link *link = calloc(1, sizeof(*link));
+    if (!link) {
+        wlr_log(WLR_ERROR, "Failed to allocate link!");
+        return NULL;
+    }
+
+    link->node_1 = node_1;
+    link->node_2 = node_2;
+
+    return link;
+}
+
+//
+// Linked Node
+//
+
+bool linked_nodes_are_linked(struct linked_node *node_1,
+        struct linked_node *node_2) {
+    return node_1->link != NULL && node_2->link != NULL
+        && node_1->link == node_2->link;
+}
+
+void linked_node_init_link(struct linked_node *node_1,
+        struct linked_node *node_2) {
+    if (node_1->link || node_2->link) {
+        assert(linked_nodes_are_linked(node_1, node_2));
+        return;
+    }
+
+    struct link *link = link_init(node_1, node_2);
+    node_1->link = link;
+    node_2->link = link;
+}
+
+struct linked_node *linked_nodes_get_sibling(struct linked_node *node) {
+    if (node->link == NULL) {
+        return NULL;
+    }
+    assert(node == node->link->node_1 || node == node->link->node_2);
+
+    struct link *link = node->link;
+    if (link->node_1 == node) {
+        return link->node_2;
+    }
+    return link->node_1;
+}
+
+void linked_node_unlink(struct linked_node *node_1,
+        struct linked_node *node_2) {
+    assert(linked_nodes_are_linked(node_1, node_2));
+
+    struct link *link = node_1->link;
+    node_1->link = NULL;
+    node_2->link = NULL;
+    free(link);
+}
+
+void linked_node_destroy(struct linked_node *node) {
+    if (node->link == NULL) {
+        return;
+    }
+    assert(node == node->link->node_1 || node == node->link->node_2);
+    linked_node_unlink(node->link->node_1, node->link->node_2);
+}
diff --git a/scenefx/types/scene/wlr_scene.c b/scenefx/types/scene/wlr_scene.c
index b4c1b63..51c90a5 100644
--- a/scenefx/types/scene/wlr_scene.c
+++ b/scenefx/types/scene/wlr_scene.c
@@ -2,33 +2,34 @@
 #include <pixman.h>
 #include <stdio.h>
 #include <stdlib.h>
-#include <scenefx/types/wlr_scene.h>
 #include <string.h>
 #include <wlr/backend.h>
-#include <wlr/render/gles2.h>
-#include <wlr/render/drm_syncobj.h>
 #include <wlr/render/swapchain.h>
+#include <wlr/render/drm_syncobj.h>
+#include <wlr/render/wlr_renderer.h>
+#include <wlr/types/wlr_color_management_v1.h>
 #include <wlr/types/wlr_compositor.h>
 #include <wlr/types/wlr_damage_ring.h>
 #include <wlr/types/wlr_gamma_control_v1.h>
+#include <wlr/types/wlr_linux_dmabuf_v1.h>
 #include <wlr/types/wlr_presentation_time.h>
-#include <wlr/types/wlr_subcompositor.h>
-#include <wlr/types/wlr_xdg_shell.h>
 #include <wlr/util/log.h>
 #include <wlr/util/region.h>
 #include <wlr/util/transform.h>
 
-#include "scenefx/render/fx_renderer/fx_effect_framebuffers.h"
-#include "scenefx/render/fx_renderer/fx_renderer.h"
+#include "render/color.h"
+#include "render/tracy.h"
+#include "scenefx/render/fx_renderer/fx_offscreen_buffers.h"
 #include "scenefx/render/pass.h"
+#include "scenefx/types/fx/blur_data.h"
 #include "scenefx/types/fx/clipped_region.h"
-#include "scenefx/types/fx/corner_location.h"
+#include "scenefx/types/wlr_scene.h"
+#include "types/fx/clipped_region.h"
 #include "types/wlr_output.h"
 #include "types/wlr_scene.h"
 #include "util/array.h"
 #include "util/env.h"
 #include "util/time.h"
-#include "wlr/util/box.h"
 
 #include <wlr/config.h>
 
@@ -72,6 +73,12 @@ struct wlr_scene_shadow *wlr_scene_shadow_from_node(struct wlr_scene_node *node)
 	return shadow;
 }
 
+struct wlr_scene_blur *wlr_scene_blur_from_node(struct wlr_scene_node *node) {
+	assert(node->type == WLR_SCENE_NODE_BLUR);
+	struct wlr_scene_blur *blur = wl_container_of(node, blur, node);
+	return blur;
+}
+
 struct wlr_scene *scene_node_get_root(struct wlr_scene_node *node) {
 	struct wlr_scene_tree *tree;
 	if (node->type == WLR_SCENE_NODE_TREE) {
@@ -150,6 +157,7 @@ void wlr_scene_node_destroy(struct wlr_scene_node *node) {
 		scene_buffer_set_texture(scene_buffer, NULL);
 		pixman_region32_fini(&scene_buffer->opaque_region);
 		wlr_drm_syncobj_timeline_unref(scene_buffer->wait_timeline);
+		linked_node_destroy(&scene_buffer->blur);
 
 		assert(wl_list_empty(&scene_buffer->events.output_leave.listener_list));
 		assert(wl_list_empty(&scene_buffer->events.output_enter.listener_list));
@@ -178,6 +186,9 @@ void wlr_scene_node_destroy(struct wlr_scene_node *node) {
 				&scene_tree->children, link) {
 			wlr_scene_node_destroy(child);
 		}
+	} else if (node->type == WLR_SCENE_NODE_BLUR) {
+		struct wlr_scene_blur *blur = wlr_scene_blur_from_node(node);
+		linked_node_destroy(&blur->transparency_mask_source);
 	}
 
 	assert(wl_list_empty(&node->events.destroy.listener_list));
@@ -207,6 +218,8 @@ struct wlr_scene *wlr_scene_create(void) {
 	wl_list_init(&scene->gamma_control_manager_v1_destroy.link);
 	wl_list_init(&scene->gamma_control_manager_v1_set_gamma.link);
 
+	scene->restack_xwayland_surfaces = true;
+
 	const char *debug_damage_options[] = {
 		"none",
 		"rerender",
@@ -236,8 +249,6 @@ struct wlr_scene_tree *wlr_scene_tree_create(struct wlr_scene_tree *parent) {
 	return tree;
 }
 
-static void scene_node_get_size(struct wlr_scene_node *node, int *lx, int *ly);
-
 typedef bool (*scene_node_box_iterator_func_t)(struct wlr_scene_node *node,
 	int sx, int sy, void *data);
 
@@ -257,10 +268,11 @@ static bool _scene_nodes_in_box(struct wlr_scene_node *node, struct wlr_box *box
 			}
 		}
 		break;
-	case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
 	case WLR_SCENE_NODE_RECT:
+	case WLR_SCENE_NODE_BUFFER:
 	case WLR_SCENE_NODE_SHADOW:
-	case WLR_SCENE_NODE_BUFFER:;
+	case WLR_SCENE_NODE_OPTIMIZED_BLUR:
+	case WLR_SCENE_NODE_BLUR:;
 		struct wlr_box node_box = { .x = lx, .y = ly };
 		scene_node_get_size(node, &node_box.width, &node_box.height);
 
@@ -282,6 +294,32 @@ static bool scene_nodes_in_box(struct wlr_scene_node *node, struct wlr_box *box,
 	return _scene_nodes_in_box(node, box, iterator, user_data, x, y);
 }
 
+static pixman_region32_t create_corner_location_region(struct fx_corner_radii corners, int x, int y, int width, int height) {
+	pixman_region32_t corner_region;
+	pixman_region32_init(&corner_region);
+	if (corners.top_left) {
+		pixman_region32_union_rect(&corner_region, &corner_region, x, y, corners.top_left, corners.top_left);
+	}
+
+	if (corners.top_right) {
+		pixman_region32_union_rect(&corner_region, &corner_region, x + (width - corners.top_right), y,
+			corners.top_right, corners.top_right);
+	}
+
+	if (corners.bottom_left) {
+		pixman_region32_union_rect(&corner_region, &corner_region, x, y + (height - corners.bottom_left),
+			corners.bottom_left, corners.bottom_left);
+	}
+
+	if (corners.bottom_right) {
+		pixman_region32_union_rect(&corner_region, &corner_region,
+			x + (width - corners.bottom_right), y + (height - corners.bottom_right),
+			corners.bottom_right, corners.bottom_right);
+	}
+
+	return corner_region;
+}
+
 static void scene_node_opaque_region(struct wlr_scene_node *node, int x, int y,
 		pixman_region32_t *opaque) {
 	int width, height;
@@ -289,28 +327,29 @@ static void scene_node_opaque_region(struct wlr_scene_node *node, int x, int y,
 
 	if (node->type == WLR_SCENE_NODE_RECT) {
 		struct wlr_scene_rect *scene_rect = wlr_scene_rect_from_node(node);
-		if (scene_rect->corner_radius > 0) {
-			// TODO: this is incorrect
-			return;
-		}
 		if (scene_rect->color[3] != 1) {
 			return;
 		}
-		if (!wlr_box_empty(&scene_rect->clipped_region.area)) {
-			pixman_region32_fini(opaque);
-			pixman_region32_init_rect(opaque, x, y, width, height);
 
-			// Subtract the clipped region from a otherwise fully opaque rect
+		pixman_region32_fini(opaque);
+		pixman_region32_init_rect(opaque, x, y, width, height);
+
+		// subtract corners from opaque region
+		if (!fx_corner_radii_is_empty(&scene_rect->corners)) {
+			pixman_region32_t corners = create_corner_location_region(scene_rect->corners, x, y, width, height);
+			pixman_region32_subtract(opaque, opaque, &corners);
+			pixman_region32_fini(&corners);
+		}
+
+		// subtract clipped area from opaque region
+		if (!wlr_box_empty(&scene_rect->clipped_region.area)) {
 			struct wlr_box *clipped = &scene_rect->clipped_region.area;
 			pixman_region32_t clipped_region;
 			pixman_region32_init_rect(&clipped_region, clipped->x + x, clipped->y + y,
 					clipped->width, clipped->height);
 			pixman_region32_subtract(opaque, opaque, &clipped_region);
 			pixman_region32_fini(&clipped_region);
-			return;
 		}
-	} else if (node->type == WLR_SCENE_NODE_SHADOW) {
-		// TODO: test & handle case of blur sigma = 0 and color[3] = 1?
 		return;
 	} else if (node->type == WLR_SCENE_NODE_BUFFER) {
 		struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_from_node(node);
@@ -323,18 +362,27 @@ static void scene_node_opaque_region(struct wlr_scene_node *node, int x, int y,
 			return;
 		}
 
-		if (scene_buffer->corner_radius > 0) {
-			// TODO: this is incorrect
-			return;
-		}
-
 		if (!scene_buffer->buffer_is_opaque) {
 			pixman_region32_copy(opaque, &scene_buffer->opaque_region);
 			pixman_region32_intersect_rect(opaque, opaque, 0, 0, width, height);
 			pixman_region32_translate(opaque, x, y);
-			return;
+		} else {
+			pixman_region32_fini(opaque);
+			pixman_region32_init_rect(opaque, x, y, width, height);
+		}
+
+		// subtract the corners from the opaque region
+		if (!fx_corner_radii_is_empty(&scene_buffer->corners)) {
+			pixman_region32_t corners = create_corner_location_region(scene_buffer->corners, x, y, width, height);
+			pixman_region32_subtract(opaque, opaque, &corners);
+			pixman_region32_fini(&corners);
 		}
-	} else if (node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
+
+		return;
+	} else if (node->type == WLR_SCENE_NODE_SHADOW) {
+		// TODO: test & handle case of blur sigma = 0 and color[3] = 1?
+		return;
+	} else if (node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR || node->type == WLR_SCENE_NODE_BLUR) {
 		// Always transparent
 		return;
 	}
@@ -345,19 +393,18 @@ static void scene_node_opaque_region(struct wlr_scene_node *node, int x, int y,
 
 struct scene_update_data {
 	pixman_region32_t *visible;
-	pixman_region32_t *update_region;
+	const pixman_region32_t *update_region;
 	struct wlr_box update_box;
 	struct wl_list *outputs;
 	bool calculate_visibility;
+	bool restack_xwayland_surfaces;
 
 #if WLR_HAS_XWAYLAND
 	struct wlr_xwayland_surface *restack_above;
 #endif
-
-	bool optimized_blur_dirty;
 };
 
-static uint32_t region_area(pixman_region32_t *region) {
+static uint32_t region_area(const pixman_region32_t *region) {
 	uint32_t area = 0;
 
 	int nrects;
@@ -385,10 +432,8 @@ struct render_data {
 
 	struct wlr_scene_output *output;
 
-	struct fx_gles_render_pass *render_pass;
+	struct wlr_render_pass *render_pass;
 	pixman_region32_t damage;
-
-	bool has_blur;
 };
 
 static void logical_to_buffer_coords(pixman_region32_t *region, const struct render_data *data,
@@ -451,7 +496,7 @@ static void scene_output_damage_whole(struct wlr_scene_output *scene_output) {
 	pixman_region32_fini(&damage);
 }
 
-static void scene_damage_outputs(struct wlr_scene *scene, pixman_region32_t *damage) {
+static void scene_damage_outputs(struct wlr_scene *scene, const pixman_region32_t *damage) {
 	if (pixman_region32_empty(damage)) {
 		return;
 	}
@@ -486,46 +531,51 @@ static void update_node_update_outputs(struct wlr_scene_node *node,
 	size_t count = 0;
 	uint64_t active_outputs = 0;
 
-	// let's update the outputs in two steps:
-	//  - the primary outputs
-	//  - the enter/leave signals
-	// This ensures that the enter/leave signals can rely on the primary output
-	// to have a reasonable value. Otherwise, they may get a value that's in
-	// the middle of a calculation.
-	struct wlr_scene_output *scene_output;
-	wl_list_for_each(scene_output, outputs, link) {
-		if (scene_output == ignore) {
-			continue;
-		}
-
-		if (!scene_output->output->enabled) {
-			continue;
-		}
+	if (!pixman_region32_empty(&node->visible)) {
+		uint32_t visible_area = region_area(&node->visible);
+
+		// let's update the outputs in two steps:
+		//  - the primary outputs
+		//  - the enter/leave signals
+		// This ensures that the enter/leave signals can rely on the primary output
+		// to have a reasonable value. Otherwise, they may get a value that's in
+		// the middle of a calculation.
+		struct wlr_scene_output *scene_output;
+		wl_list_for_each(scene_output, outputs, link) {
+			if (scene_output == ignore) {
+				continue;
+			}
 
-		struct wlr_box output_box = {
-			.x = scene_output->x,
-			.y = scene_output->y,
-		};
-		wlr_output_effective_resolution(scene_output->output,
-			&output_box.width, &output_box.height);
+			if (!scene_output->output->enabled) {
+				continue;
+			}
 
-		pixman_region32_t intersection;
-		pixman_region32_init(&intersection);
-		pixman_region32_intersect_rect(&intersection, &node->visible,
-			output_box.x, output_box.y, output_box.width, output_box.height);
+			struct wlr_box output_box = {
+				.x = scene_output->x,
+				.y = scene_output->y,
+			};
+			wlr_output_effective_resolution(scene_output->output,
+				&output_box.width, &output_box.height);
 
-		if (!pixman_region32_empty(&intersection)) {
+			pixman_region32_t intersection;
+			pixman_region32_init(&intersection);
+			pixman_region32_intersect_rect(&intersection, &node->visible,
+				output_box.x, output_box.y, output_box.width, output_box.height);
 			uint32_t overlap = region_area(&intersection);
-			if (overlap >= largest_overlap) {
-				largest_overlap = overlap;
-				scene_buffer->primary_output = scene_output;
-			}
+			pixman_region32_fini(&intersection);
+
+			// If the overlap accounts for less than 10% of the visible node area,
+			// ignore this output
+			if (overlap >= 0.1 * visible_area) {
+				if (overlap >= largest_overlap) {
+					largest_overlap = overlap;
+					scene_buffer->primary_output = scene_output;
+				}
 
-			active_outputs |= 1ull << scene_output->index;
-			count++;
+				active_outputs |= 1ull << scene_output->index;
+				count++;
+			}
 		}
-
-		pixman_region32_fini(&intersection);
 	}
 
 	if (old_primary_output != scene_buffer->primary_output) {
@@ -536,6 +586,7 @@ static void update_node_update_outputs(struct wlr_scene_node *node,
 	uint64_t old_active = scene_buffer->active_outputs;
 	scene_buffer->active_outputs = active_outputs;
 
+	struct wlr_scene_output *scene_output;
 	wl_list_for_each(scene_output, outputs, link) {
 		uint64_t mask = 1ull << scene_output->index;
 		bool intersects = active_outputs & mask;
@@ -620,25 +671,6 @@ static void restack_xwayland_surface(struct wlr_scene_node *node,
 
 	data->restack_above = xwayland_surface;
 }
-
-static void restack_xwayland_surface_below(struct wlr_scene_node *node) {
-	if (node->type == WLR_SCENE_NODE_TREE) {
-		struct wlr_scene_tree *scene_tree = wlr_scene_tree_from_node(node);
-		struct wlr_scene_node *child;
-		wl_list_for_each(child, &scene_tree->children, link) {
-			restack_xwayland_surface_below(child);
-		}
-		return;
-	}
-
-	struct wlr_xwayland_surface *xwayland_surface =
-		scene_node_try_get_managed_xwayland_surface(node);
-	if (!xwayland_surface) {
-		return;
-	}
-
-	wlr_xwayland_surface_restack(xwayland_surface, NULL, XCB_STACK_MODE_BELOW);
-}
 #endif
 
 static bool scene_node_update_iterator(struct wlr_scene_node *node,
@@ -648,9 +680,9 @@ static bool scene_node_update_iterator(struct wlr_scene_node *node,
 	if (node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
 		struct wlr_scene_optimized_blur *scene_blur = wlr_scene_optimized_blur_from_node(node);
 		if (scene_blur->dirty) {
-			data->optimized_blur_dirty = true;
-			// Restore the visible region back to default for the nodes below
-			// the optimized blur node
+			// Restore the visible region back to default, without any opaque
+			// regions. This ensures that all nodes below are fully re-rendered
+			// and not culled by above nodes.
 			pixman_region32_clear(data->visible);
 			pixman_region32_copy(data->visible, data->update_region);
 		}
@@ -664,7 +696,7 @@ static bool scene_node_update_iterator(struct wlr_scene_node *node,
 	pixman_region32_intersect_rect(&node->visible, &node->visible,
 		lx, ly, box.width, box.height);
 
-	if (data->calculate_visibility && !data->optimized_blur_dirty) {
+	if (data->calculate_visibility) {
 		pixman_region32_t opaque;
 		pixman_region32_init(&opaque);
 		scene_node_opaque_region(node, lx, ly, &opaque);
@@ -674,7 +706,9 @@ static bool scene_node_update_iterator(struct wlr_scene_node *node,
 
 	update_node_update_outputs(node, data->outputs, NULL, NULL);
 #if WLR_HAS_XWAYLAND
-	restack_xwayland_surface(node, &box, data);
+	if (data->restack_xwayland_surfaces) {
+		restack_xwayland_surface(node, &box, data);
+	}
 #endif
 
 	return false;
@@ -719,7 +753,7 @@ static void scene_node_bounds(struct wlr_scene_node *node,
 }
 
 static void scene_update_region(struct wlr_scene *scene,
-		pixman_region32_t *update_region) {
+		const pixman_region32_t *update_region) {
 	pixman_region32_t visible;
 	pixman_region32_init(&visible);
 	pixman_region32_copy(&visible, update_region);
@@ -736,7 +770,7 @@ static void scene_update_region(struct wlr_scene *scene,
 		},
 		.outputs = &scene->outputs,
 		.calculate_visibility = scene->calculate_visibility,
-		.optimized_blur_dirty = false,
+		.restack_xwayland_surfaces = scene->restack_xwayland_surfaces,
 	};
 
 	// update node visibility and output enter/leave events
@@ -745,16 +779,62 @@ static void scene_update_region(struct wlr_scene *scene,
 	pixman_region32_fini(&visible);
 }
 
+static void scene_node_cleanup_when_disabled(struct wlr_scene_node *node,
+		bool xwayland_restack, struct wl_list *outputs) {
+	if (node->type == WLR_SCENE_NODE_TREE) {
+		struct wlr_scene_tree *scene_tree = wlr_scene_tree_from_node(node);
+		struct wlr_scene_node *child;
+		wl_list_for_each(child, &scene_tree->children, link) {
+			if (!child->enabled) {
+				continue;
+			}
+
+			scene_node_cleanup_when_disabled(child, xwayland_restack, outputs);
+		}
+		return;
+	}
+
+	pixman_region32_clear(&node->visible);
+	update_node_update_outputs(node, outputs, NULL, NULL);
+
+#if WLR_HAS_XWAYLAND
+	if (xwayland_restack) {
+		struct wlr_xwayland_surface *xwayland_surface =
+			scene_node_try_get_managed_xwayland_surface(node);
+		if (!xwayland_surface) {
+			return;
+		}
+
+		wlr_xwayland_surface_restack(xwayland_surface, NULL, XCB_STACK_MODE_BELOW);
+	}
+#endif
+}
+
+/**
+ * Updates the nodes visibility, xwayland restacking, send leave/enter events
+ * and damages the screen. The damage region is used to not only damage the
+ * screen, but to direct the update logic to only update certain parts of the
+ * screen and not the whole thing. If a NULL damage is given, the damage is
+ * assumed to be the previous nodes visibility.
+ *
+ * Currently, the only usage for an explicit damage is for update scenarios where
+ * the scene node might be enabled/disabled. If the scene node is disabled, the
+ * update logic will ignore the node. This is normally desirable as most update
+ * scenarios like updating the color or whatever. However, it's not what we want
+ * when disabling the node. Note that reparenting the node could lead to the node
+ * being reparented to a disabled super tree.
+ */
 static void scene_node_update(struct wlr_scene_node *node,
 		pixman_region32_t *damage) {
 	struct wlr_scene *scene = scene_node_get_root(node);
 
 	int x, y;
 	if (!wlr_scene_node_coords(node, &x, &y)) {
-#if WLR_HAS_XWAYLAND
-		restack_xwayland_surface_below(node);
-#endif
+		// We assume explicit damage on a disabled tree means the node was just
+		// disabled.
 		if (damage) {
+			scene_node_cleanup_when_disabled(node, scene->restack_xwayland_surfaces, &scene->outputs);
+
 			scene_update_region(scene, damage);
 			scene_damage_outputs(scene, damage);
 			pixman_region32_fini(damage);
@@ -797,13 +877,9 @@ struct wlr_scene_rect *wlr_scene_rect_create(struct wlr_scene_tree *parent,
 	scene_rect->width = width;
 	scene_rect->height = height;
 	memcpy(scene_rect->color, color, sizeof(scene_rect->color));
-	scene_rect->corner_radius = 0;
-	scene_rect->corners = CORNER_LOCATION_NONE;
+	scene_rect->corners = (struct fx_corner_radii){0};
 	scene_rect->accepts_input = true;
 	scene_rect->clipped_region = clipped_region_get_default();
-	scene_rect->backdrop_blur = false;
-	scene_rect->backdrop_blur_optimized = false;
-	scene_rect->fade_inset = 0;
 
 	scene_node_update(&scene_rect->node, NULL);
 
@@ -831,28 +907,11 @@ void wlr_scene_rect_set_color(struct wlr_scene_rect *rect, const float color[sta
 	scene_node_update(&rect->node, NULL);
 }
 
-void wlr_scene_rect_set_backdrop_blur(struct wlr_scene_rect *rect,
-		bool enabled) {
-	if (rect->backdrop_blur == enabled) {
-		return;
-	}
-	rect->backdrop_blur = enabled;
-	scene_node_update(&rect->node, NULL);
-}
-
-void wlr_scene_rect_set_backdrop_blur_optimized(struct wlr_scene_rect *rect,
-		bool enabled) {
-	if (rect->backdrop_blur_optimized == enabled) {
-		return;
-	}
-	rect->backdrop_blur_optimized = enabled;
-	scene_node_update(&rect->node, NULL);
-}
-
 void wlr_scene_rect_set_fade_inset(struct wlr_scene_rect *rect, int fade_inset) {
 	if (rect->fade_inset == fade_inset) {
 		return;
 	}
+
 	rect->fade_inset = fade_inset;
 	scene_node_update(&rect->node, NULL);
 }
@@ -925,29 +984,26 @@ static void scene_buffer_set_wait_timeline(struct wlr_scene_buffer *scene_buffer
 	}
 }
 
-void wlr_scene_rect_set_corner_radius(struct wlr_scene_rect *rect, int corner_radius,
-		enum corner_location corners) {
-	if (rect->corner_radius == corner_radius && rect->corners == corners) {
+inline void wlr_scene_rect_set_corner_radius(struct wlr_scene_rect *rect, int corner_radius) {
+	wlr_scene_rect_set_corner_radii(rect, corner_radii_all(corner_radius));
+}
+
+void wlr_scene_rect_set_corner_radii(struct wlr_scene_rect *rect, struct fx_corner_radii corners) {
+	if (fx_corner_radii_eq(rect->corners, corners)) {
 		return;
 	}
 
-	rect->corner_radius = corner_radius;
 	rect->corners = corners;
 	scene_node_update(&rect->node, NULL);
 }
 
 void wlr_scene_rect_set_clipped_region(struct wlr_scene_rect *rect,
 		struct clipped_region clipped_region) {
-	if (rect->clipped_region.corner_radius == clipped_region.corner_radius &&
-			rect->clipped_region.corners == clipped_region.corners &&
+	if (fx_corner_radii_eq(rect->clipped_region.corners, clipped_region.corners) &&
 			wlr_box_equal(&rect->clipped_region.area, &clipped_region.area)) {
 		return;
 	}
 
-	if (clipped_region.corner_radius && clipped_region.corners == CORNER_LOCATION_NONE) {
-		wlr_log(WLR_ERROR, "Applying corner radius without specifying which"
-				" corners to round for rect: %p", rect);
-	}
 	rect->clipped_region = clipped_region;
 	scene_node_update(&rect->node, NULL);
 }
@@ -1013,16 +1069,11 @@ void wlr_scene_shadow_set_color(struct wlr_scene_shadow *shadow, const float col
 
 void wlr_scene_shadow_set_clipped_region(struct wlr_scene_shadow *shadow,
 		struct clipped_region clipped_region) {
-	if (shadow->clipped_region.corner_radius == clipped_region.corner_radius &&
-			shadow->clipped_region.corners == clipped_region.corners &&
+	if (fx_corner_radii_eq(shadow->clipped_region.corners, clipped_region.corners) &&
 			wlr_box_equal(&shadow->clipped_region.area, &clipped_region.area)) {
 		return;
 	}
 
-	if (clipped_region.corner_radius && clipped_region.corners == CORNER_LOCATION_NONE) {
-		wlr_log(WLR_ERROR, "Applying corner radius without specifying which"
-				" corners to round for shadow: %p", shadow);
-	}
 	shadow->clipped_region = clipped_region;
 	scene_node_update(&shadow->node, NULL);
 }
@@ -1040,6 +1091,123 @@ static void mark_all_optimized_blur_nodes_dirty(struct wlr_scene_node *node) {
 	}
 }
 
+struct wlr_scene_blur *wlr_scene_blur_create(struct wlr_scene_tree *parent,
+	   int width, int height) {
+	struct wlr_scene_blur *blur = calloc(1, sizeof(*blur));
+	if (blur == NULL) {
+		return NULL;
+	}
+	assert(parent);
+	scene_node_init(&blur->node, WLR_SCENE_NODE_BLUR, parent);
+
+	blur->alpha = 1.0f;
+	blur->strength = 1.0f;
+	blur->clipped_region = (struct clipped_region){0};
+	blur->corners = corner_radii_all(0);
+	blur->should_only_blur_bottom_layer = false;
+	blur->transparency_mask_source = linked_node_init();
+	blur->width = width;
+	blur->height = height;
+
+	scene_node_update(&blur->node, NULL);
+
+	return blur;
+}
+
+void wlr_scene_blur_set_size(struct wlr_scene_blur *blur, int width, int height) {
+	if (blur->width == width && blur->height == height) {
+		return;
+	}
+
+	blur->width = width;
+	blur->height = height;
+
+	scene_node_update(&blur->node, NULL);
+}
+
+inline void wlr_scene_blur_set_corner_radius(struct wlr_scene_blur *blur, int corner_radius) {
+	wlr_scene_blur_set_corner_radii(blur, corner_radii_all(corner_radius));
+}
+
+void wlr_scene_blur_set_corner_radii(struct wlr_scene_blur *blur, struct fx_corner_radii corners) {
+	if (fx_corner_radii_eq(blur->corners, corners)) {
+		return;
+	}
+
+	blur->corners = corners;
+	scene_node_update(&blur->node, NULL);
+}
+
+void wlr_scene_blur_set_should_only_blur_bottom_layer(struct wlr_scene_blur *blur,
+	bool should_only_blur_bottom_layer) {
+	if (blur->should_only_blur_bottom_layer == should_only_blur_bottom_layer) {
+		return;
+	}
+
+	blur->should_only_blur_bottom_layer = should_only_blur_bottom_layer;
+	scene_node_update(&blur->node, NULL);
+}
+
+void wlr_scene_blur_set_transparency_mask_source(struct wlr_scene_blur *blur,
+       struct wlr_scene_buffer *source) {
+	if (source == NULL && blur->transparency_mask_source.link == NULL) {
+		return;
+	}
+
+	if (source != NULL && linked_nodes_are_linked(&blur->transparency_mask_source, &source->blur)) {
+		return;
+	}
+
+	linked_node_destroy(&blur->transparency_mask_source);
+	linked_node_destroy(&source->blur);
+
+	if (source != NULL) {
+		linked_node_init_link(&blur->transparency_mask_source, &source->blur);
+	}
+
+	scene_node_update(&blur->node, NULL);
+}
+
+struct wlr_scene_buffer *wlr_scene_blur_get_transparency_mask_source(
+	struct wlr_scene_blur *blur) {
+	struct linked_node *node = linked_nodes_get_sibling(&blur->transparency_mask_source);
+	if (node == NULL) {
+		return NULL;
+	}
+
+	struct wlr_scene_buffer *output = wl_container_of(node, output, blur);
+	return output;
+}
+
+void wlr_scene_blur_set_alpha(struct wlr_scene_blur *blur, float alpha) {
+	if (blur->alpha == alpha) {
+		return;
+	}
+
+	blur->alpha = alpha;
+	scene_node_update(&blur->node, NULL);
+}
+
+void wlr_scene_blur_set_strength(struct wlr_scene_blur *blur, float strength) {
+	if (blur->strength == strength) {
+		return;
+	}
+
+	blur->strength = strength;
+	scene_node_update(&blur->node, NULL);
+}
+
+void wlr_scene_blur_set_clipped_region(struct wlr_scene_blur *blur,
+		struct clipped_region clipped_region) {
+	if (fx_corner_radii_eq(blur->clipped_region.corners, clipped_region.corners) &&
+		wlr_box_equal(&blur->clipped_region.area, &clipped_region.area)) {
+		return;
+	}
+
+	blur->clipped_region = clipped_region;
+	scene_node_update(&blur->node, NULL);
+}
+
 void wlr_scene_set_blur_data(struct wlr_scene *scene, int num_passes,
 		int radius, float noise, float brightness, float contrast, float saturation) {
 	struct blur_data *buff_data = &scene->blur_data;
@@ -1185,11 +1353,9 @@ struct wlr_scene_buffer *wlr_scene_buffer_create(struct wlr_scene_tree *parent,
 	wl_list_init(&scene_buffer->renderer_destroy.link);
 	scene_buffer->opacity = 1;
 
-	scene_buffer->corner_radius = 0;
-	scene_buffer->backdrop_blur = false;
-	scene_buffer->backdrop_blur_optimized = false;
-	scene_buffer->backdrop_blur_ignore_transparent = true;
-	scene_buffer->corners = CORNER_LOCATION_NONE;
+	scene_buffer->corners = corner_radii_none();
+
+	scene_buffer->blur = linked_node_init();
 
 	scene_buffer_set_buffer(scene_buffer, buffer);
 	scene_node_update(&scene_buffer->node, NULL);
@@ -1422,9 +1588,9 @@ void wlr_scene_buffer_set_transform(struct wlr_scene_buffer *scene_buffer,
 }
 
 void wlr_scene_buffer_send_frame_done(struct wlr_scene_buffer *scene_buffer,
-		struct timespec *now) {
+		struct wlr_scene_frame_done_event *event) {
 	if (!pixman_region32_empty(&scene_buffer->node.visible)) {
-		wl_signal_emit_mutable(&scene_buffer->events.frame_done, now);
+		wl_signal_emit_mutable(&scene_buffer->events.frame_done, event);
 	}
 }
 
@@ -1449,43 +1615,58 @@ void wlr_scene_buffer_set_filter_mode(struct wlr_scene_buffer *scene_buffer,
 	scene_node_update(&scene_buffer->node, NULL);
 }
 
-void wlr_scene_buffer_set_corner_radius(struct wlr_scene_buffer *scene_buffer,
-		int radii, enum corner_location corners) {
-	if (scene_buffer->corner_radius == radii
-			&& scene_buffer->corners == corners) {
+void wlr_scene_buffer_set_transfer_function(struct wlr_scene_buffer *scene_buffer,
+		enum wlr_color_transfer_function transfer_function) {
+	if (scene_buffer->transfer_function == transfer_function) {
+		return;
+	}
+
+	scene_buffer->transfer_function = transfer_function;
+	scene_node_update(&scene_buffer->node, NULL);
+}
+
+void wlr_scene_buffer_set_primaries(struct wlr_scene_buffer *scene_buffer,
+		enum wlr_color_named_primaries primaries) {
+	if (scene_buffer->primaries == primaries) {
 		return;
 	}
 
-	scene_buffer->corner_radius = radii;
-	scene_buffer->corners = corners;
+	scene_buffer->primaries = primaries;
 	scene_node_update(&scene_buffer->node, NULL);
 }
 
-void wlr_scene_buffer_set_backdrop_blur(struct wlr_scene_buffer *scene_buffer,
-		bool enabled) {
-	if (scene_buffer->backdrop_blur == enabled) {
+void wlr_scene_buffer_set_color_encoding(struct wlr_scene_buffer *scene_buffer,
+		enum wlr_color_encoding color_encoding) {
+	if (scene_buffer->color_encoding == color_encoding) {
 		return;
 	}
-	scene_buffer->backdrop_blur = enabled;
+
+	scene_buffer->color_encoding = color_encoding;
 	scene_node_update(&scene_buffer->node, NULL);
 }
 
-void wlr_scene_buffer_set_backdrop_blur_optimized(struct wlr_scene_buffer *scene_buffer,
-		bool enabled) {
-	if (scene_buffer->backdrop_blur_optimized == enabled) {
+void wlr_scene_buffer_set_color_range(struct wlr_scene_buffer *scene_buffer,
+		enum wlr_color_range color_range) {
+	if (scene_buffer->color_range == color_range) {
 		return;
 	}
-	scene_buffer->backdrop_blur_optimized = enabled;
+
+	scene_buffer->color_range = color_range;
 	scene_node_update(&scene_buffer->node, NULL);
 }
 
-void wlr_scene_buffer_set_backdrop_blur_ignore_transparent(
-		struct wlr_scene_buffer *scene_buffer, bool enabled) {
-	if (scene_buffer->backdrop_blur_ignore_transparent == enabled) {
+inline void wlr_scene_buffer_set_corner_radius(struct wlr_scene_buffer *scene_buffer,
+		int radii) {
+	wlr_scene_buffer_set_corner_radii(scene_buffer, corner_radii_all(radii));
+}
+
+void wlr_scene_buffer_set_corner_radii(struct wlr_scene_buffer *scene_buffer,
+	struct fx_corner_radii corner_radii) {
+	if (fx_corner_radii_eq(scene_buffer->corners, corner_radii)) {
 		return;
 	}
 
-	scene_buffer->backdrop_blur_ignore_transparent = enabled;
+	scene_buffer->corners = corner_radii;
 	scene_node_update(&scene_buffer->node, NULL);
 }
 
@@ -1511,30 +1692,18 @@ static struct wlr_texture *scene_buffer_get_texture(
 	return texture;
 }
 
-static void scene_node_get_size(struct wlr_scene_node *node,
-		int *width, int *height) {
+void scene_node_get_size(struct wlr_scene_node *node, int *width, int *height) {
 	*width = 0;
 	*height = 0;
 
 	switch (node->type) {
 	case WLR_SCENE_NODE_TREE:
 		return;
-	case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
-		struct wlr_scene_optimized_blur *scene_blur =
-			wlr_scene_optimized_blur_from_node(node);
-		*width = scene_blur->width;
-		*height = scene_blur->height;
-		break;
 	case WLR_SCENE_NODE_RECT:;
 		struct wlr_scene_rect *scene_rect = wlr_scene_rect_from_node(node);
 		*width = scene_rect->width;
 		*height = scene_rect->height;
 		break;
-	case WLR_SCENE_NODE_SHADOW:;
-		struct wlr_scene_shadow *scene_shadow = wlr_scene_shadow_from_node(node);
-		*width = scene_shadow->width;
-		*height = scene_shadow->height;
-		break;
 	case WLR_SCENE_NODE_BUFFER:;
 		struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_from_node(node);
 		if (scene_buffer->dst_width > 0 && scene_buffer->dst_height > 0) {
@@ -1546,6 +1715,22 @@ static void scene_node_get_size(struct wlr_scene_node *node,
 			wlr_output_transform_coords(scene_buffer->transform, width, height);
 		}
 		break;
+	case WLR_SCENE_NODE_SHADOW:;
+		struct wlr_scene_shadow *scene_shadow = wlr_scene_shadow_from_node(node);
+		*width = scene_shadow->width;
+		*height = scene_shadow->height;
+		break;
+	case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
+		struct wlr_scene_optimized_blur *scene_blur =
+			wlr_scene_optimized_blur_from_node(node);
+		*width = scene_blur->width;
+		*height = scene_blur->height;
+		break;
+	case WLR_SCENE_NODE_BLUR:;
+		struct wlr_scene_blur *blur = wlr_scene_blur_from_node(node);
+		*width = blur->width;
+		*height = blur->height;
+		break;
 	}
 }
 
@@ -1728,7 +1913,8 @@ static bool scene_node_at_iterator(struct wlr_scene_node *node,
 			return false;
 		}
 	} else if (node->type == WLR_SCENE_NODE_SHADOW
-			|| node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
+			|| node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR
+			|| node->type == WLR_SCENE_NODE_BLUR) {
 		// Disable interaction
 		return false;
 	}
@@ -1772,8 +1958,14 @@ struct render_list_entry {
 	int x, y;
 };
 
+static float get_luminance_multiplier(const struct wlr_color_luminances *src_lum,
+		const struct wlr_color_luminances *dst_lum) {
+	return (dst_lum->reference / src_lum->reference) * (src_lum->max / dst_lum->max);
+}
+
 static void scene_entry_render(struct render_list_entry *entry, const struct render_data *data) {
 	struct wlr_scene_node *node = entry->node;
+	struct fx_gles_render_pass *fx_pass = fx_get_render_pass(data->render_pass);
 
 	pixman_region32_t render_region;
 	pixman_region32_init(&render_region);
@@ -1812,62 +2004,22 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 		break;
 	case WLR_SCENE_NODE_RECT:;
 		struct wlr_scene_rect *scene_rect = wlr_scene_rect_from_node(node);
-		enum corner_location rect_corners = scene_rect->corners;
-
-		corner_location_transform(node_transform, &rect_corners);
-
-		// blur
-		bool has_alpha = !pixman_region32_empty(&opaque);
-		if (has_alpha && scene_rect->backdrop_blur &&
-				is_scene_blur_enabled(&scene->blur_data)) {
-			pixman_region32_t opaque_region;
-			pixman_region32_init(&opaque_region);
-			scene_node_opaque_region(node, x, y, &opaque_region);
-			logical_to_buffer_coords(&opaque_region, data, false);
-
-			/* TODO: should this be configurable? Borked when not 1.0, probably due to
-			   lack of premultiplication in the frag shader
-			*/
-			float blur_alpha = 1.0;
-
-			struct fx_render_blur_pass_options blur_options = {
-				.tex_options = {
-					.base = (struct wlr_render_texture_options) {
-						.texture = NULL,
-						.src_box = (struct wlr_fbox){0},
-						.dst_box = dst_box,
-						.transform = WL_OUTPUT_TRANSFORM_NORMAL,
-						.clip = &render_region,
-						.alpha = &blur_alpha,
-						.filter_mode = WLR_SCALE_FILTER_BILINEAR,
-						.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
-					},
-					.clip_box = &dst_box,
-					.corner_radius = scene_rect->corner_radius * data->scale,
-					.corners = rect_corners,
-					.discard_transparent = false,
-				},
-				.opaque_region = &opaque_region,
-				.use_optimized_blur = scene_rect->backdrop_blur_optimized,
-				.blur_data = &scene->blur_data,
-				.ignore_transparent = false,
-			};
-			// Render the actual blur behind the surface
-			fx_render_pass_add_blur(data->render_pass, &blur_options);
-
-			pixman_region32_fini(&opaque_region);
+		if (scene_rect->color[3] == 0.0) {
+			break;
 		}
+		struct fx_corner_radii rect_corners = scene_rect->corners;
+
+		fx_corner_radii_transform(node_transform, &rect_corners);
 
 		struct wlr_box rect_clipped_region_box = scene_rect->clipped_region.area;
-		int rect_clipped_region_corner_radius = scene_rect->clipped_region.corner_radius;
-		enum corner_location rect_clipped_corners = scene_rect->clipped_region.corners;
+		struct fx_corner_radii rect_clipped_corners = scene_rect->clipped_region.corners;
 
 		// Node relative -> Root relative
 		rect_clipped_region_box.x += x;
 		rect_clipped_region_box.y += y;
 
 		transform_output_box(&rect_clipped_region_box, data);
-		corner_location_transform(node_transform, &rect_clipped_corners);
+		fx_corner_radii_transform(node_transform, &rect_clipped_corners);
 
 		struct fx_render_rect_options rect_options = {
 			.base = {
@@ -1882,93 +2034,32 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 			},
 			.clipped_region = {
 				.area = rect_clipped_region_box,
-				.corner_radius = rect_clipped_region_corner_radius * data->scale,
-				.corners = rect_clipped_corners,
+				.corners = fx_corner_radii_scale(rect_clipped_corners, data->scale),
 			},
 		};
 
-		if ((scene_rect->corner_radius && rect_corners != CORNER_LOCATION_NONE) || scene_rect->fade_inset > 0) {
+		// TODO: Use the base wlr_render_pass_add_rect as a fast-path in the future
+		if (!fx_corner_radii_is_empty(&rect_corners) || scene_rect->fade_inset > 0) {
 			struct fx_render_rounded_rect_options rounded_rect_options = {
 				.base = rect_options.base,
-				.corner_radius = scene_rect->corner_radius * data->scale,
-				.corners = rect_corners,
+				.corners = fx_corner_radii_scale(rect_corners, data->scale),
 				.clipped_region = rect_options.clipped_region,
 				.fade_inset = scene_rect->fade_inset * data->scale,
 			};
-			fx_render_pass_add_rounded_rect(data->render_pass, &rounded_rect_options);
+			fx_render_pass_add_rounded_rect(fx_pass, &rounded_rect_options);
 		} else {
-			fx_render_pass_add_rect(data->render_pass, &rect_options);
-		}
-		break;
-	case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
-		struct wlr_scene_optimized_blur *scene_blur = wlr_scene_optimized_blur_from_node(node);
-		// Re-render the optimized blur buffer when needed
-		if (data->has_blur && is_scene_blur_enabled(&scene->blur_data)
-				&& scene_blur->dirty) {
-			const float opacity = 1.0f;
-			enum wl_output_transform transform =
-				wlr_output_transform_invert(data->transform);
-			transform = wlr_output_transform_compose(transform, data->transform);
-			struct fx_render_blur_pass_options blur_options = {
-				.tex_options = {
-					.base = {
-						.transform = transform,
-						.alpha = &opacity,
-						.blend_mode = WLR_RENDER_BLEND_MODE_NONE,
-						.dst_box = dst_box,
-					},
-					.clip_box = &dst_box,
-					.corner_radius = 0,
-					.discard_transparent = false,
-				},
-				.blur_data = &scene->blur_data,
-			};
-			bool result = fx_render_pass_add_optimized_blur(data->render_pass, &blur_options);
-			if (result) {
-				scene_blur->dirty = false;
-			}
+			fx_render_pass_add_rect(fx_pass, &rect_options);
 		}
 		break;
-	case WLR_SCENE_NODE_SHADOW:;
-		struct wlr_scene_shadow *scene_shadow = wlr_scene_shadow_from_node(node);
-
-		struct wlr_box shadow_clipped_region_box = scene_shadow->clipped_region.area;
-		int shadow_clipped_region_corner_radius = scene_shadow->clipped_region.corner_radius;
-		enum corner_location shadow_clipped_corners = scene_shadow->clipped_region.corners;
-
-		// Node relative -> Root relative
-		shadow_clipped_region_box.x += x;
-		shadow_clipped_region_box.y += y;
-
-		transform_output_box(&shadow_clipped_region_box, data);
-		corner_location_transform(node_transform, &shadow_clipped_corners);
-
-		struct fx_render_box_shadow_options shadow_options = {
-			.box = dst_box,
-			.clipped_region = {
-				.area = shadow_clipped_region_box,
-				.corner_radius = shadow_clipped_region_corner_radius * data->scale,
-				.corners = shadow_clipped_corners,
-			},
-			.blur_sigma = scene_shadow->blur_sigma,
-			.corner_radius = scene_shadow->corner_radius * data->scale,
-			.color = {
-				.r = scene_shadow->color[0],
-				.g = scene_shadow->color[1],
-				.b = scene_shadow->color[2],
-				.a = scene_shadow->color[3],
-			},
-			.clip = &render_region,
-		};
-		fx_render_pass_add_box_shadow(data->render_pass, &shadow_options);
-		break;
 	case WLR_SCENE_NODE_BUFFER:;
 		struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_from_node(node);
-		enum corner_location buffer_corners = scene_buffer->corners;
+		struct fx_corner_radii buffer_corners = scene_buffer->corners;
 
 		if (scene_buffer->is_single_pixel_buffer) {
+			// TODO: Render blur/rounded corners/etc here:
+
 			// Render the buffer as a rect, this is likely to be more efficient
-			wlr_render_pass_add_rect(&data->render_pass->base, &(struct wlr_render_rect_options){
+			wlr_render_pass_add_rect(data->render_pass, &(struct wlr_render_rect_options){
 				.box = dst_box,
 				.color = {
 					.r = (float)scene_buffer->single_pixel_buffer_color[0] / (float)UINT32_MAX,
@@ -1992,53 +2083,20 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 		enum wl_output_transform transform =
 			wlr_output_transform_invert(scene_buffer->transform);
 		transform = wlr_output_transform_compose(transform, data->transform);
-		corner_location_transform(transform, &buffer_corners);
-
-		// Blur
-		if (scene_buffer->backdrop_blur && is_scene_blur_enabled(&scene->blur_data)) {
-			pixman_region32_t opaque_region;
-			pixman_region32_init(&opaque_region);
-
-			bool has_alpha = !pixman_region32_empty(&opaque);
-			scene_node_opaque_region(node, x, y, &opaque_region);
-			logical_to_buffer_coords(&opaque_region, data, false);
-
-			if (has_alpha) {
-				// Translate the opaque_region by the potential clipping offset.
-				// Fixes GTK CSD offsetting the opaque_region
-				pixman_region32_translate(&opaque_region,
-						-scene_buffer->src_box.x, -scene_buffer->src_box.y);
-
-				// TODO: should I be configurable? We should probably move blur to a node
-				float blur_alpha = 1.0;
-				struct fx_render_blur_pass_options blur_options = {
-					.tex_options = {
-						.base = (struct wlr_render_texture_options) {
-							.texture = texture,
-							.src_box = scene_buffer->src_box,
-							.dst_box = dst_box,
-							.transform = WL_OUTPUT_TRANSFORM_NORMAL,
-							.clip = &render_region, // Render with the smaller region, clipping CSD
-							.alpha = &blur_alpha,
-							.filter_mode = WLR_SCALE_FILTER_BILINEAR,
-						},
-						.clip_box = &dst_box,
-						.corner_radius = scene_buffer->corner_radius * data->scale,
-						.corners = buffer_corners,
-						.discard_transparent = false,
-					},
-					.opaque_region = &opaque_region,
-					.use_optimized_blur = scene_buffer->backdrop_blur_optimized,
-					.blur_data = &scene->blur_data,
-					.ignore_transparent = scene_buffer->backdrop_blur_ignore_transparent,
-				};
-				// Render the actual blur behind the surface
-				fx_render_pass_add_blur(data->render_pass, &blur_options);
+		fx_corner_radii_transform(transform, &buffer_corners);
 
-			}
-			pixman_region32_fini(&opaque_region);
+		struct wlr_color_primaries primaries = {0};
+		if (scene_buffer->primaries != 0) {
+			wlr_color_primaries_from_named(&primaries, scene_buffer->primaries);
 		}
 
+		struct wlr_color_luminances src_lum, srgb_lum;
+		wlr_color_transfer_function_get_default_luminance(
+			scene_buffer->transfer_function, &src_lum);
+		wlr_color_transfer_function_get_default_luminance(
+			WLR_COLOR_TRANSFER_FUNCTION_SRGB, &srgb_lum);
+		float luminance_multiplier = get_luminance_multiplier(&src_lum, &srgb_lum);
+
 		struct fx_render_texture_options tex_options = {
 			.base = (struct wlr_render_texture_options){
 				.texture = texture,
@@ -2051,29 +2109,142 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren
 				.blend_mode = !data->output->scene->calculate_visibility ||
 					!pixman_region32_empty(&opaque) ?
 					WLR_RENDER_BLEND_MODE_PREMULTIPLIED : WLR_RENDER_BLEND_MODE_NONE,
+				.transfer_function = scene_buffer->transfer_function,
+				.primaries = scene_buffer->primaries != 0 ? &primaries : NULL,
+				.color_encoding = scene_buffer->color_encoding,
+				.color_range = scene_buffer->color_range,
+				.luminance_multiplier = &luminance_multiplier,
 				.wait_timeline = scene_buffer->wait_timeline,
 				.wait_point = scene_buffer->wait_point,
 			},
 			.clip_box = &dst_box,
-			.corners = buffer_corners,
-			.corner_radius = scene_buffer->corner_radius * data->scale,
+			.corners = fx_corner_radii_scale(buffer_corners, data->scale),
+			.clipped_region = {0},
 		};
 
-		fx_render_pass_add_texture(data->render_pass, &tex_options);
+		// TODO: Use the base wlr_render_pass_add_texture as a fast-path in the future
+		fx_render_pass_add_texture(fx_pass, &tex_options);
 
 		struct wlr_scene_output_sample_event sample_event = {
 			.output = data->output,
 			.direct_scanout = false,
+			.release_timeline = data->output->in_timeline,
+			.release_point = data->output->in_point,
 		};
 		wl_signal_emit_mutable(&scene_buffer->events.output_sample, &sample_event);
+
 		if (entry->highlight_transparent_region) {
-			wlr_render_pass_add_rect(&data->render_pass->base, &(struct wlr_render_rect_options){
+			wlr_render_pass_add_rect(data->render_pass, &(struct wlr_render_rect_options){
 					.box = dst_box,
 					.color = { .r = 0, .g = 0.3, .b = 0, .a = 0.3 },
 					.clip = &opaque,
 			});
 		}
 		break;
+	case WLR_SCENE_NODE_SHADOW:;
+		struct wlr_scene_shadow *scene_shadow = wlr_scene_shadow_from_node(node);
+
+		struct wlr_box shadow_clipped_region_box = scene_shadow->clipped_region.area;
+		struct fx_corner_radii shadow_clipped_corners = scene_shadow->clipped_region.corners;
+
+		// Node relative -> Root relative
+		shadow_clipped_region_box.x += x;
+		shadow_clipped_region_box.y += y;
+
+		transform_output_box(&shadow_clipped_region_box, data);
+		fx_corner_radii_transform(node_transform, &shadow_clipped_corners);
+
+		struct fx_render_box_shadow_options shadow_options = {
+			.box = dst_box,
+			.clipped_region = {
+				.area = shadow_clipped_region_box,
+				.corners = fx_corner_radii_scale(shadow_clipped_corners, data->scale),
+			},
+			.blur_sigma = scene_shadow->blur_sigma,
+			.corner_radius = scene_shadow->corner_radius * data->scale,
+			.color = {
+				.r = scene_shadow->color[0],
+				.g = scene_shadow->color[1],
+				.b = scene_shadow->color[2],
+				.a = scene_shadow->color[3],
+			},
+			.clip = &render_region,
+		};
+		fx_render_pass_add_box_shadow(fx_pass, &shadow_options);
+		break;
+	case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
+		struct wlr_scene_optimized_blur *scene_blur = wlr_scene_optimized_blur_from_node(node);
+		// Re-render the optimized blur buffer when needed. Retry rendering
+		// until there's a visible blur_node.
+		if (fx_pass->has_blur && is_scene_blur_enabled(&scene->blur_data)
+				&& scene_blur->dirty) {
+			const float opacity = 1.0f;
+			enum wl_output_transform transform =
+				wlr_output_transform_invert(data->transform);
+			transform = wlr_output_transform_compose(transform, data->transform);
+			struct fx_render_blur_pass_options blur_options = {
+				.tex_options = {
+					.base = {
+						.transform = transform,
+						.alpha = &opacity,
+						.blend_mode = WLR_RENDER_BLEND_MODE_NONE,
+						.dst_box = dst_box,
+					},
+					.clip_box = &dst_box,
+					.discard_transparent = false,
+					.clipped_region = {0}
+				},
+				.blur_data = &scene->blur_data,
+				.blur_strength = 1.0f,
+			};
+			bool result = fx_render_pass_add_optimized_blur(fx_pass, &blur_options);
+			if (result) {
+				scene_blur->dirty = false;
+			}
+		}
+		break;
+	case WLR_SCENE_NODE_BLUR:;
+		struct wlr_scene_blur *blur = wlr_scene_blur_from_node(node);
+
+		struct wlr_texture *tex = NULL;
+		struct wlr_scene_buffer *mask = wlr_scene_blur_get_transparency_mask_source(blur);
+
+		enum wl_output_transform mask_transform = WL_OUTPUT_TRANSFORM_NORMAL;
+
+		struct wlr_fbox mask_src_box = {0};
+		if (mask != NULL) {
+			tex = scene_buffer_get_texture(mask, data->output->output->renderer);
+			mask_transform = wlr_output_transform_invert(mask->transform);
+			mask_transform = wlr_output_transform_compose(mask_transform, data->transform);
+			mask_src_box = mask->src_box;
+		}
+
+		struct fx_corner_radii blur_corners = blur->corners;
+		fx_corner_radii_transform(node_transform, &blur_corners);
+
+		struct fx_render_blur_pass_options blur_options = {
+			.tex_options = {
+				.base = (struct wlr_render_texture_options) {
+					.texture = tex,
+					.src_box = mask_src_box,
+					.dst_box = dst_box,
+					.transform = mask_transform,
+					.clip = &render_region,
+					.alpha = &blur->alpha,
+					.filter_mode = WLR_SCALE_FILTER_BILINEAR,
+					.blend_mode = WLR_RENDER_BLEND_MODE_PREMULTIPLIED,
+				},
+				.clip_box = &dst_box,
+				.corners = fx_corner_radii_scale(blur_corners, data->scale),
+				.discard_transparent = false,
+			},
+			.use_optimized_blur = blur->should_only_blur_bottom_layer,
+			.blur_data = &scene->blur_data,
+			.ignore_transparent = mask != NULL,
+			.blur_strength = blur->strength,
+		};
+		fx_render_pass_add_blur(fx_pass, &blur_options);
+		break;
 	}
 
 	pixman_region32_fini(&opaque);
@@ -2110,6 +2281,8 @@ static void scene_handle_gamma_control_manager_v1_set_gamma(struct wl_listener *
 
 	output->gamma_lut_changed = true;
 	output->gamma_lut = event->control;
+	wlr_color_transform_unref(output->gamma_lut_color_transform);
+	output->gamma_lut_color_transform = wlr_gamma_control_v1_get_color_transform(event->control);
 	wlr_output_schedule_frame(output->output);
 }
 
@@ -2127,11 +2300,13 @@ static void scene_handle_gamma_control_manager_v1_destroy(struct wl_listener *li
 	wl_list_for_each(output, &scene->outputs, link) {
 		output->gamma_lut_changed = false;
 		output->gamma_lut = NULL;
+		wlr_color_transform_unref(output->gamma_lut_color_transform);
+		output->gamma_lut_color_transform = NULL;
 	}
 }
 
 void wlr_scene_set_gamma_control_manager_v1(struct wlr_scene *scene,
-		struct wlr_gamma_control_manager_v1 *gamma_control) {
+	    struct wlr_gamma_control_manager_v1 *gamma_control) {
 	assert(scene->gamma_control_manager_v1 == NULL);
 	scene->gamma_control_manager_v1 = gamma_control;
 
@@ -2143,6 +2318,21 @@ void wlr_scene_set_gamma_control_manager_v1(struct wlr_scene *scene,
 	wl_signal_add(&gamma_control->events.set_gamma, &scene->gamma_control_manager_v1_set_gamma);
 }
 
+static void scene_handle_color_manager_v1_destroy(struct wl_listener *listener, void *data) {
+	struct wlr_scene *scene = wl_container_of(listener, scene, color_manager_v1_destroy);
+	wl_list_remove(&scene->color_manager_v1_destroy.link);
+	wl_list_init(&scene->color_manager_v1_destroy.link);
+	scene->color_manager_v1 = NULL;
+}
+
+void wlr_scene_set_color_manager_v1(struct wlr_scene *scene, struct wlr_color_manager_v1 *manager) {
+	assert(scene->color_manager_v1 == NULL);
+	scene->color_manager_v1 = manager;
+
+	scene->color_manager_v1_destroy.notify = scene_handle_color_manager_v1_destroy;
+	wl_signal_add(&manager->events.destroy, &scene->color_manager_v1_destroy);
+}
+
 static void scene_output_handle_destroy(struct wlr_addon *addon) {
 	struct wlr_scene_output *scene_output =
 		wl_container_of(addon, scene_output, addon);
@@ -2189,7 +2379,7 @@ static void scene_output_handle_commit(struct wl_listener *listener, void *data)
 	if (state->committed & WLR_OUTPUT_STATE_BUFFER) {
 		if (state->committed & WLR_OUTPUT_STATE_DAMAGE) {
 			pixman_region32_subtract(&scene_output->pending_commit_damage,
-					&scene_output->pending_commit_damage, &state->damage);
+				&scene_output->pending_commit_damage, &state->damage);
 		} else {
 			pixman_region32_fini(&scene_output->pending_commit_damage);
 			pixman_region32_init(&scene_output->pending_commit_damage);
@@ -2211,6 +2401,11 @@ static void scene_output_handle_commit(struct wl_listener *listener, void *data)
 		wlr_output_schedule_frame(scene_output->output);
 	}
 
+	TRACY_WHEN_CONNECTED({
+		// Queue a new frame as soon as possible when profiling
+		wlr_output_schedule_frame(scene_output->output);
+	})
+
 	// Next time the output is enabled, try to re-apply the gamma LUT
 	if (scene_output->scene->gamma_control_manager_v1 &&
 			(state->committed & WLR_OUTPUT_STATE_ENABLED) &&
@@ -2275,7 +2470,10 @@ struct wlr_scene_output *wlr_scene_output_create(struct wlr_scene *scene,
 	if (drm_fd >= 0 && output->backend->features.timeline &&
 			output->renderer != NULL && output->renderer->features.timeline) {
 		scene_output->in_timeline = wlr_drm_syncobj_timeline_create(drm_fd);
-		if (scene_output->in_timeline == NULL) {
+		scene_output->out_timeline = wlr_drm_syncobj_timeline_create(drm_fd);
+		if (scene_output->in_timeline == NULL || scene_output->out_timeline == NULL) {
+			wlr_drm_syncobj_timeline_unref(scene_output->in_timeline);
+			wlr_drm_syncobj_timeline_unref(scene_output->out_timeline);
 			return NULL;
 		}
 	}
@@ -2330,7 +2528,18 @@ void wlr_scene_output_destroy(struct wlr_scene_output *scene_output) {
 	wl_list_remove(&scene_output->output_commit.link);
 	wl_list_remove(&scene_output->output_damage.link);
 	wl_list_remove(&scene_output->output_needs_frame.link);
-	wlr_drm_syncobj_timeline_unref(scene_output->in_timeline);
+	if (scene_output->in_timeline != NULL) {
+		wlr_drm_syncobj_timeline_signal(scene_output->in_timeline, UINT64_MAX);
+		wlr_drm_syncobj_timeline_unref(scene_output->in_timeline);
+	}
+	if (scene_output->out_timeline != NULL) {
+		wlr_drm_syncobj_timeline_signal(scene_output->out_timeline, UINT64_MAX);
+		wlr_drm_syncobj_timeline_unref(scene_output->out_timeline);
+	}
+	wlr_color_transform_unref(scene_output->gamma_lut_color_transform);
+	wlr_color_transform_unref(scene_output->prev_gamma_lut_color_transform);
+	wlr_color_transform_unref(scene_output->prev_supplied_color_transform);
+	wlr_color_transform_unref(scene_output->combined_color_transform);
 	wl_array_release(&scene_output->render_list);
 	free(scene_output);
 }
@@ -2364,18 +2573,16 @@ static bool scene_node_invisible(struct wlr_scene_node *node) {
 		return true;
 	} else if (node->type == WLR_SCENE_NODE_RECT) {
 		struct wlr_scene_rect *rect = wlr_scene_rect_from_node(node);
-
+		// TODO: Check if clipped region covers whole rect?
 		return rect->color[3] == 0.f;
-	} else if (node->type == WLR_SCENE_NODE_SHADOW) {
-		struct wlr_scene_shadow *shadow = wlr_scene_shadow_from_node(node);
-
-		return shadow->color[3] == 0.f;
-	} else if (node->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
-		return false;
 	} else if (node->type == WLR_SCENE_NODE_BUFFER) {
 		struct wlr_scene_buffer *buffer = wlr_scene_buffer_from_node(node);
 
 		return buffer->buffer == NULL && buffer->texture == NULL;
+	} else if (node->type == WLR_SCENE_NODE_SHADOW) {
+		struct wlr_scene_shadow *shadow = wlr_scene_shadow_from_node(node);
+
+		return shadow->color[3] == 0.f;
 	}
 
 	return false;
@@ -2395,7 +2602,18 @@ static bool scene_buffer_is_black_opaque(struct wlr_scene_buffer *scene_buffer)
 		scene_buffer->single_pixel_buffer_color[1] == 0 &&
 		scene_buffer->single_pixel_buffer_color[2] == 0 &&
 		scene_buffer->single_pixel_buffer_color[3] == UINT32_MAX &&
-		scene_buffer->opacity == 1.0;
+		scene_buffer->opacity == 1.0 &&
+		fx_corner_radii_is_empty(&scene_buffer->corners);
+}
+
+static bool scene_rect_is_black_opaque(struct wlr_scene_rect *scene_rect) {
+	return scene_rect->color[0] == 0.f &&
+		scene_rect->color[1] == 0.f &&
+		scene_rect->color[2] == 0.f &&
+		scene_rect->color[3] == 1.f &&
+		fx_corner_radii_is_empty(&scene_rect->corners) &&
+		fx_corner_radii_is_empty(&scene_rect->clipped_region.corners) &&
+		wlr_box_empty(&scene_rect->clipped_region.area);
 }
 
 static bool construct_render_list_iterator(struct wlr_scene_node *node,
@@ -2413,9 +2631,8 @@ static bool construct_render_list_iterator(struct wlr_scene_node *node,
 	if (node->type == WLR_SCENE_NODE_RECT && data->calculate_visibility &&
 			(!data->fractional_scale || data->render_list->size == 0)) {
 		struct wlr_scene_rect *rect = wlr_scene_rect_from_node(node);
-		float *black = (float[4]){ 0.f, 0.f, 0.f, 1.f };
 
-		if (memcmp(rect->color, black, sizeof(float) * 4) == 0) {
+		if (scene_rect_is_black_opaque(rect)) {
 			return false;
 		}
 	}
@@ -2482,12 +2699,41 @@ static void scene_buffer_send_dmabuf_feedback(const struct wlr_scene *scene,
 		return;
 	}
 
+	enum wl_output_transform preferred_buffer_transform = WL_OUTPUT_TRANSFORM_NORMAL;
+	if (options->scanout_primary_output != NULL) {
+		preferred_buffer_transform = options->scanout_primary_output->transform;
+	}
+
+	// TODO: also send wl_surface.preferred_buffer_transform when running with
+	// pure software rendering
+	wlr_surface_set_preferred_buffer_transform(surface->surface, preferred_buffer_transform);
 	wlr_linux_dmabuf_v1_set_surface_feedback(scene->linux_dmabuf_v1,
 		surface->surface, &feedback);
 
 	wlr_linux_dmabuf_feedback_v1_finish(&feedback);
 }
 
+static bool color_management_is_scanout_allowed(const struct wlr_output_image_description *img_desc,
+		const struct wlr_scene_buffer *buffer) {
+	// Disallow scanout if the output has colorimetry information but buffer
+	// doesn't; allow it only if the output also lacks it.
+	if (buffer->transfer_function == 0 && buffer->primaries == 0) {
+		return img_desc == NULL;
+	}
+
+	// If the output has colorimetry information, the buffer must match it for
+	// direct scanout to be allowed.
+	if (img_desc != NULL) {
+		return img_desc->transfer_function == buffer->transfer_function &&
+				img_desc->primaries == buffer->primaries;
+	}
+	// If the output doesn't have colorimetry image description set, we can only
+	// scan out buffers with default colorimetry (gamma2.2 transfer and sRGB
+	// primaries) used in wlroots.
+	return buffer->transfer_function == WLR_COLOR_TRANSFER_FUNCTION_GAMMA22 &&
+			buffer->primaries == WLR_COLOR_NAMED_PRIMARIES_SRGB;
+}
+
 enum scene_direct_scanout_result {
 	// This scene node is not a candidate for scanout
 	SCANOUT_INELIGIBLE,
@@ -2544,6 +2790,20 @@ static enum scene_direct_scanout_result scene_entry_try_direct_scanout(
 		return SCANOUT_INELIGIBLE;
 	}
 
+	const struct wlr_output_image_description *img_desc = output_pending_image_description(scene_output->output, state);
+	if (!color_management_is_scanout_allowed(img_desc, buffer)) {
+		return SCANOUT_INELIGIBLE;
+	}
+
+	bool is_color_repr_none = buffer->color_encoding == WLR_COLOR_ENCODING_NONE &&
+			buffer->color_range == WLR_COLOR_RANGE_NONE;
+	bool is_color_repr_identity_full = buffer->color_encoding == WLR_COLOR_ENCODING_IDENTITY &&
+			buffer->color_range == WLR_COLOR_RANGE_FULL;
+
+	if (!(is_color_repr_none || is_color_repr_identity_full)) {
+		return SCANOUT_INELIGIBLE;
+	}
+
 	// We want to ensure optimal buffer selection, but as direct-scanout can be enabled and disabled
 	// on a frame-by-frame basis, we wait for a few frames to send the new format recommendations.
 	// Maybe we should only send feedback in this case if tests fail.
@@ -2585,6 +2845,12 @@ static enum scene_direct_scanout_result scene_entry_try_direct_scanout(
 	if (buffer->wait_timeline != NULL) {
 		wlr_output_state_set_wait_timeline(&pending, buffer->wait_timeline, buffer->wait_point);
 	}
+
+	if (scene_output->out_timeline) {
+		scene_output->out_point++;
+		wlr_output_state_set_signal_timeline(&pending, scene_output->out_timeline, scene_output->out_point);
+	}
+
 	if (!wlr_output_test_state(scene_output->output, &pending)) {
 		wlr_output_state_finish(&pending);
 		return SCANOUT_CANDIDATE;
@@ -2596,6 +2862,8 @@ static enum scene_direct_scanout_result scene_entry_try_direct_scanout(
 	struct wlr_scene_output_sample_event sample_event = {
 		.output = scene_output,
 		.direct_scanout = true,
+		.release_timeline = data->output->out_timeline,
+		.release_point = data->output->out_point,
 	};
 	wl_signal_emit_mutable(&buffer->events.output_sample, &sample_event);
 	return SCANOUT_SUCCESS;
@@ -2607,76 +2875,77 @@ bool wlr_scene_output_needs_frame(struct wlr_scene_output *scene_output) {
 		scene_output->gamma_lut_changed;
 }
 
-static void apply_blur_region(struct wlr_scene_node *node,
-		struct wlr_scene_output *scene_output, pixman_region32_t *blur_region) {
-	int x, y;
-	wlr_scene_node_coords(node, &x, &y);
+static bool should_blur_node_extend_damage(struct wlr_scene_node *node,
+		struct fx_gles_render_pass *fx_pass, struct blur_data *blur_data) {
+	switch (node->type) {
+	case WLR_SCENE_NODE_BLUR:;
+		struct wlr_scene_blur *blur_node = wlr_scene_blur_from_node(node);
+		// No artifact prevention needed when the whole blur is already
+		// rendered
+		if (blur_node->should_only_blur_bottom_layer && blur_node->strength == 1.0) {
+			fx_pass->has_blur = true;
+			return false;
+		}
+		// Apply the blur strength to avoid rendering more blur than
+		// what's needed
+		if (blur_node->strength < 1.0f) {
+			*blur_data = blur_data_apply_strength(blur_data, blur_node->strength);
+		}
+		break;
+	case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
+		struct wlr_scene_optimized_blur *optimized_node =
+			wlr_scene_optimized_blur_from_node(node);
+		if (!optimized_node->dirty) {
+			return false;
+		}
+		break;
+	default:
+		return false;
+	}
 
-	pixman_region32_t opaque_region;
-	pixman_region32_init(&opaque_region);
-	scene_node_opaque_region(node, x, y, &opaque_region);
-	// Add the buffer to the blur_region if it's not fully opaque
-	if (pixman_region32_empty(&opaque_region)) {
-		struct wlr_box node_box = {
-			.x = x - scene_output->x,
-			.y = y - scene_output->y,
-		};
-		scene_node_get_size(node, &node_box.width, &node_box.height);
+	return is_scene_blur_enabled(blur_data);
+}
 
-		struct wlr_output *output = scene_output->output;
+static bool apply_blur_region(struct wlr_scene_node *node, struct blur_data *blur_data,
+		struct render_data *render_data, struct wlr_output_state *output_state,
+		const pixman_region32_t *original_damage, pixman_region32_t *blur_padding_region) {
+	bool should_compensate_blur = false;
+	const int sample_size = blur_data_calc_size(blur_data);
 
-		int output_width, output_height;
-		wlr_output_transformed_resolution(output, &output_width, &output_height);
+	pixman_region32_t node_visible_region;
+	pixman_region32_init(&node_visible_region);
+	pixman_region32_copy(&node_visible_region, &node->visible);
+	pixman_region32_translate(&node_visible_region, -render_data->output->x, -render_data->output->y);
+	logical_to_buffer_coords(&node_visible_region, render_data, false);
 
-		// Transform the box back to regular un-transformed units
-		scale_box(&node_box, output->scale);
-		wlr_box_transform(&node_box, &node_box,
-				wlr_output_transform_invert(output->transform),
-				output_width, output_height);
+	pixman_region32_t expanded_damage;
+	pixman_region32_init(&expanded_damage);
+	wlr_region_expand(&expanded_damage, original_damage, sample_size);
 
-		pixman_region32_union_rect(blur_region, blur_region,
-				node_box.x,
-				node_box.y,
-				node_box.width, node_box.height);
-	}
-	pixman_region32_fini(&opaque_region);
-}
+	pixman_region32_t intersection;
+	pixman_region32_init(&intersection);
+	if (pixman_region32_intersect(&intersection, &expanded_damage, &node_visible_region)) {
+		should_compensate_blur = true;
 
-static bool scene_output_has_blur(int list_len,
-		struct render_list_entry *list_data, struct wlr_scene_output *scene_output,
-		pixman_region32_t *blur_region) {
-	if (scene_output->scene->blur_data.radius <= 0 ||
-			scene_output->scene->blur_data.num_passes <= 0) {
-		return false;
-	}
+		// Expand the render damage to re-render surrounding blur nodes
+		pixman_region32_union(&render_data->damage, &render_data->damage, &intersection);
+		// Also make sure that the backend also knows about the new
+		// damage. Very important
+		output_state->committed |= WLR_OUTPUT_STATE_DAMAGE;
+		pixman_region32_union(&output_state->damage, &output_state->damage, &intersection);
 
-	for (int i = list_len - 1; i >= 0; i--) {
-		struct wlr_scene_node *node = list_data[i].node;
-		switch (node->type) {
-		case WLR_SCENE_NODE_BUFFER:;
-			struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_from_node(node);
-			if (scene_buffer->backdrop_blur) {
-				apply_blur_region(node, scene_output, blur_region);
-			}
-			break;
-		case WLR_SCENE_NODE_RECT:;
-			struct wlr_scene_rect *scene_rect = wlr_scene_rect_from_node(node);
-			if (scene_rect->backdrop_blur) {
-				apply_blur_region(node, scene_output, blur_region);
-			}
-			break;
-		case WLR_SCENE_NODE_OPTIMIZED_BLUR:;
-			struct wlr_scene_optimized_blur *scene_blur = wlr_scene_optimized_blur_from_node(node);
-			if (scene_blur->dirty) {
-				apply_blur_region(node, scene_output, blur_region);
-			}
-			break;
-		default:
-			// TODO: Add support for other node types
-			break;
-		}
+		// Expand it once more to get the blur padding region
+		// which is key for artifact removal :)
+		wlr_region_expand(&intersection, &intersection, sample_size);
+		// Don't re-add already added rectangles
+		pixman_region32_subtract(&intersection, &intersection, blur_padding_region);
+		pixman_region32_union(blur_padding_region, blur_padding_region, &intersection);
 	}
-	return !pixman_region32_empty(blur_region);
+
+	pixman_region32_fini(&intersection);
+	pixman_region32_fini(&expanded_damage);
+	pixman_region32_fini(&node_visible_region);
+	return should_compensate_blur;
 }
 
 bool wlr_scene_output_commit(struct wlr_scene_output *scene_output,
@@ -2713,16 +2982,15 @@ static void scene_output_state_attempt_gamma(struct wlr_scene_output *scene_outp
 		return;
 	}
 
-	if (!wlr_gamma_control_v1_apply(scene_output->gamma_lut, &gamma_pending)) {
-		wlr_output_state_finish(&gamma_pending);
-		return;
-	}
-
+	wlr_output_state_set_color_transform(&gamma_pending, scene_output->gamma_lut_color_transform);
 	scene_output->gamma_lut_changed = false;
+
 	if (!wlr_output_test_state(scene_output->output, &gamma_pending)) {
 		wlr_gamma_control_v1_send_failed_and_destroy(scene_output->gamma_lut);
 
 		scene_output->gamma_lut = NULL;
+		wlr_color_transform_unref(scene_output->gamma_lut_color_transform);
+		scene_output->gamma_lut_color_transform = NULL;
 		wlr_output_state_finish(&gamma_pending);
 		return;
 	}
@@ -2731,6 +2999,79 @@ static void scene_output_state_attempt_gamma(struct wlr_scene_output *scene_outp
 	wlr_output_state_finish(&gamma_pending);
 }
 
+static bool scene_output_combine_color_transforms(
+		struct wlr_scene_output *scene_output, struct wlr_color_transform *supplied,
+		const struct wlr_output_image_description *img_desc, bool render_gamma_lut) {
+	bool result = false;
+	struct wlr_color_transform *color_matrix = NULL;
+	struct wlr_color_transform *inv_eotf = NULL;
+	struct wlr_color_transform *user_gamma = NULL;
+
+	if (img_desc != NULL) {
+		assert(supplied == NULL);
+		struct wlr_color_primaries primaries_srgb;
+		wlr_color_primaries_from_named(&primaries_srgb, WLR_COLOR_NAMED_PRIMARIES_SRGB);
+		struct wlr_color_primaries primaries;
+		wlr_color_primaries_from_named(&primaries, img_desc->primaries);
+		float matrix[9];
+		wlr_color_primaries_transform_absolute_colorimetric(&primaries_srgb, &primaries, matrix);
+
+		struct wlr_color_luminances srgb_lum, dst_lum;
+		wlr_color_transfer_function_get_default_luminance(
+			WLR_COLOR_TRANSFER_FUNCTION_SRGB, &srgb_lum);
+		wlr_color_transfer_function_get_default_luminance(img_desc->transfer_function, &dst_lum);
+		float luminance_multiplier = get_luminance_multiplier(&srgb_lum, &dst_lum);
+		for (int i = 0; i < 9; ++i) {
+			matrix[i] *= luminance_multiplier;
+		}
+
+		color_matrix = wlr_color_transform_init_matrix(matrix);
+		inv_eotf = wlr_color_transform_init_linear_to_inverse_eotf(img_desc->transfer_function);
+		if (color_matrix == NULL || inv_eotf == NULL) {
+			goto cleanup_transforms;
+		}
+	} else if (supplied != NULL) {
+		inv_eotf = wlr_color_transform_ref(supplied);
+	} else {
+		inv_eotf = wlr_color_transform_init_linear_to_inverse_eotf(
+			WLR_COLOR_TRANSFER_FUNCTION_GAMMA22);
+		if (inv_eotf == NULL) {
+			goto cleanup_transforms;
+		}
+	}
+
+	struct wlr_color_transform *gamma_lut = scene_output->gamma_lut_color_transform;
+	if (gamma_lut != NULL && render_gamma_lut) {
+		user_gamma = wlr_color_transform_ref(gamma_lut);
+	}
+
+	struct wlr_color_transform *combined;
+	struct wlr_color_transform *transforms[] = {
+		color_matrix,
+		inv_eotf,
+		user_gamma,
+	};
+	const size_t transforms_len = sizeof(transforms) / sizeof(transforms[0]);
+	if (!color_transform_compose(&combined, transforms, transforms_len)) {
+		goto cleanup_transforms;
+	}
+
+	wlr_color_transform_unref(scene_output->prev_gamma_lut_color_transform);
+	scene_output->prev_gamma_lut_color_transform = gamma_lut ? wlr_color_transform_ref(gamma_lut) : NULL;
+	wlr_color_transform_unref(scene_output->prev_supplied_color_transform);
+	scene_output->prev_supplied_color_transform = supplied ? wlr_color_transform_ref(supplied) : NULL;
+	wlr_color_transform_unref(scene_output->combined_color_transform);
+	scene_output->combined_color_transform = combined;
+
+	result = true;
+
+cleanup_transforms:
+	wlr_color_transform_unref(color_matrix);
+	wlr_color_transform_unref(inv_eotf);
+	wlr_color_transform_unref(user_gamma);
+	return result;
+}
+
 bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 		struct wlr_output_state *state, const struct wlr_scene_output_state_options *options) {
 	struct wlr_scene_output_state_options default_options = {0};
@@ -2754,12 +3095,21 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 	enum wlr_scene_debug_damage_option debug_damage =
 		scene_output->scene->debug_damage_option;
 
+	bool render_gamma_lut = false;
+	if (wlr_output_get_gamma_size(output) == 0 && output->renderer->features.output_color_transform) {
+		if (scene_output->gamma_lut_color_transform != scene_output->prev_gamma_lut_color_transform) {
+			scene_output_damage_whole(scene_output);
+		}
+		if (scene_output->gamma_lut_color_transform != NULL) {
+			render_gamma_lut = true;
+		}
+	}
+
 	struct render_data render_data = {
 		.transform = output->transform,
 		.scale = output->scale,
 		.logical = { .x = scene_output->x, .y = scene_output->y },
 		.output = scene_output,
-		.has_blur = false,
 	};
 
 	int resolution_width, resolution_height;
@@ -2831,7 +3181,7 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 		pixman_region32_init(&acc_damage);
 		struct highlight_region *damage, *tmp_damage;
 		wl_list_for_each_safe(damage, tmp_damage, regions, link) {
-			// remove overlaping damage regions
+			// remove overlapping damage regions
 			pixman_region32_subtract(&damage->region, &damage->region, &acc_damage);
 			pixman_region32_union(&acc_damage, &acc_damage, &damage->region);
 
@@ -2855,7 +3205,7 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 	// - There are no color transforms that need to be applied
 	// - Damage highlight debugging is not enabled
 	enum scene_direct_scanout_result scanout_result = SCANOUT_INELIGIBLE;
-	if (options->color_transform == NULL && list_len == 1
+	if (options->color_transform == NULL && !render_gamma_lut && list_len == 1
 			&& debug_damage != WLR_SCENE_DEBUG_DAMAGE_HIGHLIGHT) {
 		scanout_result = scene_entry_try_direct_scanout(&list_data[0], state, &render_data);
 	}
@@ -2887,12 +3237,15 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 			timespec_sub(&duration, &end_time, &start_time);
 			timer->pre_render_duration = timespec_to_nsec(&duration);
 		}
+
+		TRACY_MARK_FRAME;
 		return true;
 	}
 
 	struct wlr_swapchain *swapchain = options->swapchain;
 	if (!swapchain) {
 		if (!wlr_output_configure_primary_swapchain(output, state, &output->swapchain)) {
+			TRACY_MARK_FRAME;
 			return false;
 		}
 
@@ -2901,6 +3254,7 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 
 	struct wlr_buffer *buffer = wlr_swapchain_acquire(swapchain);
 	if (buffer == NULL) {
+		TRACY_MARK_FRAME;
 		return false;
 	}
 
@@ -2915,78 +3269,120 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 		timer->pre_render_duration = timespec_to_nsec(&duration);
 	}
 
+	if ((render_gamma_lut
+			&& scene_output->gamma_lut_color_transform != scene_output->prev_gamma_lut_color_transform)
+			|| scene_output->prev_supplied_color_transform != options->color_transform
+			|| (state->committed & WLR_OUTPUT_STATE_IMAGE_DESCRIPTION)) {
+		const struct wlr_output_image_description *output_description =
+			output_pending_image_description(output, state);
+		if (!scene_output_combine_color_transforms(scene_output, options->color_transform,
+				output_description, render_gamma_lut)) {
+			wlr_buffer_unlock(buffer);
+			return false;
+		}
+	}
+
 	scene_output->in_point++;
-	struct fx_gles_render_pass *render_pass =
-		fx_renderer_begin_buffer_pass(output->renderer, buffer, output,
-				&(struct fx_buffer_pass_options) {
-					.base = &(struct wlr_buffer_pass_options){
-						.timer = timer ? timer->render_timer : NULL,
-						.color_transform = options->color_transform,
-						.signal_timeline = scene_output->in_timeline,
-						.signal_point = scene_output->in_point,
-					},
-					.swapchain = swapchain,
-				}
-			);
+	struct wlr_render_pass *render_pass = wlr_renderer_begin_buffer_pass(output->renderer, buffer,
+			&(struct wlr_buffer_pass_options){
+		.timer = timer ? timer->render_timer : NULL,
+		.color_transform = scene_output->combined_color_transform,
+		.signal_timeline = scene_output->in_timeline,
+		.signal_point = scene_output->in_point,
+	});
 	if (render_pass == NULL) {
 		wlr_buffer_unlock(buffer);
+
+		TRACY_MARK_FRAME;
 		return false;
 	}
-	struct fx_effect_framebuffers *effect_fbos = render_pass->fx_effect_framebuffers;
 
 	render_data.render_pass = render_pass;
+
 	pixman_region32_init(&render_data.damage);
 	wlr_damage_ring_rotate_buffer(&scene_output->damage_ring, buffer,
 		&render_data.damage);
 
-	// Blur artifact prevention
-	pixman_region32_t blur_region;
-	pixman_region32_init(&blur_region);
-	render_data.has_blur =
-		scene_output_has_blur(list_len, list_data, scene_output, &blur_region);
-	bool whole_output_blur_damaged = false;
-	// Expand the damage to compensate for blur
-	if (render_data.has_blur) {
-		int output_width = output->width;
-		int output_height = output->height;
-		struct blur_data *blur_data = &scene_output->scene->blur_data;
-		pixman_region32_t *damage = &render_data.damage;
-
-		// ensure that the damage isn't expanding past the output's size
-		int32_t damage_width = damage->extents.x2 - damage->extents.x1;
-		int32_t damage_height = damage->extents.y2 - damage->extents.y1;
-		if (damage_width > output_width || damage_height > output_height) {
-			pixman_region32_intersect_rect(damage, damage,
-					0, 0, output_width, output_height);
-			// No need to compensate for blur artifacts when the damage spans
-			// the whole output
-			whole_output_blur_damaged = true;
-		}else {
-			// copy the surrounding content where the blur would display artifacts
-			// and draw it above the artifacts
-			pixman_region32_t extended_damage;
-			pixman_region32_init(&extended_damage);
-			pixman_region32_intersect(&extended_damage, damage, &blur_region);
-			// Expand the region to compensate for blur artifacts
-			wlr_region_expand(&extended_damage, &extended_damage, blur_data_calc_size(blur_data));
-			// Limit to the monitors viewport
-			pixman_region32_intersect_rect(&extended_damage, &extended_damage,
-					0, 0, output_width, output_height);
-
-			// capture the padding pixels around the blur where artifacts will be drawn
-			pixman_region32_subtract(&effect_fbos->blur_padding_region,
-					&extended_damage, damage);
-			// Combine into the surface damage (we need to redraw the padding area as well)
-			pixman_region32_union(damage, damage, &extended_damage);
-			pixman_region32_fini(&extended_damage);
-
-			// Capture the padding pixels before blur for later use
-			fx_renderer_read_to_buffer(render_pass, &effect_fbos->blur_padding_region,
-					effect_fbos->blur_saved_pixels_buffer,
-					render_pass->buffer);
-		}
-	}
-	pixman_region32_fini(&blur_region);
+	struct fx_gles_render_pass *fx_pass = fx_get_render_pass(render_pass);
+	bool should_compensate_blur = false;
+	if (fx_render_pass_init_offscreen_buffers(render_pass, output)
+			&& pixman_region32_not_empty(&render_data.damage)) {
+		// Blur artifact prevention
+		// Note: Supports individual blur node blur_data
+		pixman_region32_t original_damage;
+		pixman_region32_init(&original_damage);
+		pixman_region32_copy(&original_damage, &render_data.damage);
+
+		// Only compensate for blur artifacts when the damage doesn't span
+		// the whole output
+		const bool full_damage =
+			original_damage.extents.x2 - original_damage.extents.x1 >= output->width
+			&& original_damage.extents.y2 - original_damage.extents.y1 >= output->height;
+
+		// The extra region we copy and paste onto the framebuffer after render
+		// for artifact removal
+		pixman_region32_t blur_padding_region;
+		pixman_region32_init(&blur_padding_region);
+
+		// Check if the original damage expanded by the to-be-rendered blur
+		// nodes sampling size intersects with said nodes visible region. The
+		// intersection gets expanded once more which becomes the region to be
+		// copied before render and pasted onto the framebuffer after render
+		// (the region where artifacts will be visible). This method fixes blur
+		// nodes not updating properly when nearby, non-blurred nodes get
+		// damaged while avoiding to re-render the whole nodes blur region. It
+		// also allows for node-individual blur_data if needed in the future.
+		for (int i = list_len - 1; i >= 0; i--) {
+			struct render_list_entry *entry = &list_data[i];
+			struct wlr_scene_node *node = entry->node;
+			struct blur_data blur_data = scene_output->scene->blur_data;
+			if (!should_blur_node_extend_damage(node, fx_pass, &blur_data)) {
+				continue;
+			}
+
+			// End early. No need to compensate for blur if the whole output
+			// is damaged anyway, but still make sure to let the renderer know that
+			// there's blur nodes to render.
+			if (full_damage) {
+				should_compensate_blur = false;
+				fx_pass->has_blur = true;
+				break;
+			}
+
+			if (apply_blur_region(node, &blur_data, &render_data, state,
+						&original_damage, &blur_padding_region)) {
+				should_compensate_blur = true;
+				fx_pass->has_blur = true;
+			}
+		}
+
+		// Expand the damage to compensate for blur
+		if (should_compensate_blur) {
+			// Capture the padding pixels around the blur where artifacts will be drawn
+			pixman_region32_subtract(&fx_pass->blur_padding_region,
+					&blur_padding_region, &render_data.damage);
+			// Make sure that the padding and damage doesn't exceed the output bounds
+			pixman_region32_intersect_rect(&fx_pass->blur_padding_region, &fx_pass->blur_padding_region,
+					0, 0, output->width, output->height);
+
+			// Combine with the render damage (we need to redraw the padding area as well)
+			pixman_region32_union(&render_data.damage,
+					&render_data.damage, &fx_pass->blur_padding_region);
+			pixman_region32_intersect_rect(&render_data.damage, &render_data.damage,
+					0, 0, output->width, output->height);
+
+			// Copy the surrounding content where the blur would display artifacts
+			// and draw it above the artifacts. Otherwise The old rendered
+			// content would be included into the new blur. This means that
+			// content like a high z-index toplevel would be included into the
+			// blur of a toplevel with a low z-index.
+			fx_render_pass_read_to_buffer(fx_pass, &fx_pass->blur_padding_region,
+					fx_pass->fx_offscreen_buffers->blur_saved_pixels_buffer, fx_pass->buffer);
+		}
+
+		pixman_region32_fini(&blur_padding_region);
+		pixman_region32_fini(&original_damage);
+	}
 
 	pixman_region32_t background;
 	pixman_region32_init(&background);
@@ -3024,7 +3420,7 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 		}
 	}
 
-	wlr_render_pass_add_rect(&render_pass->base, &(struct wlr_render_rect_options){
+	wlr_render_pass_add_rect(render_pass, &(struct wlr_render_rect_options){
 		.box = { .width = buffer->width, .height = buffer->height },
 		.color = { .r = 0, .g = 0, .b = 0, .a = 1 },
 		.clip = &background,
@@ -3053,12 +3449,6 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 		}
 	}
 
-	if (render_data.has_blur && !whole_output_blur_damaged) {
-		// Render the saved pixels over the blur artifacts
-		fx_renderer_read_to_buffer(render_pass, &effect_fbos->blur_padding_region,
-				render_pass->buffer, effect_fbos->blur_saved_pixels_buffer);
-	}
-
 	if (debug_damage == WLR_SCENE_DEBUG_DAMAGE_HIGHLIGHT) {
 		struct highlight_region *damage;
 		wl_list_for_each(damage, &scene_output->damage_highlight_regions, link) {
@@ -3067,7 +3457,7 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 			int64_t time_diff_ms = timespec_to_msec(&time_diff);
 			float alpha = 1.0 - (double)time_diff_ms / HIGHLIGHT_DAMAGE_FADEOUT_TIME;
 
-			wlr_render_pass_add_rect(&render_pass->base, &(struct wlr_render_rect_options){
+			wlr_render_pass_add_rect(render_pass, &(struct wlr_render_rect_options){
 				.box = { .width = buffer->width, .height = buffer->height },
 				.color = { .r = alpha * 0.5, .g = 0, .b = 0, .a = alpha * 0.5 },
 				.clip = &damage->region,
@@ -3075,15 +3465,24 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 		}
 	}
 
-	wlr_output_add_software_cursors_to_render_pass(output, &render_pass->base, &render_data.damage);
+	wlr_output_add_software_cursors_to_render_pass(output, render_pass, &render_data.damage);
+
+	if (should_compensate_blur) {
+		// Render the saved pixels over the blur artifacts
+		fx_render_pass_read_to_buffer(fx_pass, &fx_pass->blur_padding_region,
+				fx_pass->buffer, fx_pass->fx_offscreen_buffers->blur_saved_pixels_buffer);
+	}
+
 	pixman_region32_fini(&render_data.damage);
 
-	if (!wlr_render_pass_submit(&render_pass->base)) {
+	if (!wlr_render_pass_submit(render_pass)) {
 		wlr_buffer_unlock(buffer);
 
 		// if we failed to render the buffer, it will have undefined contents
 		// Trash the damage ring
 		wlr_damage_ring_add_whole(&scene_output->damage_ring);
+
+		TRACY_MARK_FRAME;
 		return false;
 	}
 
@@ -3093,10 +3492,16 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output,
 	if (scene_output->in_timeline != NULL) {
 		wlr_output_state_set_wait_timeline(state, scene_output->in_timeline,
 			scene_output->in_point);
+		scene_output->out_point++;
+		wlr_output_state_set_signal_timeline(state, scene_output->out_timeline,
+			scene_output->out_point);
 	}
 
-	scene_output_state_attempt_gamma(scene_output, state);
+	if (!render_gamma_lut) {
+		scene_output_state_attempt_gamma(scene_output, state);
+	}
 
+	TRACY_MARK_FRAME;
 	return true;
 }
 
@@ -3124,10 +3529,11 @@ static void scene_node_send_frame_done(struct wlr_scene_node *node,
 	if (node->type == WLR_SCENE_NODE_BUFFER) {
 		struct wlr_scene_buffer *scene_buffer =
 			wlr_scene_buffer_from_node(node);
-
-		if (scene_buffer->primary_output == scene_output) {
-			wlr_scene_buffer_send_frame_done(scene_buffer, now);
-		}
+		struct wlr_scene_frame_done_event event = {
+			.output = scene_output,
+			.when = *now,
+		};
+		wlr_scene_buffer_send_frame_done(scene_buffer, &event);
 	} else if (node->type == WLR_SCENE_NODE_TREE) {
 		struct wlr_scene_tree *scene_tree = wlr_scene_tree_from_node(node);
 		struct wlr_scene_node *child;
diff --git a/scenefx/util/array.c b/scenefx/util/array.c
index ec16a7b..d4771e8 100644
--- a/scenefx/util/array.c
+++ b/scenefx/util/array.c
@@ -14,7 +14,7 @@ bool array_realloc(struct wl_array *arr, size_t size) {
 	// If the size is less than 1/4th of the allocation size, we shrink it.
 	// 1/4th is picked to provide hysteresis, without which an array with size
 	// arr->alloc would constantly reallocate if an element is added and then
-	// removed continously.
+	// removed continuously.
 	size_t alloc;
 	if (arr->alloc > 0 && size > arr->alloc / 4) {
 		alloc = arr->alloc;
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 95896b1..933c980 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -391,12 +391,10 @@ impl Cursor {
                 if !wlr_xcursor.is_null() && (*wlr_xcursor).image_count > 0 {
                     let image = *(*wlr_xcursor).images;
                     if !image.is_null() {
+                        let buffer = ffi::wlr_xcursor_image_get_buffer(image);
                         ffi::wlr_xwayland_set_cursor(
                             (*(*self.seat).server).xwayland,
-                            (*image).buffer,
-                            (*image).width * 4,
-                            (*image).width,
-                            (*image).height,
+                            buffer,
                             (*image).hotspot_x as i32,
                             (*image).hotspot_y as i32,
                         );
diff --git a/src/server/input_manager.rs b/src/server/input_manager.rs
index 4a22e27..fcc1f3d 100644
--- a/src/server/input_manager.rs
+++ b/src/server/input_manager.rs
@@ -87,12 +87,12 @@ impl InputManager {
         // Connect new_text_input listener
         let new_text_input_ptr = &mut self.new_text_input as *mut ffi::wl_listener as *mut WlListener;
         (*new_text_input_ptr).notify = Some(handle_new_text_input);
-        wl_signal_add(&mut (*self.text_input_manager).events.text_input, &mut self.new_text_input);
+        wl_signal_add(&mut (*self.text_input_manager).events.new_text_input, &mut self.new_text_input);
 
         // Connect new_input_method listener
         let new_input_method_ptr = &mut self.new_input_method as *mut ffi::wl_listener as *mut WlListener;
         (*new_input_method_ptr).notify = Some(handle_new_input_method);
-        wl_signal_add(&mut (*self.input_method_manager).events.input_method, &mut self.new_input_method);
+        wl_signal_add(&mut (*self.input_method_manager).events.new_input_method, &mut self.new_input_method);
 
         // Connect new_virtual_pointer listener
         let new_virtual_pointer_ptr = &mut self.new_virtual_pointer_listener as *mut ffi::wl_listener as *mut WlListener;
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index f0626b7..15e605e 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -652,35 +652,79 @@ void river_wlr_keyboard_init(struct wlr_keyboard *keyboard,
 	wlr_keyboard_init(keyboard, &impl, name);
 }
 
-struct river_blur_data {
-	bool enabled;
-	bool optimized;
-	bool ignore_transparent;
-};
+static struct wlr_scene_node *find_blur_node(struct wlr_scene_tree *tree) {
+	struct wlr_scene_node *child;
+	wl_list_for_each(child, &tree->children, link) {
+		if (child->type == WLR_SCENE_NODE_BLUR || child->type == WLR_SCENE_NODE_OPTIMIZED_BLUR) {
+			return child;
+		}
+	}
+	return NULL;
+}
 
-static void enable_blur_iterator(struct wlr_scene_buffer *buffer, int sx, int sy, void *user_data) {
+static void get_size_iterator(struct wlr_scene_buffer *buffer, int sx, int sy, void *user_data) {
 	(void)sx;
 	(void)sy;
-	struct wlr_scene_surface *scene_surface = wlr_scene_surface_try_from_buffer(buffer);
-	if (!scene_surface) {
+	int *size = (int *)user_data;
+	if (buffer->dst_width > size[0]) size[0] = buffer->dst_width;
+	if (buffer->dst_height > size[1]) size[1] = buffer->dst_height;
+}
+
+void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled, bool optimized, bool ignore_transparent) {
+	(void)ignore_transparent;
+	if (node->type != WLR_SCENE_NODE_TREE) {
 		return;
 	}
-	struct river_blur_data *data = (struct river_blur_data *)user_data;
-	bool enabled = data->enabled;
-	bool optimized = data->optimized && enabled;
-	bool ignore_transparent = data->ignore_transparent && enabled;
-	if (buffer->backdrop_blur != enabled ||
-		buffer->backdrop_blur_optimized != optimized ||
-		buffer->backdrop_blur_ignore_transparent != ignore_transparent) {
-		wlr_scene_buffer_set_backdrop_blur(buffer, enabled);
-		wlr_scene_buffer_set_backdrop_blur_optimized(buffer, optimized);
-		wlr_scene_buffer_set_backdrop_blur_ignore_transparent(buffer, ignore_transparent);
+	struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node);
+	struct wlr_scene_node *blur_node = find_blur_node(tree);
+
+	if (!enabled) {
+		if (blur_node) {
+			wlr_scene_node_destroy(blur_node);
+		}
+		return;
 	}
-}
 
-void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled, bool optimized, bool ignore_transparent) {
-	struct river_blur_data data = { .enabled = enabled, .optimized = optimized, .ignore_transparent = ignore_transparent };
-	wlr_scene_node_for_each_buffer(node, enable_blur_iterator, &data);
+	int width = 0, height = 0;
+	int size[2] = {0, 0};
+	wlr_scene_node_for_each_buffer(node, get_size_iterator, size);
+	width = size[0];
+	height = size[1];
+
+	if (width <= 0 || height <= 0) {
+		width = 1920; // Fallback defaults
+		height = 1080;
+	}
+
+	enum wlr_scene_node_type expected_type = optimized ? WLR_SCENE_NODE_OPTIMIZED_BLUR : WLR_SCENE_NODE_BLUR;
+	if (blur_node && blur_node->type != expected_type) {
+		wlr_scene_node_destroy(blur_node);
+		blur_node = NULL;
+	}
+
+	if (!blur_node) {
+		if (optimized) {
+			struct wlr_scene_optimized_blur *opt_blur = wlr_scene_optimized_blur_create(tree, width, height);
+			if (opt_blur) {
+				blur_node = &opt_blur->node;
+			}
+		} else {
+			struct wlr_scene_blur *std_blur = wlr_scene_blur_create(tree, width, height);
+			if (std_blur) {
+				wlr_scene_blur_set_should_only_blur_bottom_layer(std_blur, true);
+				blur_node = &std_blur->node;
+			}
+		}
+		if (blur_node) {
+			wlr_scene_node_lower_to_bottom(blur_node);
+		}
+	} else {
+		if (optimized) {
+			wlr_scene_optimized_blur_set_size((struct wlr_scene_optimized_blur *)blur_node, width, height);
+		} else {
+			wlr_scene_blur_set_size((struct wlr_scene_blur *)blur_node, width, height);
+		}
+	}
 }
 
 static void set_opacity_iterator(struct wlr_scene_buffer *buffer, int sx, int sy, void *user_data) {
@@ -700,13 +744,18 @@ static void set_corner_radius_iterator(struct wlr_scene_buffer *buffer, int sx,
 	(void)sx;
 	(void)sy;
 	int radius = *(int *)user_data;
-	if (buffer->corner_radius != radius || buffer->corners != CORNER_LOCATION_ALL) {
-		wlr_scene_buffer_set_corner_radius(buffer, radius, CORNER_LOCATION_ALL);
-	}
+	wlr_scene_buffer_set_corner_radius(buffer, radius);
 }
 
 void river_scene_node_set_corner_radius(struct wlr_scene_node *node, int radius) {
 	wlr_scene_node_for_each_buffer(node, set_corner_radius_iterator, &radius);
+	if (node->type == WLR_SCENE_NODE_TREE) {
+		struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node);
+		struct wlr_scene_node *blur_node = find_blur_node(tree);
+		if (blur_node && blur_node->type == WLR_SCENE_NODE_BLUR) {
+			wlr_scene_blur_set_corner_radius((struct wlr_scene_blur *)blur_node, radius);
+		}
+	}
 }
 
 void river_scene_buffer_set_dest_size_if_changed(struct wlr_scene_buffer *scene_buffer, int width, int height) {
@@ -728,7 +777,7 @@ void river_scene_rect_set_size_if_changed(struct wlr_scene_rect *rect, int width
 }
 
 void river_scene_rect_set_corner_radius(struct wlr_scene_rect *rect, int radius) {
-	wlr_scene_rect_set_corner_radius(rect, radius, CORNER_LOCATION_ALL);
+	wlr_scene_rect_set_corner_radius(rect, radius);
 }
 
 int river_scene_buffer_get_width(struct wlr_scene_buffer *scene_buffer) {
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index 709f578..b8b7101 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -793,8 +793,19 @@ unsafe extern "C" fn handle_decoration_request_mode(listener: *mut ffi::wl_liste
     (*window).set_decoration_hint(hint);
 
     if ffi::river_wlr_xdg_surface_get_initialized(base) {
-        let mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
+        let mut mode = (*(*decoration).wlr_decoration).requested_mode;
+        if mode == ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_NONE {
+            let server = (*window).server;
+            let rule_ssd = (*server).wm.get_rule_for_window(window).and_then(|r| r.ssd);
+            if let Some(true) = rule_ssd {
+                mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
+            } else {
+                mode = ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE;
+            }
+        }
         ffi::wlr_xdg_toplevel_decoration_v1_set_mode((*decoration).wlr_decoration, mode);
+        (*window).wm_requested.ssd = mode == ffi::wlr_xdg_toplevel_decoration_v1_mode_WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE;
+        (*(*window).server).wm.dirty_windowing();
     }
 }