diff --git a/CMakeLists.txt b/CMakeLists.txt
index b530b86b7..d8aed5f2f 100755
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -269,6 +269,7 @@ protocolNew("unstable/idle-inhibit/idle-inhibit-unstable-v1.xml" "idle-inhibit-u
 protocolNew("unstable/relative-pointer/relative-pointer-unstable-v1.xml" "relative-pointer-unstable-v1" false)
 protocolNew("unstable/xdg-decoration/xdg-decoration-unstable-v1.xml" "xdg-decoration-unstable-v1" false)
 protocolNew("staging/alpha-modifier/alpha-modifier-v1.xml" "alpha-modifier-v1" false)
+protocolNew("protocols/wlr-gamma-control-unstable-v1.xml" "wlr-gamma-control-unstable-v1" true)
 
 # tools
 add_subdirectory(hyprctl)
diff --git a/protocols/meson.build b/protocols/meson.build
index 8247d26ac..eaeaada03 100644
--- a/protocols/meson.build
+++ b/protocols/meson.build
@@ -39,6 +39,7 @@ protocols = [
 ]
 
 new_protocols = [
+  ['wlr-gamma-control-unstable-v1.xml'],
   [wl_protocol_dir, 'staging/tearing-control/tearing-control-v1.xml'],
   [wl_protocol_dir, 'staging/fractional-scale/fractional-scale-v1.xml'],
   [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'],
diff --git a/protocols/wlr-gamma-control-unstable-v1.xml b/protocols/wlr-gamma-control-unstable-v1.xml
new file mode 100644
index 000000000..16e0be8b1
--- /dev/null
+++ b/protocols/wlr-gamma-control-unstable-v1.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="wlr_gamma_control_unstable_v1">
+  <copyright>
+    Copyright © 2015 Giulio camuffo
+    Copyright © 2018 Simon Ser
+
+    Permission to use, copy, modify, distribute, and sell this
+    software and its documentation for any purpose is hereby granted
+    without fee, provided that the above copyright notice appear in
+    all copies and that both that copyright notice and this permission
+    notice appear in supporting documentation, and that the name of
+    the copyright holders not be used in advertising or publicity
+    pertaining to distribution of the software without specific,
+    written prior permission.  The copyright holders make no
+    representations about the suitability of this software for any
+    purpose.  It is provided "as is" without express or implied
+    warranty.
+
+    THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+    SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+    FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+    SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+    AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+    ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+    THIS SOFTWARE.
+  </copyright>
+
+  <description summary="manage gamma tables of outputs">
+    This protocol allows a privileged client to set the gamma tables for
+    outputs.
+
+    Warning! The protocol described in this file is experimental and
+    backward incompatible changes may be made. Backward compatible changes
+    may be added together with the corresponding interface version bump.
+    Backward incompatible changes are done by bumping the version number in
+    the protocol and interface names and resetting the interface version.
+    Once the protocol is to be declared stable, the 'z' prefix and the
+    version number in the protocol and interface names are removed and the
+    interface version number is reset.
+  </description>
+
+  <interface name="zwlr_gamma_control_manager_v1" version="1">
+    <description summary="manager to create per-output gamma controls">
+      This interface is a manager that allows creating per-output gamma
+      controls.
+    </description>
+
+    <request name="get_gamma_control">
+      <description summary="get a gamma control for an output">
+        Create a gamma control that can be used to adjust gamma tables for the
+        provided output.
+      </description>
+      <arg name="id" type="new_id" interface="zwlr_gamma_control_v1"/>
+      <arg name="output" type="object" interface="wl_output"/>
+    </request>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the manager">
+        All objects created by the manager will still remain valid, until their
+        appropriate destroy request has been called.
+      </description>
+    </request>
+  </interface>
+
+  <interface name="zwlr_gamma_control_v1" version="1">
+    <description summary="adjust gamma tables for an output">
+      This interface allows a client to adjust gamma tables for a particular
+      output.
+
+      The client will receive the gamma size, and will then be able to set gamma
+      tables. At any time the compositor can send a failed event indicating that
+      this object is no longer valid.
+
+      There can only be at most one gamma control object per output, which
+      has exclusive access to this particular output. When the gamma control
+      object is destroyed, the gamma table is restored to its original value.
+    </description>
+
+    <event name="gamma_size">
+      <description summary="size of gamma ramps">
+        Advertise the size of each gamma ramp.
+
+        This event is sent immediately when the gamma control object is created.
+      </description>
+      <arg name="size" type="uint" summary="number of elements in a ramp"/>
+    </event>
+
+    <enum name="error">
+      <entry name="invalid_gamma" value="1" summary="invalid gamma tables"/>
+    </enum>
+
+    <request name="set_gamma">
+      <description summary="set the gamma table">
+        Set the gamma table. The file descriptor can be memory-mapped to provide
+        the raw gamma table, which contains successive gamma ramps for the red,
+        green and blue channels. Each gamma ramp is an array of 16-byte unsigned
+        integers which has the same length as the gamma size.
+
+        The file descriptor data must have the same length as three times the
+        gamma size.
+      </description>
+      <arg name="fd" type="fd" summary="gamma table file descriptor"/>
+    </request>
+
+    <event name="failed">
+      <description summary="object no longer valid">
+        This event indicates that the gamma control is no longer valid. This
+        can happen for a number of reasons, including:
+        - The output doesn't support gamma tables
+        - Setting the gamma tables failed
+        - Another client already has exclusive gamma control for this output
+        - The compositor has transferred gamma control to another client
+
+        Upon receiving this event, the client should destroy this object.
+      </description>
+    </event>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy this control">
+        Destroys the gamma control object. If the object is still valid, this
+        restores the original gamma tables.
+      </description>
+    </request>
+  </interface>
+</protocol>
diff --git a/src/Compositor.cpp b/src/Compositor.cpp
index 2997bf6ed..aafb4fcb0 100644
--- a/src/Compositor.cpp
+++ b/src/Compositor.cpp
@@ -188,8 +188,6 @@ void CCompositor::initServer() {
     wlr_primary_selection_v1_device_manager_create(m_sWLDisplay);
     wlr_viewporter_create(m_sWLDisplay);
 
-    m_sWLRGammaCtrlMgr = wlr_gamma_control_manager_v1_create(m_sWLDisplay);
-
     m_sWLROutputLayout = wlr_output_layout_create(m_sWLDisplay);
 
     m_sWLROutputPowerMgr = wlr_output_power_manager_v1_create(m_sWLDisplay);
@@ -299,7 +297,6 @@ void CCompositor::initAllSignals() {
     addWLSignal(&m_sWLRTextInputMgr->events.text_input, &Events::listen_newTextInput, m_sWLRTextInputMgr, "TextInputMgr");
     addWLSignal(&m_sWLRActivation->events.request_activate, &Events::listen_activateXDG, m_sWLRActivation, "ActivationV1");
     addWLSignal(&m_sWLRSessionLockMgr->events.new_lock, &Events::listen_newSessionLock, m_sWLRSessionLockMgr, "SessionLockMgr");
-    addWLSignal(&m_sWLRGammaCtrlMgr->events.set_gamma, &Events::listen_setGamma, m_sWLRGammaCtrlMgr, "GammaCtrlMgr");
     addWLSignal(&m_sWLRKbShInhibitMgr->events.new_inhibitor, &Events::listen_newShortcutInhibitor, m_sWLRKbShInhibitMgr, "ShortcutInhibitMgr");
 
     if (m_sWRLDRMLeaseMgr)
@@ -349,7 +346,6 @@ void CCompositor::removeAllSignals() {
     removeWLSignal(&Events::listen_newTextInput);
     removeWLSignal(&Events::listen_activateXDG);
     removeWLSignal(&Events::listen_newSessionLock);
-    removeWLSignal(&Events::listen_setGamma);
     removeWLSignal(&Events::listen_newShortcutInhibitor);
 
     if (m_sWRLDRMLeaseMgr)
diff --git a/src/Compositor.hpp b/src/Compositor.hpp
index af777d037..c6a38ebfe 100644
--- a/src/Compositor.hpp
+++ b/src/Compositor.hpp
@@ -77,7 +77,6 @@ class CCompositor {
     wlr_linux_dmabuf_v1*                       m_sWLRLinuxDMABuf;
     wlr_backend*                               m_sWLRHeadlessBackend;
     wlr_session_lock_manager_v1*               m_sWLRSessionLockMgr;
-    wlr_gamma_control_manager_v1*              m_sWLRGammaCtrlMgr;
     // ------------------------------------------------- //
 
     std::string                               m_szWLDisplaySocket   = "";
diff --git a/src/events/Events.hpp b/src/events/Events.hpp
index 74b81b00b..3a03f3d00 100644
--- a/src/events/Events.hpp
+++ b/src/events/Events.hpp
@@ -133,9 +133,6 @@ namespace Events {
     // Session Lock
     LISTENER(newSessionLock);
 
-    // Gamma control
-    LISTENER(setGamma);
-
     // Shortcut inhibitor
     LISTENER(newShortcutInhibitor);
 };
diff --git a/src/events/Misc.cpp b/src/events/Misc.cpp
index 3952920a8..6381937cf 100644
--- a/src/events/Misc.cpp
+++ b/src/events/Misc.cpp
@@ -222,23 +222,6 @@ void Events::listener_newSessionLock(wl_listener* listener, void* data) {
     g_pSessionLockManager->onNewSessionLock((wlr_session_lock_v1*)data);
 }
 
-void Events::listener_setGamma(wl_listener* listener, void* data) {
-    Debug::log(LOG, "New Gamma event at {:x}", (uintptr_t)data);
-
-    const auto E = (wlr_gamma_control_manager_v1_set_gamma_event*)data;
-
-    const auto PMONITOR = g_pCompositor->getMonitorFromOutput(E->output);
-
-    if (!PMONITOR) {
-        Debug::log(ERR, "Gamma event object references non-existent output {:x} ?", (uintptr_t)E->output);
-        return;
-    }
-
-    PMONITOR->gammaChanged = true;
-
-    g_pCompositor->scheduleFrameForMonitor(PMONITOR);
-}
-
 void Events::listener_newShortcutInhibitor(wl_listener* listener, void* data) {
     const auto INHIBITOR = (wlr_keyboard_shortcuts_inhibitor_v1*)data;
 
diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp
index ceb021c87..f7d2274c7 100644
--- a/src/helpers/Monitor.cpp
+++ b/src/helpers/Monitor.cpp
@@ -1,10 +1,8 @@
 #include "Monitor.hpp"
-
 #include "MiscFunctions.hpp"
-
 #include "../Compositor.hpp"
-
 #include "../config/ConfigValue.hpp"
+#include "../protocols/GammaControl.hpp"
 
 int ratHandler(void* data) {
     g_pHyprRenderer->renderMonitor((CMonitor*)data);
@@ -26,6 +24,8 @@ CMonitor::~CMonitor() {
     hyprListener_monitorNeedsFrame.removeCallback();
     hyprListener_monitorCommit.removeCallback();
     hyprListener_monitorBind.removeCallback();
+
+    events.destroy.emit();
 }
 
 void CMonitor::onConnect(bool noRule) {
@@ -214,6 +214,10 @@ void CMonitor::onConnect(bool noRule) {
     renderTimer = wl_event_loop_add_timer(g_pCompositor->m_sWLEventLoop, ratHandler, this);
 
     g_pCompositor->scheduleFrameForMonitor(this);
+
+    PROTO::gamma->applyGammaToState(this);
+
+    events.connect.emit();
 }
 
 void CMonitor::onDisconnect(bool destroy) {
@@ -228,6 +232,8 @@ void CMonitor::onDisconnect(bool destroy) {
 
     Debug::log(LOG, "onDisconnect called for {}", output->name);
 
+    events.disconnect.emit();
+
     // Cleanup everything. Move windows back, snap cursor, shit.
     CMonitor* BACKUPMON = nullptr;
     for (auto& m : g_pCompositor->m_vMonitors) {
diff --git a/src/helpers/Monitor.hpp b/src/helpers/Monitor.hpp
index 909abb444..4dd58a4ea 100644
--- a/src/helpers/Monitor.hpp
+++ b/src/helpers/Monitor.hpp
@@ -11,6 +11,8 @@
 #include "Region.hpp"
 #include <optional>
 
+#include "signal/Signal.hpp"
+
 struct SMonitorRule {
     std::string         name        = "";
     Vector2D            resolution  = Vector2D(1280, 720);
@@ -83,7 +85,6 @@ class CMonitor {
     bool                    noFrameSchedule = false;
     bool                    scheduledRecalc = false;
     wl_output_transform     transform       = WL_OUTPUT_TRANSFORM_NORMAL;
-    bool                    gammaChanged    = false;
     float                   xwaylandScale   = 1.f;
     std::array<float, 9>    projMatrix      = {0};
     std::optional<Vector2D> forceSize;
@@ -120,6 +121,12 @@ class CMonitor {
         bool frameScheduledWhileBusy = false;
     } tearingState;
 
+    struct {
+        CSignal destroy;
+        CSignal connect;
+        CSignal disconnect;
+    } events;
+
     std::array<std::vector<std::unique_ptr<SLayerSurface>>, 4> m_aLayerSurfaceLayers;
 
     DYNLISTENER(monitorFrame);
diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp
index 368c7bb26..96a0707bb 100644
--- a/src/managers/ProtocolManager.cpp
+++ b/src/managers/ProtocolManager.cpp
@@ -8,6 +8,7 @@
 #include "../protocols/RelativePointer.hpp"
 #include "../protocols/XDGDecoration.hpp"
 #include "../protocols/AlphaModifier.hpp"
+#include "../protocols/GammaControl.hpp"
 
 #include "tearing-control-v1.hpp"
 #include "fractional-scale-v1.hpp"
@@ -17,6 +18,7 @@
 #include "relative-pointer-unstable-v1.hpp"
 #include "xdg-decoration-unstable-v1.hpp"
 #include "alpha-modifier-v1.hpp"
+#include "wlr-gamma-control-unstable-v1.hpp"
 
 CProtocolManager::CProtocolManager() {
 
@@ -28,6 +30,7 @@ CProtocolManager::CProtocolManager() {
     PROTO::relativePointer = std::make_unique<CRelativePointerProtocol>(&zwp_relative_pointer_manager_v1_interface, 1, "RelativePointer");
     PROTO::xdgDecoration   = std::make_unique<CXDGDecorationProtocol>(&zxdg_decoration_manager_v1_interface, 1, "XDGDecoration");
     PROTO::alphaModifier   = std::make_unique<CAlphaModifierProtocol>(&wp_alpha_modifier_v1_interface, 1, "AlphaModifier");
+    PROTO::gamma           = std::make_unique<CGammaControlProtocol>(&zwlr_gamma_control_manager_v1_interface, 1, "GammaControl");
 
     // Old protocol implementations.
     // TODO: rewrite them to use hyprwayland-scanner.
diff --git a/src/protocols/GammaControl.cpp b/src/protocols/GammaControl.cpp
new file mode 100644
index 000000000..fa7fddd75
--- /dev/null
+++ b/src/protocols/GammaControl.cpp
@@ -0,0 +1,177 @@
+#include "GammaControl.hpp"
+#include <fcntl.h>
+#include <unistd.h>
+#include "../helpers/Monitor.hpp"
+#include "../Compositor.hpp"
+
+CGammaControl::CGammaControl(SP<CZwlrGammaControlV1> resource_, wl_resource* output) : resource(resource_) {
+    if (!resource_->resource())
+        return;
+
+    wlr_output* wlrOutput = wlr_output_from_resource(output);
+
+    if (!wlrOutput) {
+        Debug::log(ERR, "[gamma] No wlr_output");
+        resource->sendFailed();
+        return;
+    }
+
+    pMonitor = g_pCompositor->getRealMonitorFromOutput(wlrOutput);
+
+    if (!pMonitor) {
+        Debug::log(ERR, "[gamma] No CMonitor");
+        resource->sendFailed();
+        return;
+    }
+
+    for (auto& g : PROTO::gamma->m_vGammaControllers) {
+        if (g->pMonitor == pMonitor) {
+            resource->sendFailed();
+            return;
+        }
+    }
+
+    gammaSize = wlr_output_get_gamma_size(wlrOutput);
+
+    if (gammaSize <= 0) {
+        Debug::log(ERR, "[gamma] Output {} doesn't support gamma", pMonitor->szName);
+        resource->sendFailed();
+        return;
+    }
+
+    gammaTable.resize(gammaSize * 3);
+
+    resource->setDestroy([this](CZwlrGammaControlV1* gamma) { PROTO::gamma->destroyGammaControl(this); });
+    resource->setOnDestroy([this](CZwlrGammaControlV1* gamma) { PROTO::gamma->destroyGammaControl(this); });
+
+    resource->setSetGamma([this](CZwlrGammaControlV1* gamma, int32_t fd) {
+        Debug::log(LOG, "[gamma] setGamma for {}", pMonitor->szName);
+
+        int fdFlags = fcntl(fd, F_GETFL, 0);
+        if (fdFlags < 0) {
+            Debug::log(ERR, "[gamma] Failed to get fd flags");
+            resource->sendFailed();
+            close(fd);
+            return;
+        }
+
+        if (fcntl(fd, F_SETFL, fdFlags | O_NONBLOCK) < 0) {
+            Debug::log(ERR, "[gamma] Failed to set fd flags");
+            resource->sendFailed();
+            close(fd);
+            return;
+        }
+
+        ssize_t readBytes = pread(fd, gammaTable.data(), gammaTable.size() * sizeof(uint16_t), 0);
+        if (readBytes < 0 || (size_t)readBytes != gammaTable.size() * sizeof(uint16_t)) {
+            Debug::log(ERR, "[gamma] Failed to read bytes");
+            close(fd);
+
+            if ((size_t)readBytes != gammaTable.size() * sizeof(uint16_t)) {
+                wl_resource_post_error(gamma->resource(), ZWLR_GAMMA_CONTROL_V1_ERROR_INVALID_GAMMA, "Gamma ramps size mismatch");
+                return;
+            }
+
+            resource->sendFailed();
+            return;
+        }
+
+        gammaTableSet = true;
+        close(fd);
+        applyToMonitor();
+    });
+
+    resource->sendGammaSize(gammaSize);
+
+    listeners.monitorDestroy    = pMonitor->events.destroy.registerListener([this](std::any) { this->onMonitorDestroy(); });
+    listeners.monitorDisconnect = pMonitor->events.destroy.registerListener([this](std::any) { this->onMonitorDestroy(); });
+}
+
+CGammaControl::~CGammaControl() {
+    if (!gammaTableSet || !pMonitor)
+        return;
+
+    // reset the LUT if the client dies for whatever reason and doesn't unset the gamma
+    wlr_output_state_set_gamma_lut(pMonitor->state.wlr(), 0, nullptr, nullptr, nullptr);
+}
+
+bool CGammaControl::good() {
+    return resource->resource();
+}
+
+void CGammaControl::applyToMonitor() {
+    if (!pMonitor)
+        return; // ??
+
+    Debug::log(LOG, "[gamma] setting to monitor {}", pMonitor->szName);
+
+    if (!gammaTableSet) {
+        wlr_output_state_set_gamma_lut(pMonitor->state.wlr(), 0, nullptr, nullptr, nullptr);
+        return;
+    }
+
+    uint16_t* red   = &gammaTable.at(0);
+    uint16_t* green = &gammaTable.at(gammaSize);
+    uint16_t* blue  = &gammaTable.at(gammaSize * 2);
+
+    wlr_output_state_set_gamma_lut(pMonitor->state.wlr(), gammaSize, red, green, blue);
+
+    if (!pMonitor->state.test()) {
+        Debug::log(LOG, "[gamma] setting to monitor {} failed", pMonitor->szName);
+        wlr_output_state_set_gamma_lut(pMonitor->state.wlr(), 0, nullptr, nullptr, nullptr);
+    }
+
+    g_pHyprRenderer->damageMonitor(pMonitor);
+}
+
+CMonitor* CGammaControl::getMonitor() {
+    return pMonitor;
+}
+
+void CGammaControl::onMonitorDestroy() {
+    resource->sendFailed();
+    pMonitor = nullptr;
+}
+
+CGammaControlProtocol::CGammaControlProtocol(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) {
+    ;
+}
+
+void CGammaControlProtocol::bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) {
+    const auto RESOURCE = m_vManagers.emplace_back(std::make_unique<CZwlrGammaControlManagerV1>(client, ver, id)).get();
+    RESOURCE->setOnDestroy([this](CZwlrGammaControlManagerV1* p) { this->onManagerResourceDestroy(p->resource()); });
+
+    RESOURCE->setDestroy([this](CZwlrGammaControlManagerV1* pMgr) { this->onManagerResourceDestroy(pMgr->resource()); });
+    RESOURCE->setGetGammaControl([this](CZwlrGammaControlManagerV1* pMgr, uint32_t id, wl_resource* output) { this->onGetGammaControl(pMgr, id, output); });
+}
+
+void CGammaControlProtocol::onManagerResourceDestroy(wl_resource* res) {
+    std::erase_if(m_vManagers, [&](const auto& other) { return other->resource() == res; });
+}
+
+void CGammaControlProtocol::destroyGammaControl(CGammaControl* gamma) {
+    std::erase_if(m_vGammaControllers, [&](const auto& other) { return other.get() == gamma; });
+}
+
+void CGammaControlProtocol::onGetGammaControl(CZwlrGammaControlManagerV1* pMgr, uint32_t id, wl_resource* output) {
+    const auto CLIENT = wl_resource_get_client(pMgr->resource());
+    const auto RESOURCE =
+        m_vGammaControllers.emplace_back(std::make_unique<CGammaControl>(std::make_shared<CZwlrGammaControlV1>(CLIENT, wl_resource_get_version(pMgr->resource()), id), output))
+            .get();
+
+    if (!RESOURCE->good()) {
+        wl_resource_post_no_memory(pMgr->resource());
+        m_vGammaControllers.pop_back();
+        return;
+    }
+}
+
+void CGammaControlProtocol::applyGammaToState(CMonitor* pMonitor) {
+    for (auto& g : m_vGammaControllers) {
+        if (g->getMonitor() != pMonitor)
+            continue;
+
+        g->applyToMonitor();
+        break;
+    }
+}
\ No newline at end of file
diff --git a/src/protocols/GammaControl.hpp b/src/protocols/GammaControl.hpp
new file mode 100644
index 000000000..074a51f02
--- /dev/null
+++ b/src/protocols/GammaControl.hpp
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <memory>
+#include <vector>
+#include <cstdint>
+#include "WaylandProtocol.hpp"
+#include "wlr-gamma-control-unstable-v1.hpp"
+#include "../helpers/signal/Listener.hpp"
+
+class CMonitor;
+
+class CGammaControl {
+  public:
+    CGammaControl(SP<CZwlrGammaControlV1> resource_, wl_resource* output);
+    ~CGammaControl();
+
+    bool      good();
+    void      applyToMonitor();
+    CMonitor* getMonitor();
+
+  private:
+    SP<CZwlrGammaControlV1> resource;
+    CMonitor*               pMonitor      = nullptr;
+    size_t                  gammaSize     = 0;
+    bool                    gammaTableSet = false;
+    std::vector<uint16_t>   gammaTable;
+
+    void                    onMonitorDestroy();
+
+    struct {
+        CHyprSignalListener monitorDisconnect;
+        CHyprSignalListener monitorDestroy;
+    } listeners;
+};
+
+class CGammaControlProtocol : public IWaylandProtocol {
+  public:
+    CGammaControlProtocol(const wl_interface* iface, const int& ver, const std::string& name);
+
+    virtual void bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id);
+
+    void         applyGammaToState(CMonitor* pMonitor);
+
+  private:
+    void onManagerResourceDestroy(wl_resource* res);
+    void destroyGammaControl(CGammaControl* gamma);
+    void onGetGammaControl(CZwlrGammaControlManagerV1* pMgr, uint32_t id, wl_resource* output);
+
+    //
+    std::vector<UP<CZwlrGammaControlManagerV1>> m_vManagers;
+    std::vector<UP<CGammaControl>>              m_vGammaControllers;
+
+    friend class CGammaControl;
+};
+
+namespace PROTO {
+    inline UP<CGammaControlProtocol> gamma;
+};
diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp
index f183bf447..9a8819e13 100644
--- a/src/render/Renderer.cpp
+++ b/src/render/Renderer.cpp
@@ -1151,25 +1151,6 @@ void CHyprRenderer::renderMonitor(CMonitor* pMonitor) {
         g_pLayoutManager->getCurrentLayout()->recalculateMonitor(pMonitor->ID);
     }
 
-    // gamma stuff
-    if (pMonitor->gammaChanged) {
-        pMonitor->gammaChanged = false;
-
-        const auto PGAMMACTRL = wlr_gamma_control_manager_v1_get_control(g_pCompositor->m_sWLRGammaCtrlMgr, pMonitor->output);
-
-        if (!wlr_gamma_control_v1_apply(PGAMMACTRL, pMonitor->state.wlr())) {
-            Debug::log(ERR, "Could not apply gamma control to {}", pMonitor->szName);
-            return;
-        }
-
-        if (!wlr_output_test_state(pMonitor->output, pMonitor->state.wlr())) {
-            Debug::log(ERR, "Output test failed for setting gamma to {}", pMonitor->szName);
-            // aka rollback
-            wlr_gamma_control_v1_apply(nullptr, pMonitor->state.wlr());
-            wlr_gamma_control_v1_send_failed_and_destroy(PGAMMACTRL);
-        }
-    }
-
     // tearing and DS first
     bool shouldTear = false;
     if (pMonitor->tearingState.nextRenderTorn) {