From f4c8789aec7150c6573ad69927eb53ec9f710dcf Mon Sep 17 00:00:00 2001
From: Vaxry <vaxry@vaxry.net>
Date: Sat, 6 Jul 2024 17:38:27 +0200
Subject: [PATCH] explicit sync

---
 CMakeLists.txt                              |   1 +
 src/helpers/Monitor.cpp                     |   6 +
 src/helpers/Monitor.hpp                     |   9 +-
 src/helpers/sync/SyncTimeline.cpp           | 187 ++++++++++++++++++++
 src/helpers/sync/SyncTimeline.hpp           |  46 +++++
 src/managers/ProtocolManager.cpp            |   2 +
 src/managers/eventLoop/EventLoopManager.hpp |   2 +
 src/protocols/DRMSyncobj.cpp                | 182 +++++++++++++++++++
 src/protocols/DRMSyncobj.hpp                |  82 +++++++++
 src/protocols/Viewporter.cpp                |  29 +--
 src/protocols/Viewporter.hpp                |   5 +-
 src/protocols/core/Compositor.cpp           | 133 +++++++++-----
 src/protocols/core/Compositor.hpp           |  12 +-
 src/render/OpenGL.cpp                       |  64 +++++++
 src/render/OpenGL.hpp                       |  20 +++
 src/render/Renderer.cpp                     |  61 ++++---
 src/render/Renderer.hpp                     |   2 +
 17 files changed, 757 insertions(+), 86 deletions(-)
 create mode 100644 src/helpers/sync/SyncTimeline.cpp
 create mode 100644 src/helpers/sync/SyncTimeline.hpp
 create mode 100644 src/protocols/DRMSyncobj.cpp
 create mode 100644 src/protocols/DRMSyncobj.hpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 54271fc3d..1db303f38 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -296,6 +296,7 @@ protocolNew("staging/xwayland-shell" "xwayland-shell-v1" false)
 protocolNew("stable/viewporter" "viewporter" false)
 protocolNew("stable/linux-dmabuf" "linux-dmabuf-v1" false)
 protocolNew("staging/drm-lease" "drm-lease-v1" false)
+protocolNew("staging/linux-drm-syncobj" "linux-drm-syncobj-v1" false)
 
 protocolWayland()
 
diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp
index accc77834..495e772e2 100644
--- a/src/helpers/Monitor.cpp
+++ b/src/helpers/Monitor.cpp
@@ -10,6 +10,7 @@
 #include "../protocols/DRMLease.hpp"
 #include "../protocols/core/Output.hpp"
 #include "../managers/PointerManager.hpp"
+#include "sync/SyncTimeline.hpp"
 #include <hyprutils/string/String.hpp>
 using namespace Hyprutils::String;
 
@@ -29,6 +30,11 @@ CMonitor::~CMonitor() {
 
 void CMonitor::onConnect(bool noRule) {
 
+    if (output->supportsExplicit) {
+        inTimeline  = CSyncTimeline::create(g_pCompositor->m_iDRMFD);
+        outTimeline = CSyncTimeline::create(g_pCompositor->m_iDRMFD);
+    }
+
     listeners.frame      = output->events.frame.registerListener([this](std::any d) { Events::listener_monitorFrame(this, nullptr); });
     listeners.destroy    = output->events.destroy.registerListener([this](std::any d) { Events::listener_monitorDestroy(this, nullptr); });
     listeners.commit     = output->events.commit.registerListener([this](std::any d) { Events::listener_monitorCommit(this, nullptr); });
diff --git a/src/helpers/Monitor.hpp b/src/helpers/Monitor.hpp
index 640eabe7e..7488d7dcb 100644
--- a/src/helpers/Monitor.hpp
+++ b/src/helpers/Monitor.hpp
@@ -40,6 +40,7 @@ struct SMonitorRule {
 };
 
 class CMonitor;
+class CSyncTimeline;
 
 class CMonitorState {
   public:
@@ -116,7 +117,13 @@ class CMonitor {
 
     SMonitorRule                activeMonitorRule;
 
-    WP<CMonitor>                self;
+    // explicit sync
+    SP<CSyncTimeline> inTimeline;
+    SP<CSyncTimeline> outTimeline;
+    uint64_t          lastWaitPoint = 0;
+    uint64_t          commitSeq     = 0;
+
+    WP<CMonitor>      self;
 
     // mirroring
     CMonitor*              pMirrorOf = nullptr;
diff --git a/src/helpers/sync/SyncTimeline.cpp b/src/helpers/sync/SyncTimeline.cpp
new file mode 100644
index 000000000..1a7286e00
--- /dev/null
+++ b/src/helpers/sync/SyncTimeline.cpp
@@ -0,0 +1,187 @@
+#include "SyncTimeline.hpp"
+#include "../../defines.hpp"
+#include "../../managers/eventLoop/EventLoopManager.hpp"
+
+#include <xf86drm.h>
+#include <sys/eventfd.h>
+
+SP<CSyncTimeline> CSyncTimeline::create(int drmFD_) {
+    auto timeline   = SP<CSyncTimeline>(new CSyncTimeline);
+    timeline->drmFD = drmFD_;
+    timeline->self  = timeline;
+
+    if (drmSyncobjCreate(drmFD_, 0, &timeline->handle)) {
+        Debug::log(ERR, "CSyncTimeline: failed to create a drm syncobj??");
+        return nullptr;
+    }
+
+    return timeline;
+}
+
+SP<CSyncTimeline> CSyncTimeline::create(int drmFD_, int drmSyncobjFD) {
+    auto timeline   = SP<CSyncTimeline>(new CSyncTimeline);
+    timeline->drmFD = drmFD_;
+    timeline->self  = timeline;
+
+    if (drmSyncobjFDToHandle(drmFD_, drmSyncobjFD, &timeline->handle)) {
+        Debug::log(ERR, "CSyncTimeline: failed to create a drm syncobj from fd??");
+        return nullptr;
+    }
+
+    return timeline;
+}
+
+CSyncTimeline::~CSyncTimeline() {
+    if (handle == 0)
+        return;
+
+    drmSyncobjDestroy(drmFD, handle);
+}
+
+std::optional<bool> CSyncTimeline::check(uint64_t point, uint32_t flags) {
+#ifdef __FreeBSD__
+    constexpr int ETIME_ERR = ETIMEDOUT;
+#else
+    constexpr int ETIME_ERR = ETIME;
+#endif
+
+    uint32_t signaled = 0;
+    int      ret      = drmSyncobjTimelineWait(drmFD, &handle, &point, 1, 0, flags, &signaled);
+    if (ret != 0 && ret != -ETIME_ERR) {
+        Debug::log(ERR, "CSyncTimeline::check: drmSyncobjTimelineWait failed");
+        return std::nullopt;
+    }
+
+    return ret == 0;
+}
+
+static int handleWaiterFD(int fd, uint32_t mask, void* data) {
+    auto waiter = (CSyncTimeline::SWaiter*)data;
+
+    if (mask & (WL_EVENT_HANGUP | WL_EVENT_ERROR)) {
+        Debug::log(ERR, "handleWaiterFD: eventfd error");
+        return 0;
+    }
+
+    if (mask & WL_EVENT_READABLE) {
+        uint64_t value = 0;
+        if (read(fd, &value, sizeof(value)) <= 0)
+            Debug::log(ERR, "handleWaiterFD: failed to read from eventfd");
+    }
+
+    wl_event_source_remove(waiter->source);
+    waiter->source = nullptr;
+
+    if (waiter->fn)
+        waiter->fn();
+
+    if (waiter->timeline)
+        waiter->timeline->removeWaiter(waiter);
+
+    return 0;
+}
+
+bool CSyncTimeline::addWaiter(const std::function<void()>& waiter, uint64_t point, uint32_t flags) {
+    auto w      = makeShared<SWaiter>();
+    w->fn       = waiter;
+    w->timeline = self;
+
+    int eventFD = eventfd(0, EFD_CLOEXEC);
+
+    if (eventFD < 0) {
+        Debug::log(ERR, "CSyncTimeline::addWaiter: failed to acquire an eventfd");
+        return false;
+    }
+
+    drm_syncobj_eventfd syncobjEventFD = {
+        .handle = handle,
+        .flags  = flags,
+        .point  = point,
+        .fd     = eventFD,
+    };
+
+    if (drmIoctl(drmFD, DRM_IOCTL_SYNCOBJ_EVENTFD, &syncobjEventFD) != 0) {
+        Debug::log(ERR, "CSyncTimeline::addWaiter: drmIoctl failed");
+        close(eventFD);
+        return false;
+    }
+
+    w->source = wl_event_loop_add_fd(g_pEventLoopManager->m_sWayland.loop, eventFD, WL_EVENT_READABLE, ::handleWaiterFD, w.get());
+    if (!w->source) {
+        Debug::log(ERR, "CSyncTimeline::addWaiter: wl_event_loop_add_fd failed");
+        close(eventFD);
+        return false;
+    }
+
+    waiters.emplace_back(w);
+
+    return true;
+}
+
+void CSyncTimeline::removeWaiter(SWaiter* w) {
+    if (w->source) {
+        wl_event_source_remove(w->source);
+        w->source = nullptr;
+    }
+    std::erase_if(waiters, [w](const auto& e) { return e.get() == w; });
+}
+
+int CSyncTimeline::exportAsSyncFileFD(uint64_t src) {
+    int      sync = -1;
+
+    uint32_t syncHandle = 0;
+    if (drmSyncobjCreate(drmFD, 0, &syncHandle)) {
+        Debug::log(ERR, "exportAsSyncFileFD: drmSyncobjCreate failed");
+        return -1;
+    }
+
+    if (drmSyncobjTransfer(drmFD, syncHandle, 0, handle, src, 0)) {
+        Debug::log(ERR, "exportAsSyncFileFD: drmSyncobjTransfer failed");
+        drmSyncobjDestroy(drmFD, syncHandle);
+        return -1;
+    }
+
+    if (drmSyncobjExportSyncFile(drmFD, syncHandle, &sync)) {
+        Debug::log(ERR, "exportAsSyncFileFD: drmSyncobjExportSyncFile failed");
+        drmSyncobjDestroy(drmFD, syncHandle);
+        return -1;
+    }
+
+    drmSyncobjDestroy(drmFD, syncHandle);
+    return sync;
+}
+
+bool CSyncTimeline::importFromSyncFileFD(uint64_t dst, int fd) {
+    uint32_t syncHandle = 0;
+
+    if (drmSyncobjCreate(drmFD, 0, &syncHandle)) {
+        Debug::log(ERR, "importFromSyncFileFD: drmSyncobjCreate failed");
+        return false;
+    }
+
+    if (drmSyncobjImportSyncFile(drmFD, syncHandle, fd)) {
+        Debug::log(ERR, "importFromSyncFileFD: drmSyncobjImportSyncFile failed");
+        drmSyncobjDestroy(drmFD, syncHandle);
+        return -1;
+    }
+
+    if (drmSyncobjTransfer(drmFD, handle, dst, syncHandle, 0, 0)) {
+        Debug::log(ERR, "importFromSyncFileFD: drmSyncobjTransfer failed");
+        drmSyncobjDestroy(drmFD, syncHandle);
+        return -1;
+    }
+
+    drmSyncobjDestroy(drmFD, syncHandle);
+    return true;
+}
+
+bool CSyncTimeline::transfer(SP<CSyncTimeline> from, uint64_t fromPoint, uint64_t toPoint) {
+    ASSERT(from->drmFD == drmFD);
+
+    if (drmSyncobjTransfer(drmFD, handle, toPoint, from->handle, fromPoint, 0)) {
+        Debug::log(ERR, "CSyncTimeline::transfer: drmSyncobjTransfer failed");
+        return false;
+    }
+
+    return true;
+}
diff --git a/src/helpers/sync/SyncTimeline.hpp b/src/helpers/sync/SyncTimeline.hpp
new file mode 100644
index 000000000..4bb8019b9
--- /dev/null
+++ b/src/helpers/sync/SyncTimeline.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+#include <vector>
+#include <functional>
+
+/*
+    Hyprland synchronization timelines are based on the wlroots' ones, which
+    are based on Vk timeline semaphores: https://www.khronos.org/blog/vulkan-timeline-semaphores
+*/
+
+struct wl_event_source;
+
+class CSyncTimeline {
+  public:
+    static SP<CSyncTimeline> create(int drmFD_);
+    static SP<CSyncTimeline> create(int drmFD_, int drmSyncobjFD);
+    ~CSyncTimeline();
+
+    struct SWaiter {
+        std::function<void()> fn;
+        wl_event_source*      source = nullptr;
+        WP<CSyncTimeline>     timeline;
+    };
+
+    // check if the timeline point has been signaled
+    // flags: DRM_SYNCOBJ_WAIT_FLAGS_WAIT_FOR_SUBMIT or DRM_SYNCOBJ_WAIT_FLAGS_WAIT_AVAILABLE
+    // std::nullopt on fail
+    std::optional<bool> check(uint64_t point, uint32_t flags);
+
+    bool                addWaiter(const std::function<void()>& waiter, uint64_t point, uint32_t flags);
+    void                removeWaiter(SWaiter*);
+    int                 exportAsSyncFileFD(uint64_t src);
+    bool                importFromSyncFileFD(uint64_t dst, int fd);
+    bool                transfer(SP<CSyncTimeline> from, uint64_t fromPoint, uint64_t toPoint);
+
+    int                 drmFD  = -1;
+    uint32_t            handle = 0;
+    WP<CSyncTimeline>   self;
+
+  private:
+    CSyncTimeline() = default;
+
+    std::vector<SP<SWaiter>> waiters;
+};
diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp
index 47cf1767e..db81e0aeb 100644
--- a/src/managers/ProtocolManager.cpp
+++ b/src/managers/ProtocolManager.cpp
@@ -36,6 +36,7 @@
 #include "../protocols/MesaDRM.hpp"
 #include "../protocols/LinuxDMABUF.hpp"
 #include "../protocols/DRMLease.hpp"
+#include "../protocols/DRMSyncobj.hpp"
 
 #include "../protocols/core/Seat.hpp"
 #include "../protocols/core/DataDevice.hpp"
@@ -131,6 +132,7 @@ CProtocolManager::CProtocolManager() {
     PROTO::dataWlr             = std::make_unique<CDataDeviceWLRProtocol>(&zwlr_data_control_manager_v1_interface, 2, "DataDeviceWlr");
     PROTO::primarySelection    = std::make_unique<CPrimarySelectionProtocol>(&zwp_primary_selection_device_manager_v1_interface, 1, "PrimarySelection");
     PROTO::xwaylandShell       = std::make_unique<CXWaylandShellProtocol>(&xwayland_shell_v1_interface, 1, "XWaylandShell");
+    PROTO::sync                = std::make_unique<CDRMSyncobjProtocol>(&wp_linux_drm_syncobj_manager_v1_interface, 1, "DRMSyncobj");
 
     for (auto& b : g_pCompositor->m_pAqBackend->getImplementations()) {
         if (b->type() != Aquamarine::AQ_BACKEND_DRM)
diff --git a/src/managers/eventLoop/EventLoopManager.hpp b/src/managers/eventLoop/EventLoopManager.hpp
index eff281d6e..6f3e8529b 100644
--- a/src/managers/eventLoop/EventLoopManager.hpp
+++ b/src/managers/eventLoop/EventLoopManager.hpp
@@ -50,6 +50,8 @@ class CEventLoopManager {
 
     SIdleData m_sIdle;
     std::vector<SP<Aquamarine::SPollFD>> aqPollFDs;
+
+    friend class CSyncTimeline;
 };
 
 inline std::unique_ptr<CEventLoopManager> g_pEventLoopManager;
\ No newline at end of file
diff --git a/src/protocols/DRMSyncobj.cpp b/src/protocols/DRMSyncobj.cpp
new file mode 100644
index 000000000..69b9fae24
--- /dev/null
+++ b/src/protocols/DRMSyncobj.cpp
@@ -0,0 +1,182 @@
+#include "DRMSyncobj.hpp"
+#include <algorithm>
+
+#include "core/Compositor.hpp"
+#include "../helpers/sync/SyncTimeline.hpp"
+#include "../Compositor.hpp"
+
+#include <fcntl.h>
+
+#define LOGM PROTO::sync->protoLog
+
+CDRMSyncobjSurfaceResource::CDRMSyncobjSurfaceResource(SP<CWpLinuxDrmSyncobjSurfaceV1> resource_, SP<CWLSurfaceResource> surface_) : surface(surface_), resource(resource_) {
+    if (!good())
+        return;
+
+    resource->setData(this);
+
+    resource->setOnDestroy([this](CWpLinuxDrmSyncobjSurfaceV1* r) { PROTO::sync->destroyResource(this); });
+    resource->setDestroy([this](CWpLinuxDrmSyncobjSurfaceV1* r) { PROTO::sync->destroyResource(this); });
+
+    resource->setSetAcquirePoint([this](CWpLinuxDrmSyncobjSurfaceV1* r, wl_resource* timeline_, uint32_t hi, uint32_t lo) {
+        if (!surface) {
+            resource->error(WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_NO_SURFACE, "Surface is gone");
+            return;
+        }
+
+        auto timeline   = CDRMSyncobjTimelineResource::fromResource(timeline_);
+        acquireTimeline = timeline;
+        acquirePoint    = ((uint64_t)hi << 32) | (uint64_t)lo;
+    });
+
+    resource->setSetReleasePoint([this](CWpLinuxDrmSyncobjSurfaceV1* r, wl_resource* timeline_, uint32_t hi, uint32_t lo) {
+        if (!surface) {
+            resource->error(WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_NO_SURFACE, "Surface is gone");
+            return;
+        }
+
+        auto timeline   = CDRMSyncobjTimelineResource::fromResource(timeline_);
+        releaseTimeline = timeline;
+        releasePoint    = ((uint64_t)hi << 32) | (uint64_t)lo;
+    });
+
+    listeners.surfacePrecommit = surface->events.precommit.registerListener([this](std::any d) {
+        if (!!acquireTimeline != !!releaseTimeline) {
+            resource->error(acquireTimeline ? WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_NO_RELEASE_POINT : WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_NO_ACQUIRE_POINT, "Missing timeline");
+            surface->pending.rejected = true;
+            return;
+        }
+
+        if ((acquireTimeline || releaseTimeline) && !surface->pending.buffer) {
+            resource->error(WP_LINUX_DRM_SYNCOBJ_SURFACE_V1_ERROR_NO_BUFFER, "Missing buffer");
+            surface->pending.rejected = true;
+            return;
+        }
+
+        if (!acquireTimeline)
+            return;
+
+        // wait for the acquire timeline to materialize
+        auto materialized = acquireTimeline->timeline->check(acquirePoint, DRM_SYNCOBJ_WAIT_FLAGS_WAIT_AVAILABLE);
+        if (!materialized.has_value()) {
+            resource->noMemory();
+            return;
+        }
+
+        if (materialized)
+            return;
+
+        surface->lockPendingState();
+        acquireTimeline->timeline->addWaiter([this]() { surface->unlockPendingState(); }, acquirePoint, DRM_SYNCOBJ_WAIT_FLAGS_WAIT_AVAILABLE);
+    });
+}
+
+bool CDRMSyncobjSurfaceResource::good() {
+    return resource->resource();
+}
+
+CDRMSyncobjTimelineResource::CDRMSyncobjTimelineResource(SP<CWpLinuxDrmSyncobjTimelineV1> resource_, int fd_) : fd(fd_), resource(resource_) {
+    if (!good())
+        return;
+
+    resource->setData(this);
+
+    resource->setOnDestroy([this](CWpLinuxDrmSyncobjTimelineV1* r) { PROTO::sync->destroyResource(this); });
+    resource->setDestroy([this](CWpLinuxDrmSyncobjTimelineV1* r) { PROTO::sync->destroyResource(this); });
+
+    timeline = CSyncTimeline::create(PROTO::sync->drmFD, fd);
+
+    if (!timeline) {
+        resource->error(WP_LINUX_DRM_SYNCOBJ_MANAGER_V1_ERROR_INVALID_TIMELINE, "Timeline failed importing");
+        return;
+    }
+}
+
+SP<CDRMSyncobjTimelineResource> CDRMSyncobjTimelineResource::fromResource(wl_resource* res) {
+    auto data = (CDRMSyncobjTimelineResource*)(((CWpLinuxDrmSyncobjTimelineV1*)wl_resource_get_user_data(res))->data());
+    return data ? data->self.lock() : nullptr;
+}
+
+bool CDRMSyncobjTimelineResource::good() {
+    return resource->resource();
+}
+
+CDRMSyncobjManagerResource::CDRMSyncobjManagerResource(SP<CWpLinuxDrmSyncobjManagerV1> resource_) : resource(resource_) {
+    if (!good())
+        return;
+
+    resource->setOnDestroy([this](CWpLinuxDrmSyncobjManagerV1* r) { PROTO::sync->destroyResource(this); });
+    resource->setDestroy([this](CWpLinuxDrmSyncobjManagerV1* r) { PROTO::sync->destroyResource(this); });
+
+    resource->setGetSurface([this](CWpLinuxDrmSyncobjManagerV1* r, uint32_t id, wl_resource* surf) {
+        if (!surf) {
+            resource->error(-1, "Invalid surface");
+            return;
+        }
+
+        auto SURF = CWLSurfaceResource::fromResource(surf);
+        if (!SURF) {
+            resource->error(-1, "Invalid surface (2)");
+            return;
+        }
+
+        if (SURF->syncobj) {
+            resource->error(WP_LINUX_DRM_SYNCOBJ_MANAGER_V1_ERROR_SURFACE_EXISTS, "Surface already has a syncobj attached");
+            return;
+        }
+
+        auto RESOURCE = makeShared<CDRMSyncobjSurfaceResource>(makeShared<CWpLinuxDrmSyncobjSurfaceV1>(resource->client(), resource->version(), id), SURF);
+        if (!RESOURCE->good()) {
+            resource->noMemory();
+            return;
+        }
+
+        PROTO::sync->m_vSurfaces.emplace_back(RESOURCE);
+        SURF->syncobj = RESOURCE;
+
+        LOGM(LOG, "New linux_syncobj at {:x} for surface {:x}", (uintptr_t)RESOURCE.get(), (uintptr_t)SURF.get());
+    });
+
+    resource->setImportTimeline([this](CWpLinuxDrmSyncobjManagerV1* r, uint32_t id, int32_t fd) {
+        auto RESOURCE = makeShared<CDRMSyncobjTimelineResource>(makeShared<CWpLinuxDrmSyncobjTimelineV1>(resource->client(), resource->version(), id), fd);
+        if (!RESOURCE->good()) {
+            resource->noMemory();
+            return;
+        }
+
+        PROTO::sync->m_vTimelines.emplace_back(RESOURCE);
+        RESOURCE->self = RESOURCE;
+
+        LOGM(LOG, "New linux_drm_timeline at {:x}", (uintptr_t)RESOURCE.get());
+    });
+}
+
+bool CDRMSyncobjManagerResource::good() {
+    return resource->resource();
+}
+
+CDRMSyncobjProtocol::CDRMSyncobjProtocol(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) {
+    drmFD = fcntl(g_pCompositor->m_iDRMFD, F_DUPFD_CLOEXEC, 0);
+}
+
+void CDRMSyncobjProtocol::bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) {
+    const auto RESOURCE = m_vManagers.emplace_back(makeShared<CDRMSyncobjManagerResource>(makeShared<CWpLinuxDrmSyncobjManagerV1>(client, ver, id)));
+
+    if (!RESOURCE->good()) {
+        wl_client_post_no_memory(client);
+        m_vManagers.pop_back();
+        return;
+    }
+}
+
+void CDRMSyncobjProtocol::destroyResource(CDRMSyncobjManagerResource* resource) {
+    std::erase_if(m_vManagers, [resource](const auto& e) { return e.get() == resource; });
+}
+
+void CDRMSyncobjProtocol::destroyResource(CDRMSyncobjTimelineResource* resource) {
+    std::erase_if(m_vTimelines, [resource](const auto& e) { return e.get() == resource; });
+}
+
+void CDRMSyncobjProtocol::destroyResource(CDRMSyncobjSurfaceResource* resource) {
+    std::erase_if(m_vSurfaces, [resource](const auto& e) { return e.get() == resource; });
+}
diff --git a/src/protocols/DRMSyncobj.hpp b/src/protocols/DRMSyncobj.hpp
new file mode 100644
index 000000000..c1c884fff
--- /dev/null
+++ b/src/protocols/DRMSyncobj.hpp
@@ -0,0 +1,82 @@
+#pragma once
+
+#include <memory>
+#include <vector>
+#include "WaylandProtocol.hpp"
+#include "linux-drm-syncobj-v1.hpp"
+#include "../helpers/signal/Signal.hpp"
+
+class CWLSurfaceResource;
+class CDRMSyncobjTimelineResource;
+class CSyncTimeline;
+
+class CDRMSyncobjSurfaceResource {
+  public:
+    CDRMSyncobjSurfaceResource(SP<CWpLinuxDrmSyncobjSurfaceV1> resource_, SP<CWLSurfaceResource> surface_);
+
+    bool                            good();
+
+    WP<CWLSurfaceResource>          surface;
+    WP<CDRMSyncobjTimelineResource> acquireTimeline, releaseTimeline;
+    uint64_t                        acquirePoint = 0, releasePoint = 0;
+
+  private:
+    SP<CWpLinuxDrmSyncobjSurfaceV1> resource;
+
+    struct {
+        CHyprSignalListener surfacePrecommit;
+    } listeners;
+};
+
+class CDRMSyncobjTimelineResource {
+  public:
+    CDRMSyncobjTimelineResource(SP<CWpLinuxDrmSyncobjTimelineV1> resource_, int fd_);
+    static SP<CDRMSyncobjTimelineResource> fromResource(wl_resource*);
+
+    bool                                   good();
+
+    WP<CDRMSyncobjTimelineResource>        self;
+    int                                    fd = -1;
+    SP<CSyncTimeline>                      timeline;
+
+  private:
+    SP<CWpLinuxDrmSyncobjTimelineV1> resource;
+};
+
+class CDRMSyncobjManagerResource {
+  public:
+    CDRMSyncobjManagerResource(SP<CWpLinuxDrmSyncobjManagerV1> resource_);
+
+    bool good();
+
+  private:
+    SP<CWpLinuxDrmSyncobjManagerV1> resource;
+};
+
+class CDRMSyncobjProtocol : public IWaylandProtocol {
+  public:
+    CDRMSyncobjProtocol(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);
+
+  private:
+    void destroyResource(CDRMSyncobjManagerResource* resource);
+    void destroyResource(CDRMSyncobjTimelineResource* resource);
+    void destroyResource(CDRMSyncobjSurfaceResource* resource);
+
+    //
+    std::vector<SP<CDRMSyncobjManagerResource>>  m_vManagers;
+    std::vector<SP<CDRMSyncobjTimelineResource>> m_vTimelines;
+    std::vector<SP<CDRMSyncobjSurfaceResource>>  m_vSurfaces;
+
+    //
+    int drmFD = -1;
+
+    friend class CDRMSyncobjManagerResource;
+    friend class CDRMSyncobjTimelineResource;
+    friend class CDRMSyncobjSurfaceResource;
+};
+
+namespace PROTO {
+    inline UP<CDRMSyncobjProtocol> sync;
+};
diff --git a/src/protocols/Viewporter.cpp b/src/protocols/Viewporter.cpp
index 8cb69dbe3..7fa45e69b 100644
--- a/src/protocols/Viewporter.cpp
+++ b/src/protocols/Viewporter.cpp
@@ -52,6 +52,21 @@ CViewportResource::CViewportResource(SP<CWpViewport> resource_, SP<CWLSurfaceRes
         surface->pending.viewport.hasSource = true;
         surface->pending.viewport.source    = {x, y, w, h};
     });
+
+    listeners.surfacePrecommit = surface->events.precommit.registerListener([this](std::any d) {
+        if (!surface)
+            return;
+
+        if (surface->pending.viewport.hasSource) {
+            auto& src = surface->pending.viewport.source;
+
+            if (src.w + src.x > surface->pending.size.x || src.h + src.y > surface->pending.size.y) {
+                resource->error(WP_VIEWPORT_ERROR_BAD_VALUE, "Box doesn't fit");
+                surface->pending.rejected = true;
+                return;
+            }
+        }
+    });
 }
 
 CViewportResource::~CViewportResource() {
@@ -66,20 +81,6 @@ bool CViewportResource::good() {
     return resource->resource();
 }
 
-void CViewportResource::verify() {
-    if (!surface)
-        return;
-
-    if (surface->pending.viewport.hasSource) {
-        auto& src = surface->pending.viewport.source;
-
-        if (src.w + src.x > surface->pending.size.x || src.h + src.y > surface->pending.size.y) {
-            resource->error(WP_VIEWPORT_ERROR_BAD_VALUE, "Box doesn't fit");
-            return;
-        }
-    }
-}
-
 CViewporterResource::CViewporterResource(SP<CWpViewporter> resource_) : resource(resource_) {
     if (!good())
         return;
diff --git a/src/protocols/Viewporter.hpp b/src/protocols/Viewporter.hpp
index 012782036..fd0da88e1 100644
--- a/src/protocols/Viewporter.hpp
+++ b/src/protocols/Viewporter.hpp
@@ -15,11 +15,14 @@ class CViewportResource {
     ~CViewportResource();
 
     bool                   good();
-    void                   verify();
     WP<CWLSurfaceResource> surface;
 
   private:
     SP<CWpViewport> resource;
+
+    struct {
+      CHyprSignalListener surfacePrecommit;
+    } listeners;
 };
 
 class CViewporterResource {
diff --git a/src/protocols/core/Compositor.cpp b/src/protocols/core/Compositor.cpp
index 5f016918a..165f365cd 100644
--- a/src/protocols/core/Compositor.cpp
+++ b/src/protocols/core/Compositor.cpp
@@ -6,6 +6,8 @@
 #include "Subcompositor.hpp"
 #include "../Viewporter.hpp"
 #include "../../helpers/Monitor.hpp"
+#include "../PresentationTime.hpp"
+#include "../DRMSyncobj.hpp"
 
 #define LOGM PROTO::compositor->protoLog
 
@@ -102,58 +104,14 @@ CWLSurfaceResource::CWLSurfaceResource(SP<CWlSurface> resource_) : resource(reso
             pending.size = tfs / pending.scale;
         }
 
-        if (viewportResource)
-            viewportResource->verify();
-
         pending.damage.intersect(CBox{{}, pending.size});
 
-        auto    previousBuffer       = current.buffer;
-        CRegion previousBufferDamage = accumulateCurrentBufferDamage();
+        events.precommit.emit();
+        if (pending.rejected)
+            return;
 
-        current = pending;
-        pending.damage.clear();
-        pending.bufferDamage.clear();
-
-        if (current.buffer && !bufferReleased) {
-            //                                                                                                                          without previous dolphin et al are weird vvv
-            //CRegion surfaceDamage =
-            //    current.damage.copy().scale(current.scale).transform(current.transform, current.size.x, current.size.y).add(current.bufferDamage).add(previousBufferDamage);
-            current.buffer->update(CBox{{}, {INT32_MAX, INT32_MAX}}); // FIXME: figure this out to not use this hack. QT apps are wonky without this.
-
-            // release the buffer if it's synchronous as update() has done everything thats needed
-            // so we can let the app know we're done.
-            if (current.buffer->isSynchronous()) {
-                current.buffer->sendRelease();
-                bufferReleased = true;
-            }
-        }
-
-        // TODO: we should _accumulate_ and not replace above if sync
-        if (role->role() == SURFACE_ROLE_SUBSURFACE) {
-            auto subsurface = (CWLSubsurfaceResource*)role.get();
-            if (subsurface->sync)
-                return;
-
-            events.commit.emit();
-        } else {
-            // send commit to all synced surfaces in this tree.
-            breadthfirst(
-                [](SP<CWLSurfaceResource> surf, const Vector2D& offset, void* data) {
-                    if (surf->role->role() == SURFACE_ROLE_SUBSURFACE) {
-                        auto subsurface = (CWLSubsurfaceResource*)surf->role.get();
-                        if (!subsurface->sync)
-                            return;
-                    }
-                    surf->events.commit.emit();
-                },
-                nullptr);
-        }
-
-        // for async buffers, we can only release the buffer once we are unrefing it from current.
-        if (previousBuffer && !previousBuffer->isSynchronous() && !bufferReleased) {
-            previousBuffer->sendRelease();
-            bufferReleased = true;
-        }
+        if (stateLocks <= 0)
+            commitPendingState();
     });
 
     resource->setDamage([this](CWlSurface* r, int32_t x, int32_t y, int32_t w, int32_t h) { pending.damage.add(CBox{x, y, w, h}); });
@@ -429,6 +387,83 @@ CRegion CWLSurfaceResource::accumulateCurrentBufferDamage() {
     return surfaceDamage.scale(current.scale).transform(wlTransformToHyprutils(invertTransform(current.transform)), trc.x, trc.y).add(current.bufferDamage);
 }
 
+void CWLSurfaceResource::lockPendingState() {
+    stateLocks++;
+}
+
+void CWLSurfaceResource::unlockPendingState() {
+    stateLocks--;
+    if (stateLocks <= 0)
+        commitPendingState();
+}
+
+void CWLSurfaceResource::commitPendingState() {
+    auto    previousBuffer       = current.buffer;
+    CRegion previousBufferDamage = accumulateCurrentBufferDamage();
+
+    current = pending;
+    pending.damage.clear();
+    pending.bufferDamage.clear();
+
+    if (current.buffer && !bufferReleased) {
+        //                                                                                                                          without previous dolphin et al are weird vvv
+        //CRegion surfaceDamage =
+        //    current.damage.copy().scale(current.scale).transform(current.transform, current.size.x, current.size.y).add(current.bufferDamage).add(previousBufferDamage);
+        current.buffer->update(CBox{{}, {INT32_MAX, INT32_MAX}}); // FIXME: figure this out to not use this hack. QT apps are wonky without this.
+
+        // release the buffer if it's synchronous as update() has done everything thats needed
+        // so we can let the app know we're done.
+        if (current.buffer->isSynchronous()) {
+            current.buffer->sendRelease();
+            bufferReleased = true;
+        }
+    }
+
+    // TODO: we should _accumulate_ and not replace above if sync
+    if (role->role() == SURFACE_ROLE_SUBSURFACE) {
+        auto subsurface = (CWLSubsurfaceResource*)role.get();
+        if (subsurface->sync)
+            return;
+
+        events.commit.emit();
+    } else {
+        // send commit to all synced surfaces in this tree.
+        breadthfirst(
+            [](SP<CWLSurfaceResource> surf, const Vector2D& offset, void* data) {
+                if (surf->role->role() == SURFACE_ROLE_SUBSURFACE) {
+                    auto subsurface = (CWLSubsurfaceResource*)surf->role.get();
+                    if (!subsurface->sync)
+                        return;
+                }
+                surf->events.commit.emit();
+            },
+            nullptr);
+    }
+
+    // for async buffers, we can only release the buffer once we are unrefing it from current.
+    if (previousBuffer && !previousBuffer->isSynchronous() && !bufferReleased) {
+        previousBuffer->sendRelease();
+        bufferReleased = true;
+    }
+}
+
+void CWLSurfaceResource::presentFeedback(timespec* when, CMonitor* pMonitor) {
+    frame(when);
+    auto FEEDBACK = makeShared<CQueuedPresentationData>(self.lock());
+    FEEDBACK->attachMonitor(pMonitor);
+    FEEDBACK->discarded();
+    PROTO::presentation->queueData(FEEDBACK);
+
+    if (!pMonitor || !pMonitor->outTimeline || !syncobj)
+        return;
+
+    // attach explicit sync
+    g_pHyprRenderer->explicitPresented.emplace_back(self.lock());
+
+    if (syncobj->acquirePoint > pMonitor->lastWaitPoint)
+        pMonitor->lastWaitPoint = syncobj->acquirePoint;
+}
+
 CWLCompositorResource::CWLCompositorResource(SP<CWlCompositor> resource_) : resource(resource_) {
     if (!good())
         return;
diff --git a/src/protocols/core/Compositor.hpp b/src/protocols/core/Compositor.hpp
index a232ac306..0d85a5c9e 100644
--- a/src/protocols/core/Compositor.hpp
+++ b/src/protocols/core/Compositor.hpp
@@ -24,6 +24,7 @@ class CWLSurface;
 class CWLSurfaceResource;
 class CWLSubsurfaceResource;
 class CViewportResource;
+class CDRMSyncobjSurfaceResource;
 
 class CWLCallbackResource {
   public:
@@ -74,6 +75,7 @@ class CWLSurfaceResource {
     Vector2D                      sourceSize();
 
     struct {
+        CSignal precommit;
         CSignal commit;
         CSignal map;
         CSignal unmap;
@@ -81,7 +83,7 @@ class CWLSurfaceResource {
         CSignal destroy;
     } events;
 
-    struct {
+    struct SState {
         CRegion             opaque, input = CBox{{}, {INT32_MAX, INT32_MAX}}, damage, bufferDamage = CBox{{}, {INT32_MAX, INT32_MAX}} /* initial damage */;
         wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL;
         int                 scale     = 1;
@@ -95,6 +97,7 @@ class CWLSurfaceResource {
             Vector2D destination;
             CBox     source;
         } viewport;
+        bool rejected = false;
 
         //
         void reset() {
@@ -115,9 +118,13 @@ class CWLSurfaceResource {
     std::vector<WP<CWLSubsurfaceResource>> subsurfaces;
     WP<ISurfaceRole>                       role;
     WP<CViewportResource>                  viewportResource;
+    WP<CDRMSyncobjSurfaceResource>         syncobj; // may not be present
 
     void                                   breadthfirst(std::function<void(SP<CWLSurfaceResource>, const Vector2D&, void*)> fn, void* data);
     CRegion                                accumulateCurrentBufferDamage();
+    void                                   presentFeedback(timespec* when, CMonitor* pMonitor);
+    void                                   lockPendingState();
+    void                                   unlockPendingState();
 
     // returns a pair: found surface (null if not found) and surface local coords.
     // localCoords param is relative to 0,0 of this surface
@@ -130,7 +137,10 @@ class CWLSurfaceResource {
     // tracks whether we should release the buffer
     bool bufferReleased = false;
 
+    int  stateLocks = 0;
+
     void destroy();
+    void commitPendingState();
     void bfHelper(std::vector<SP<CWLSurfaceResource>> nodes, std::function<void(SP<CWLSurfaceResource>, const Vector2D&, void*)> fn, void* data);
 };
 
diff --git a/src/render/OpenGL.cpp b/src/render/OpenGL.cpp
index 467c83f4d..adc45f2cd 100644
--- a/src/render/OpenGL.cpp
+++ b/src/render/OpenGL.cpp
@@ -171,6 +171,14 @@ CHyprOpenGLImpl::CHyprOpenGLImpl() {
     loadGLProc(&m_sProc.glEGLImageTargetTexture2DOES, "glEGLImageTargetTexture2DOES");
     loadGLProc(&m_sProc.eglDebugMessageControlKHR, "eglDebugMessageControlKHR");
     loadGLProc(&m_sProc.eglGetPlatformDisplayEXT, "eglGetPlatformDisplayEXT");
+    loadGLProc(&m_sProc.eglCreateSyncKHR, "eglCreateSyncKHR");
+    loadGLProc(&m_sProc.eglDestroySyncKHR, "eglDestroySyncKHR");
+    loadGLProc(&m_sProc.eglDupNativeFenceFDANDROID, "eglDupNativeFenceFDANDROID");
+    loadGLProc(&m_sProc.eglWaitSyncKHR, "eglWaitSyncKHR");
+
+    RASSERT(m_sProc.eglCreateSyncKHR, "Display driver doesn't support eglCreateSyncKHR");
+    RASSERT(m_sProc.eglDupNativeFenceFDANDROID, "Display driver doesn't support eglDupNativeFenceFDANDROID");
+    RASSERT(m_sProc.eglWaitSyncKHR, "Display driver doesn't support eglWaitSyncKHR");
 
     if (EGLEXTENSIONS.contains("EGL_EXT_device_base") || EGLEXTENSIONS.contains("EGL_EXT_device_enumeration"))
         loadGLProc(&m_sProc.eglQueryDevicesEXT, "eglQueryDevicesEXT");
@@ -2727,6 +2735,30 @@ std::vector<SDRMFormat> CHyprOpenGLImpl::getDRMFormats() {
     return drmFormats;
 }
 
+SP<CEGLSync> CHyprOpenGLImpl::createEGLSync(int fenceFD) {
+    std::vector<EGLint> attribs;
+    int                 dupFd = fcntl(fenceFD, F_DUPFD_CLOEXEC, 0);
+    if (dupFd < 0) {
+        Debug::log(ERR, "createEGLSync: dup failed");
+        return nullptr;
+    }
+
+    attribs.push_back(EGL_SYNC_NATIVE_FENCE_FD_ANDROID);
+    attribs.push_back(dupFd);
+    attribs.push_back(EGL_NONE);
+
+    EGLSyncKHR sync = m_sProc.eglCreateSyncKHR(m_pEglDisplay, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs.data());
+    if (sync == EGL_NO_SYNC_KHR) {
+        Debug::log(ERR, "eglCreateSyncKHR failed");
+        close(dupFd);
+        return nullptr;
+    }
+
+    auto eglsync = SP<CEGLSync>(new CEGLSync);
+    eglsync->sync = sync;
+    return eglsync;
+}
+
 void SRenderModifData::applyToBox(CBox& box) {
     if (!enabled)
         return;
@@ -2787,3 +2819,35 @@ float SRenderModifData::combinedScale() {
     }
     return scale;
 }
+
+CEGLSync::~CEGLSync() {
+    if (sync == EGL_NO_SYNC_KHR)
+        return;
+
+    if (g_pHyprOpenGL->m_sProc.eglDestroySyncKHR(g_pHyprOpenGL->m_pEglDisplay, sync) != EGL_TRUE)
+        Debug::log(ERR, "eglDestroySyncKHR failed");
+}
+
+int CEGLSync::dupFenceFD() {
+    if (sync == EGL_NO_SYNC_KHR)
+        return -1;
+
+    int fd = g_pHyprOpenGL->m_sProc.eglDupNativeFenceFDANDROID(g_pHyprOpenGL->m_pEglDisplay, sync);
+    if (fd == EGL_NO_NATIVE_FENCE_FD_ANDROID) {
+        Debug::log(ERR, "eglDupNativeFenceFDANDROID failed");
+        return -1;
+    }
+
+    return fd;
+}
+
+bool CEGLSync::wait() {
+    if (sync == EGL_NO_SYNC_KHR)
+        return false;
+
+    if (g_pHyprOpenGL->m_sProc.eglWaitSyncKHR(g_pHyprOpenGL->m_pEglDisplay, sync, 0) != EGL_TRUE) {
+        Debug::log(ERR, "eglWaitSyncKHR failed");
+        return false;
+    }
+    return true;
+}
diff --git a/src/render/OpenGL.hpp b/src/render/OpenGL.hpp
index 9696c2ccb..4c972a8f7 100644
--- a/src/render/OpenGL.hpp
+++ b/src/render/OpenGL.hpp
@@ -123,6 +123,21 @@ struct SCurrentRenderData {
     float                discardOpacity = 0.f;
 };
 
+class CEGLSync {
+  public:
+    ~CEGLSync();
+
+    EGLSyncKHR sync = nullptr;
+
+    int        dupFenceFD();
+    bool       wait();
+
+  private:
+    CEGLSync() = default;
+
+    friend class CHyprOpenGLImpl;
+};
+
 class CGradientValueData;
 
 class CHyprOpenGLImpl {
@@ -187,6 +202,7 @@ class CHyprOpenGLImpl {
     uint32_t getPreferredReadFormat(CMonitor* pMonitor);
     std::vector<SDRMFormat>                           getDRMFormats();
     EGLImageKHR                                       createEGLImage(const Aquamarine::SDMABUFAttrs& attrs);
+    SP<CEGLSync>                                      createEGLSync(int fenceFD);
 
     SCurrentRenderData                                m_RenderData;
 
@@ -219,6 +235,10 @@ class CHyprOpenGLImpl {
         PFNEGLQUERYDEVICESEXTPROC                     eglQueryDevicesEXT                     = nullptr;
         PFNEGLQUERYDEVICESTRINGEXTPROC                eglQueryDeviceStringEXT                = nullptr;
         PFNEGLQUERYDISPLAYATTRIBEXTPROC               eglQueryDisplayAttribEXT               = nullptr;
+        PFNEGLCREATESYNCKHRPROC                       eglCreateSyncKHR                       = nullptr;
+        PFNEGLDESTROYSYNCKHRPROC                      eglDestroySyncKHR                      = nullptr;
+        PFNEGLDUPNATIVEFENCEFDANDROIDPROC             eglDupNativeFenceFDANDROID             = nullptr;
+        PFNEGLWAITSYNCKHRPROC                         eglWaitSyncKHR                         = nullptr;
     } m_sProc;
 
     struct {
diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp
index ac6fd8c1e..014295737 100644
--- a/src/render/Renderer.cpp
+++ b/src/render/Renderer.cpp
@@ -14,6 +14,8 @@
 #include "../protocols/PresentationTime.hpp"
 #include "../protocols/core/DataDevice.hpp"
 #include "../protocols/core/Compositor.hpp"
+#include "../protocols/DRMSyncobj.hpp"
+#include "../helpers/sync/SyncTimeline.hpp"
 
 extern "C" {
 #include <xf86drm.h>
@@ -109,6 +111,27 @@ static void renderSurface(SP<CWLSurfaceResource> surface, int x, int y, void* da
     if (!TEXTURE->m_iTexID)
         return;
 
+    // explicit sync: wait for the timeline, if any
+    if (surface->syncobj) {
+        int fd = surface->syncobj->acquireTimeline->timeline->exportAsSyncFileFD(surface->syncobj->acquirePoint);
+        if (fd < 0) {
+            Debug::log(ERR, "Renderer: failed to get a fd from explicit timeline");
+            return;
+        }
+
+        auto sync = g_pHyprOpenGL->createEGLSync(fd);
+        close(fd);
+        if (!sync) {
+            Debug::log(ERR, "Renderer: failed to get an eglsync from explicit timeline");
+            return;
+        }
+
+        if (!sync->wait()) {
+            Debug::log(ERR, "Renderer: failed to wait on an eglsync from explicit timeline");
+            return;
+        }
+    }
+
     TRACY_GPU_ZONE("RenderSurface");
 
     double      outputX = -RDATA->pMonitor->vecPosition.x, outputY = -RDATA->pMonitor->vecPosition.y;
@@ -167,13 +190,9 @@ static void renderSurface(SP<CWLSurfaceResource> surface, int x, int y, void* da
     }
 
     if (windowBox.width <= 1 || windowBox.height <= 1) {
-        if (!g_pHyprRenderer->m_bBlockSurfaceFeedback) {
-            surface->frame(RDATA->when);
-            auto FEEDBACK = makeShared<CQueuedPresentationData>(surface);
-            FEEDBACK->attachMonitor(RDATA->pMonitor);
-            FEEDBACK->discarded();
-            PROTO::presentation->queueData(FEEDBACK);
-        }
+        if (!g_pHyprRenderer->m_bBlockSurfaceFeedback)
+            surface->presentFeedback(RDATA->when, RDATA->pMonitor);
+
         return; // invisible
     }
 
@@ -225,13 +244,8 @@ static void renderSurface(SP<CWLSurfaceResource> surface, int x, int y, void* da
             g_pHyprOpenGL->renderTexture(TEXTURE, &windowBox, ALPHA, rounding, false, true);
     }
 
-    if (!g_pHyprRenderer->m_bBlockSurfaceFeedback) {
-        surface->frame(RDATA->when);
-        auto FEEDBACK = makeShared<CQueuedPresentationData>(surface);
-        FEEDBACK->attachMonitor(RDATA->pMonitor);
-        FEEDBACK->presented();
-        PROTO::presentation->queueData(FEEDBACK);
-    }
+    if (!g_pHyprRenderer->m_bBlockSurfaceFeedback)
+        surface->presentFeedback(RDATA->when, RDATA->pMonitor);
 
     g_pHyprOpenGL->blend(true);
 
@@ -1111,12 +1125,7 @@ bool CHyprRenderer::attemptDirectScanout(CMonitor* pMonitor) {
 
     timespec now;
     clock_gettime(CLOCK_MONOTONIC, &now);
-    PSURFACE->frame(&now);
-    auto FEEDBACK = makeShared<CQueuedPresentationData>(PSURFACE);
-    FEEDBACK->attachMonitor(pMonitor);
-    FEEDBACK->presented();
-    FEEDBACK->setPresentationType(true);
-    PROTO::presentation->queueData(FEEDBACK);
+    PSURFACE->presentFeedback(&now, pMonitor);
 
     if (pMonitor->state.commit()) {
         if (m_pLastScanout.expired()) {
@@ -1421,6 +1430,18 @@ void CHyprRenderer::renderMonitor(CMonitor* pMonitor) {
     pMonitor->output->state->setPresentationMode(shouldTear ? Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_IMMEDIATE :
                                                               Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_VSYNC);
 
+    // apply timelines for explicit sync
+    pMonitor->output->state->setExplicitInFence(pMonitor->inTimeline->exportAsSyncFileFD(pMonitor->lastWaitPoint));
+
+    for (auto& e : explicitPresented) {
+        e->syncobj->releaseTimeline->timeline->transfer(pMonitor->outTimeline, pMonitor->commitSeq, e->syncobj->releasePoint);
+    }
+
+    pMonitor->lastWaitPoint = 0;
+    explicitPresented.clear();
+    pMonitor->output->state->setExplicitOutFence(pMonitor->outTimeline->exportAsSyncFileFD(pMonitor->commitSeq));
+    pMonitor->commitSeq++;
+
     if (!pMonitor->state.commit()) {
         // rollback the buffer to avoid writing to the front buffer that is being
         // displayed
diff --git a/src/render/Renderer.hpp b/src/render/Renderer.hpp
index ce37f51b2..b15ba04bb 100644
--- a/src/render/Renderer.hpp
+++ b/src/render/Renderer.hpp
@@ -99,6 +99,8 @@ class CHyprRenderer {
 
     CTimer           m_tRenderTimer;
 
+    std::vector<SP<CWLSurfaceResource>> explicitPresented;
+
     struct {
         int                           hotspotX;
         int                           hotspotY;