diff --git a/src/wayland/hyprland/surface/hyprland-surface-v1.xml b/src/wayland/hyprland/surface/hyprland-surface-v1.xml
index 2f683365..c4b1424f 100644
--- a/src/wayland/hyprland/surface/hyprland-surface-v1.xml
+++ b/src/wayland/hyprland/surface/hyprland-surface-v1.xml
@@ -34,7 +34,7 @@
     This protocol exposes hyprland-specific wl_surface properties.
   </description>
 
-  <interface name="hyprland_surface_manager_v1" version="1">
+  <interface name="hyprland_surface_manager_v1" version="2">
     <description summary="manager for hyprland surface objects">
       This interface allows a client to create hyprland surface objects.
     </description>
@@ -63,7 +63,7 @@
     </enum>
   </interface>
 
-  <interface name="hyprland_surface_v1" version="1">
+  <interface name="hyprland_surface_v1" version="2">
     <description summary="hyprland-specific wl_surface properties">
       This interface allows access to hyprland-specific properties of a wl_surface.
 
@@ -96,5 +96,31 @@
       <entry name="no_surface" value="0" summary="wl_surface was destroyed"/>
       <entry name="out_of_range" value="1" summary="given opacity was not in the range 0.0 - 1.0 (inclusive)"/>
     </enum>
+
+    <request name="set_visible_region" since="2">
+      <description summary="set the visible region of the surface">
+        This request sets the region of the surface that contains visible content.
+        Visible content refers to content that has an alpha value greater than zero.
+
+        The visible region is an optimization hint for the compositor that lets it
+        avoid drawing parts of the surface that are not visible. Setting a visible region
+        that does not contain all content in the surface may result in missing content
+        not being drawn.
+
+        The visible region is specified in buffer-local coordinates.
+
+        The compositor ignores the parts of the visible region that fall outside of the surface.
+        When all parts of the region fall outside of the buffer geometry, the compositor may
+        avoid rendering the surface entirely.
+
+        The initial value for the visible region is empty. Setting the
+        visible region has copy semantics, and the wl_region object can be destroyed immediately.
+        A NULL wl_region causes the visible region to be set to empty.
+
+        Does not take effect until wl_surface.commit is called.
+      </description>
+
+      <arg name="region" type="object" interface="wl_region" allow-null="true"/>
+    </request>
   </interface>
 </protocol>
diff --git a/src/wayland/hyprland/surface/manager.cpp b/src/wayland/hyprland/surface/manager.cpp
index 31829bb6..6354255e 100644
--- a/src/wayland/hyprland/surface/manager.cpp
+++ b/src/wayland/hyprland/surface/manager.cpp
@@ -7,13 +7,13 @@
 
 namespace qs::hyprland::surface::impl {
 
-HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(1) {
+HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(2) {
 	this->initialize();
 }
 
 HyprlandSurface*
 HyprlandSurfaceManager::createHyprlandExtension(QtWaylandClient::QWaylandWindow* surface) {
-	return new HyprlandSurface(this->get_hyprland_surface(surface->surface()));
+	return new HyprlandSurface(this->get_hyprland_surface(surface->surface()), surface);
 }
 
 HyprlandSurfaceManager* HyprlandSurfaceManager::instance() {
diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp
index 5150487f..b00ee33e 100644
--- a/src/wayland/hyprland/surface/qml.cpp
+++ b/src/wayland/hyprland/surface/qml.cpp
@@ -1,17 +1,20 @@
 #include "qml.hpp"
 #include <memory>
 
+#include <private/qhighdpiscaling_p.h>
 #include <private/qwaylandwindow_p.h>
 #include <qlogging.h>
 #include <qobject.h>
 #include <qqmlinfo.h>
+#include <qregion.h>
 #include <qtmetamacros.h>
 #include <qtypes.h>
+#include <qvariant.h>
 #include <qwindow.h>
 
+#include "../../../core/region.hpp"
 #include "../../../window/proxywindow.hpp"
 #include "../../../window/windowinterface.hpp"
-#include "../../util.hpp"
 #include "manager.hpp"
 #include "surface.hpp"
 
@@ -40,6 +43,15 @@ HyprlandWindow::HyprlandWindow(ProxyWindowBase* window): QObject(nullptr), proxy
 	    &HyprlandWindow::onWindowConnected
 	);
 
+	QObject::connect(window, &ProxyWindowBase::polished, this, &HyprlandWindow::onWindowPolished);
+
+	QObject::connect(
+	    window,
+	    &ProxyWindowBase::devicePixelRatioChanged,
+	    this,
+	    &HyprlandWindow::updateVisibleMask
+	);
+
 	QObject::connect(window, &QObject::destroyed, this, &HyprlandWindow::onProxyWindowDestroyed);
 
 	if (window->backingWindow()) {
@@ -60,14 +72,76 @@ void HyprlandWindow::setOpacity(qreal opacity) {
 
 	this->mOpacity = opacity;
 
-	if (this->surface) {
-		this->surface->setOpacity(opacity);
-		qs::wayland::util::scheduleCommit(this->proxyWindow);
+	if (this->surface && this->proxyWindow) {
+		this->pendingPolish.opacity = true;
+		this->proxyWindow->schedulePolish();
 	}
 
 	emit this->opacityChanged();
 }
 
+PendingRegion* HyprlandWindow::visibleMask() const { return this->mVisibleMask; }
+
+void HyprlandWindow::setVisibleMask(PendingRegion* mask) {
+	if (mask == this->mVisibleMask) return;
+
+	if (this->mVisibleMask) {
+		QObject::disconnect(this->mVisibleMask, nullptr, this, nullptr);
+	}
+
+	this->mVisibleMask = mask;
+
+	if (mask) {
+		QObject::connect(mask, &QObject::destroyed, this, &HyprlandWindow::onVisibleMaskDestroyed);
+		QObject::connect(mask, &PendingRegion::changed, this, &HyprlandWindow::updateVisibleMask);
+	}
+
+	this->updateVisibleMask();
+	emit this->visibleMaskChanged();
+}
+
+void HyprlandWindow::onVisibleMaskDestroyed() {
+	this->mVisibleMask = nullptr;
+	this->updateVisibleMask();
+	emit this->visibleMaskChanged();
+}
+
+void HyprlandWindow::updateVisibleMask() {
+	if (!this->surface || !this->proxyWindow) return;
+
+	this->pendingPolish.visibleMask = true;
+	this->proxyWindow->schedulePolish();
+}
+
+void HyprlandWindow::onWindowPolished() {
+	if (!this->surface) return;
+
+	if (this->pendingPolish.opacity) {
+		this->surface->setOpacity(this->mOpacity);
+		this->pendingPolish.opacity = false;
+	}
+
+	if (this->pendingPolish.visibleMask) {
+		QRegion mask;
+		if (this->mVisibleMask != nullptr) {
+			mask =
+			    this->mVisibleMask->applyTo(QRect(0, 0, this->mWindow->width(), this->mWindow->height()));
+		}
+
+		auto dpr = this->proxyWindow->devicePixelRatio();
+		if (dpr != 1.0) {
+			mask = QHighDpi::scale(mask, dpr);
+		}
+
+		if (mask.isEmpty() && this->mVisibleMask) {
+			mask = QRect(-1, -1, 1, 1);
+		}
+
+		this->surface->setVisibleRegion(mask);
+		this->pendingPolish.visibleMask = false;
+	}
+}
+
 void HyprlandWindow::onWindowConnected() {
 	this->mWindow = this->proxyWindow->backingWindow();
 	// disconnected by destructor
@@ -86,33 +160,46 @@ void HyprlandWindow::onWindowVisibleChanged() {
 		if (!this->mWindow->handle()) {
 			this->mWindow->create();
 		}
+	}
 
-		this->mWaylandWindow = dynamic_cast<QWaylandWindow*>(this->mWindow->handle());
+	auto* window = dynamic_cast<QWaylandWindow*>(this->mWindow->handle());
+	if (window == this->mWaylandWindow) return;
 
-		if (this->mWaylandWindow) {
-			// disconnected by destructor
+	if (this->mWaylandWindow) {
+		QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr);
+	}
 
-			QObject::connect(
-			    this->mWaylandWindow,
-			    &QWaylandWindow::surfaceCreated,
-			    this,
-			    &HyprlandWindow::onWaylandSurfaceCreated
-			);
+	this->mWaylandWindow = window;
+	if (!window) return;
 
-			QObject::connect(
-			    this->mWaylandWindow,
-			    &QWaylandWindow::surfaceDestroyed,
-			    this,
-			    &HyprlandWindow::onWaylandSurfaceDestroyed
-			);
+	QObject::connect(
+	    this->mWaylandWindow,
+	    &QObject::destroyed,
+	    this,
+	    &HyprlandWindow::onWaylandWindowDestroyed
+	);
 
-			if (this->mWaylandWindow->surface()) {
-				this->onWaylandSurfaceCreated();
-			}
-		}
+	QObject::connect(
+	    this->mWaylandWindow,
+	    &QWaylandWindow::surfaceCreated,
+	    this,
+	    &HyprlandWindow::onWaylandSurfaceCreated
+	);
+
+	QObject::connect(
+	    this->mWaylandWindow,
+	    &QWaylandWindow::surfaceDestroyed,
+	    this,
+	    &HyprlandWindow::onWaylandSurfaceDestroyed
+	);
+
+	if (this->mWaylandWindow->surface()) {
+		this->onWaylandSurfaceCreated();
 	}
 }
 
+void HyprlandWindow::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; }
+
 void HyprlandWindow::onWaylandSurfaceCreated() {
 	auto* manager = impl::HyprlandSurfaceManager::instance();
 
@@ -122,12 +209,26 @@ void HyprlandWindow::onWaylandSurfaceCreated() {
 		return;
 	}
 
-	auto* ext = manager->createHyprlandExtension(this->mWaylandWindow);
-	this->surface = std::unique_ptr<impl::HyprlandSurface>(ext);
+	auto v = this->mWaylandWindow->property("hyprland_window_ext");
+	if (v.canConvert<HyprlandWindow*>()) {
+		auto* windowExt = v.value<HyprlandWindow*>();
+		if (windowExt != this && windowExt->surface) {
+			this->surface.swap(windowExt->surface);
+		}
+	}
 
-	if (this->mOpacity != 1.0) {
-		this->surface->setOpacity(this->mOpacity);
-		qs::wayland::util::scheduleCommit(this->proxyWindow);
+	if (!this->surface) {
+		auto* ext = manager->createHyprlandExtension(this->mWaylandWindow);
+		this->surface = std::unique_ptr<impl::HyprlandSurface>(ext);
+	}
+
+	this->mWaylandWindow->setProperty("hyprland_window_ext", QVariant::fromValue(this));
+
+	this->pendingPolish.opacity = this->mOpacity != 1.0;
+	this->pendingPolish.visibleMask = this->mVisibleMask;
+
+	if (this->pendingPolish.opacity || this->pendingPolish.visibleMask) {
+		this->proxyWindow->schedulePolish();
 	}
 }
 
@@ -144,8 +245,9 @@ void HyprlandWindow::onProxyWindowDestroyed() {
 	// Deleting it when the proxy window is deleted will cause a full opacity frame between the destruction of the
 	// hyprland_surface_v1 and wl_surface objects.
 
+	this->proxyWindow = nullptr;
+
 	if (this->surface == nullptr) {
-		this->proxyWindow = nullptr;
 		this->deleteLater();
 	}
 }
diff --git a/src/wayland/hyprland/surface/qml.hpp b/src/wayland/hyprland/surface/qml.hpp
index ce32a967..157b8f32 100644
--- a/src/wayland/hyprland/surface/qml.hpp
+++ b/src/wayland/hyprland/surface/qml.hpp
@@ -9,6 +9,7 @@
 #include <qtypes.h>
 #include <qwindow.h>
 
+#include "../../../core/region.hpp"
 #include "../../../window/proxywindow.hpp"
 #include "surface.hpp"
 
@@ -31,11 +32,18 @@ namespace qs::hyprland::surface {
 /// [hyprland-surface-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-surface-v1.xml
 class HyprlandWindow: public QObject {
 	Q_OBJECT;
+	// clang-format off
 	/// A multiplier for the window's overall opacity, ranging from 1.0 to 0.0. Overall opacity includes the opacity of
 	/// both the window content *and* visual effects such as blur that apply to it.
 	///
 	/// Default: 1.0
 	Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity NOTIFY opacityChanged);
+	/// A hint to the compositor that only certain regions of the surface should be rendered.
+	/// This can be used to avoid rendering large empty regions of a window which can increase
+	/// performance, especially if the window is blurred. The mask should include all pixels
+	/// of the window that do not have an alpha value of 0.
+	Q_PROPERTY(PendingRegion* visibleMask READ visibleMask WRITE setVisibleMask NOTIFY visibleMaskChanged);
+	// clang-format on
 	QML_ELEMENT;
 	QML_UNCREATABLE("HyprlandWindow can only be used as an attached object.");
 	QML_ATTACHED(HyprlandWindow);
@@ -48,17 +56,25 @@ public:
 	[[nodiscard]] qreal opacity() const;
 	void setOpacity(qreal opacity);
 
+	[[nodiscard]] PendingRegion* visibleMask() const;
+	virtual void setVisibleMask(PendingRegion* mask);
+
 	static HyprlandWindow* qmlAttachedProperties(QObject* object);
 
 signals:
 	void opacityChanged();
+	void visibleMaskChanged();
 
 private slots:
 	void onWindowConnected();
 	void onWindowVisibleChanged();
+	void onWaylandWindowDestroyed();
 	void onWaylandSurfaceCreated();
 	void onWaylandSurfaceDestroyed();
 	void onProxyWindowDestroyed();
+	void onVisibleMaskDestroyed();
+	void onWindowPolished();
+	void updateVisibleMask();
 
 private:
 	void disconnectWaylandWindow();
@@ -67,7 +83,13 @@ private:
 	QWindow* mWindow = nullptr;
 	QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr;
 
+	struct {
+		bool opacity : 1 = false;
+		bool visibleMask : 1 = false;
+	} pendingPolish;
+
 	qreal mOpacity = 1.0;
+	PendingRegion* mVisibleMask = nullptr;
 	std::unique_ptr<impl::HyprlandSurface> surface;
 };
 
diff --git a/src/wayland/hyprland/surface/surface.cpp b/src/wayland/hyprland/surface/surface.cpp
index d1aa24fb..487da40b 100644
--- a/src/wayland/hyprland/surface/surface.cpp
+++ b/src/wayland/hyprland/surface/surface.cpp
@@ -1,19 +1,53 @@
 #include "surface.hpp"
+#include <cmath>
 
+#include <private/qwaylanddisplay_p.h>
+#include <private/qwaylandintegration_p.h>
+#include <qlogging.h>
+#include <qregion.h>
 #include <qtypes.h>
 #include <qwayland-hyprland-surface-v1.h>
+#include <wayland-client-protocol.h>
 #include <wayland-hyprland-surface-v1-client-protocol.h>
 #include <wayland-util.h>
 
 namespace qs::hyprland::surface::impl {
 
-HyprlandSurface::HyprlandSurface(::hyprland_surface_v1* surface)
-    : QtWayland::hyprland_surface_v1(surface) {}
+HyprlandSurface::HyprlandSurface(
+    ::hyprland_surface_v1* surface,
+    QtWaylandClient::QWaylandWindow* backer
+)
+    : QtWayland::hyprland_surface_v1(surface)
+    , backer(backer)
+    , backerSurface(backer->surface()) {}
 
 HyprlandSurface::~HyprlandSurface() { this->destroy(); }
 
+bool HyprlandSurface::surfaceEq(wl_surface* surface) const {
+	return surface == this->backerSurface;
+}
+
 void HyprlandSurface::setOpacity(qreal opacity) {
 	this->set_opacity(wl_fixed_from_double(opacity));
 }
 
+void HyprlandSurface::setVisibleRegion(const QRegion& region) {
+	if (this->version() < HYPRLAND_SURFACE_V1_SET_VISIBLE_REGION_SINCE_VERSION) {
+		qWarning() << "Cannot set hyprland surface visible region: compositor does not support "
+		              "hyprland_surface_v1.set_visible_region";
+		return;
+	}
+
+	if (region.isEmpty()) {
+		this->set_visible_region(nullptr);
+	} else {
+		static const auto* waylandIntegration = QtWaylandClient::QWaylandIntegration::instance();
+		auto* display = waylandIntegration->display();
+
+		auto* wlRegion = display->createRegion(region);
+		this->set_visible_region(wlRegion);
+		wl_region_destroy(wlRegion); // NOLINT(misc-include-cleaner)
+	}
+}
+
 } // namespace qs::hyprland::surface::impl
diff --git a/src/wayland/hyprland/surface/surface.hpp b/src/wayland/hyprland/surface/surface.hpp
index a27e50e3..1c8b5486 100644
--- a/src/wayland/hyprland/surface/surface.hpp
+++ b/src/wayland/hyprland/surface/surface.hpp
@@ -1,21 +1,31 @@
 #pragma once
 
+#include <private/qwaylandwindow_p.h>
 #include <qobject.h>
+#include <qregion.h>
 #include <qtclasshelpermacros.h>
 #include <qtmetamacros.h>
 #include <qtypes.h>
 #include <qwayland-hyprland-surface-v1.h>
+#include <wayland-client-protocol.h>
 #include <wayland-hyprland-surface-v1-client-protocol.h>
 
 namespace qs::hyprland::surface::impl {
 
 class HyprlandSurface: public QtWayland::hyprland_surface_v1 {
 public:
-	explicit HyprlandSurface(::hyprland_surface_v1* surface);
+	explicit HyprlandSurface(::hyprland_surface_v1* surface, QtWaylandClient::QWaylandWindow* backer);
 	~HyprlandSurface() override;
 	Q_DISABLE_COPY_MOVE(HyprlandSurface);
 
+	[[nodiscard]] bool surfaceEq(wl_surface* surface) const;
+
 	void setOpacity(qreal opacity);
+	void setVisibleRegion(const QRegion& region);
+
+private:
+	QtWaylandClient::QWaylandWindow* backer;
+	wl_surface* backerSurface = nullptr;
 };
 
 } // namespace qs::hyprland::surface::impl