Subject: [PATCH 15/24] wip: interlock ordinary and grouped DRM events

From: Justin Youens <[email-masked]>
Date: Sat, 15 Aug 2026 11:10:35 -0500

---
 src/backend/drm/DRM.cpp                   |   9 +-
 src/backend/drm/TiledAtomic.hpp           |  14 ++
 src/backend/drm/TiledDRMIntegration.cpp   |  59 +++++-
 src/backend/drm/TiledDRMIntegration.hpp   |   9 +
 src/backend/drm/TiledRuntime.cpp          |   7 +-
 src/backend/drm/TiledRuntime.hpp          |   1 +
 src/backend/drm/TiledRuntimeAuthority.cpp |  71 ++++++-
 src/backend/drm/TiledRuntimeAuthority.hpp |  27 +++
 src/backend/drm/impl/Atomic.cpp           | 224 ++++++++++++----------
 src/backend/drm/impl/Legacy.cpp           |  12 +-
 tests/TiledAtomic.cpp                     | 149 ++++++++++++++
 tests/TiledDRMIntegration.cpp             |  16 +-
 tests/TiledRuntime.cpp                    |   2 +-
 13 files changed, 489 insertions(+), 111 deletions(-)

diff --git a/src/backend/drm/DRM.cpp b/src/backend/drm/DRM.cpp
index e9afd9bb18cd2f05d7b8c115ff9278947af2ab0a..0617b2fd6b29b663832a53b0b45dd202cb1f50d7 100644
--- a/src/backend/drm/DRM.cpp
+++ b/src/backend/drm/DRM.cpp
@@ -1810,8 +1810,13 @@ static void                      handlePF(int fd, unsigned seq, unsigned tv_sec,
     if (!BACKEND || BACKEND->drmFD() != fd)
         return;
 
-    const auto FLIPID = rc<uintptr_t>(data);
-    const auto CRTC   = BACKEND->crtcByID(crtc_id);
+    const auto FLIPID  = rc<uintptr_t>(data);
+    const auto GROUPED = NDRM::NTiledDRMIntegration::routeGroupedPageFlip(BACKEND, FLIPID, crtc_id, seq,
+                                                                          (static_cast<uint64_t>(tv_sec) * 1'000'000'000ULL) + (static_cast<uint64_t>(tv_usec) * 1'000ULL));
+    if (GROUPED.claimed)
+        return;
+
+    const auto CRTC = BACKEND->crtcByID(crtc_id);
 
     if (!CRTC || !FLIPID || CRTC->pendingFlip.id != FLIPID) {
         BACKEND->log(AQ_LOG_DEBUG, std::format("drm: Ignoring a stale pf event, flip {} on crtc {}", FLIPID, crtc_id));
diff --git a/src/backend/drm/TiledAtomic.hpp b/src/backend/drm/TiledAtomic.hpp
index decda3473adff7c58364856469204d5054aea3cc..83fc8790a36d08f8cdedd79eac0cbf853f59eada 100644
--- a/src/backend/drm/TiledAtomic.hpp
+++ b/src/backend/drm/TiledAtomic.hpp
@@ -360,6 +360,7 @@ namespace Aquamarine::NDRM::NTiledAtomic { // NOLINT(readability-identifier-nami
         REQUEST_ALLOCATION_FAILED,
         PROPERTY_ADD_FAILED,
         RUNTIME_PREPARE_FAILED,
+        MEMBER_PENDING_FLIP,
         RESERVATION_FAILED,
         RESERVATION_BIND_FAILED,
         SUBMISSION_FAILED,
@@ -495,6 +496,19 @@ namespace Aquamarine::NDRM::NTiledAtomic { // NOLINT(readability-identifier-nami
                 return result;
             }
 
+            for (const auto& MEMBER : route->members()) {
+                if (!MEMBER.crtc || MEMBER.crtcId == 0 || MEMBER.crtc->id != MEMBER.crtcId) {
+                    ROLLBACK_RUNTIME();
+                    result.status = eTiledFrameTransactionStatus::INVALID_INPUT;
+                    return result;
+                }
+                if (MEMBER.crtc->pendingFlip.id.has_value()) {
+                    ROLLBACK_RUNTIME();
+                    result.status = eTiledFrameTransactionStatus::MEMBER_PENDING_FLIP;
+                    return result;
+                }
+            }
+
             const auto RESERVED     = registry.reserveAll(*PREPARED_ROUTE);
             result.reservationError = RESERVED.error;
             if (!RESERVED) {
diff --git a/src/backend/drm/TiledDRMIntegration.cpp b/src/backend/drm/TiledDRMIntegration.cpp
index e45ff9df9c20a1f46c925d5da32cdf934cf45482..c834ccacfe25024955a4707c393e477fa4d7555f 100644
--- a/src/backend/drm/TiledDRMIntegration.cpp
+++ b/src/backend/drm/TiledDRMIntegration.cpp
@@ -83,6 +83,14 @@ namespace {
         }
         return std::nullopt;
     }
+
+    SOrdinaryKMSGateResult invokeUnmanagedOrdinaryKMS(const std::function<bool()>& callback) noexcept {
+        if (!callback)
+            return {.status = eOrdinaryKMSGateStatus::INVALID_INPUT};
+        try {
+            return {.status = eOrdinaryKMSGateStatus::EXECUTED, .callbackResult = callback()};
+        } catch (...) { return {.status = eOrdinaryKMSGateStatus::CALLBACK_FAILED}; }
+    }
 }
 
 class CTiledDRMIntegrationRegistry::CImpl {
@@ -101,7 +109,7 @@ class CTiledDRMIntegrationRegistry::CImpl {
         std::optional<eDetachTerminalProof>                         terminalProof;
         // Diagnostic context only. terminalizeDetachedKey() never derives an
         // authorization proof from this value.
-        eDetachReason                                               detachReason         = eDetachReason::BACKEND_SHUTDOWN;
+        eDetachReason detachReason = eDetachReason::BACKEND_SHUTDOWN;
     };
 
     explicit CImpl(std::thread::id thread) : eventThread(thread), scanRegistry(thread) {}
@@ -1004,16 +1012,55 @@ SGroupedTestModesetResult CTiledDRMIntegrationRegistry::executeCurrentGroupedTes
     return executeCurrentGroupedTestModesetKey(backend.impl_, expected, group, outputMode);
 }
 
+SOrdinaryKMSGateResult CTiledDRMIntegrationRegistry::gateOrdinaryKMSKey(const void* key, const CWeakPointer<CDRMBackend>& backend, std::span<const SOrdinaryKMSCRTC> crtcs,
+                                                                        const std::function<bool()>& callback) noexcept {
+    const auto THREAD = m_impl->registeredThread(key);
+    if (!THREAD)
+        return invokeUnmanagedOrdinaryKMS(callback);
+    if (*THREAD != std::this_thread::get_id())
+        return {.status = eOrdinaryKMSGateStatus::WRONG_THREAD};
+
+    const auto ENTRY = m_impl->entries.find(key);
+    if (ENTRY == m_impl->entries.end() || !ENTRY->second.groupedRuntime)
+        return {.status = eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC};
+    if (ENTRY->second.detachBegun)
+        return {.status = eOrdinaryKMSGateStatus::TERMINAL};
+    if (ENTRY->second.allocationGateActive)
+        return {.status = eOrdinaryKMSGateStatus::BUSY};
+    if (crtcs.empty() || !callback)
+        return {.status = eOrdinaryKMSGateStatus::INVALID_INPUT};
+
+    const auto BACKEND = backend.lock();
+    if (!BACKEND || BACKEND.impl_ != key || !ENTRY->second.lifetime || ENTRY->second.lifetime->binding() != eBackendBinding::PRODUCTION_BACKEND ||
+        ENTRY->second.lifetime->drmBackend().impl_ != key)
+        return {.status = eOrdinaryKMSGateStatus::INVALID_INPUT};
+
+    for (const auto& CRTC : crtcs) {
+        if (!CRTC.owner || CRTC.id == 0 || CRTC.owner->id != CRTC.id || CRTC.owner->backend.impl_ != key)
+            return {.status = eOrdinaryKMSGateStatus::INVALID_INPUT};
+        const auto CACHED = BACKEND->crtcByID(CRTC.id);
+        if (!CACHED || CACHED.impl_ != CRTC.owner.impl_)
+            return {.status = eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC};
+    }
+
+    return ENTRY->second.groupedRuntime->gateOrdinaryKMS(crtcs, callback);
+}
+
+SOrdinaryKMSGateResult CTiledDRMIntegrationRegistry::gateOrdinaryKMS(const CWeakPointer<CDRMBackend>& backend, std::span<const SOrdinaryKMSCRTC> crtcs,
+                                                                     const std::function<bool()>& callback) noexcept {
+    return gateOrdinaryKMSKey(backend.impl_, backend, crtcs, callback);
+}
+
 SGroupedPageFlipResult CTiledDRMIntegrationRegistry::routeGroupedPageFlipKey(const void* key, uintptr_t transactionId, uint32_t crtcId, uint32_t sequence,
                                                                              uint64_t timestampNs) noexcept {
     const auto THREAD = m_impl->registeredThread(key);
     if (!THREAD || *THREAD != std::this_thread::get_id())
-        return {.claimed = transactionId != 0, .disposition = eGroupedPageFlipDisposition::WRONG_THREAD};
+        return {.disposition = eGroupedPageFlipDisposition::WRONG_THREAD};
     const auto ENTRY = m_impl->entries.find(key);
     if (ENTRY == m_impl->entries.end() || !ENTRY->second.groupedRuntime)
         return {};
     if (ENTRY->second.allocationGateActive)
-        return {.claimed = transactionId != 0, .disposition = eGroupedPageFlipDisposition::BUSY};
+        return {.claimed = ENTRY->second.groupedRuntime->claimsPageFlip(transactionId, crtcId), .disposition = eGroupedPageFlipDisposition::BUSY};
     return ENTRY->second.groupedRuntime->routePageFlip(transactionId, crtcId, sequence, timestampNs);
 }
 
@@ -1328,6 +1375,12 @@ bool Aquamarine::NDRM::NTiledDRMIntegration::withCurrentOutputBridge(const CWeak
     return REGISTRY && REGISTRY->withCurrentOutputBridge(backend, expected, callback);
 }
 
+SOrdinaryKMSGateResult Aquamarine::NDRM::NTiledDRMIntegration::gateOrdinaryKMS(const CWeakPointer<CDRMBackend>& backend, std::span<const SOrdinaryKMSCRTC> crtcs,
+                                                                               const std::function<bool()>& callback) noexcept {
+    auto* const REGISTRY = productionRegistryIfCreated();
+    return REGISTRY ? REGISTRY->gateOrdinaryKMS(backend, crtcs, callback) : invokeUnmanagedOrdinaryKMS(callback);
+}
+
 SGroupedPageFlipResult Aquamarine::NDRM::NTiledDRMIntegration::routeGroupedPageFlip(const CWeakPointer<CDRMBackend>& backend, uintptr_t transactionId, uint32_t crtcId,
                                                                                     uint32_t sequence, uint64_t timestampNs) noexcept {
     auto* const REGISTRY = productionRegistryIfCreated();
diff --git a/src/backend/drm/TiledDRMIntegration.hpp b/src/backend/drm/TiledDRMIntegration.hpp
index 226f7138e93e79e81eb20917a7e0c3b724ac89df..874aa7a12a96b8f03f546aa47d51fb2e012b2f13 100644
--- a/src/backend/drm/TiledDRMIntegration.hpp
+++ b/src/backend/drm/TiledDRMIntegration.hpp
@@ -8,6 +8,7 @@
 
 #include <cstdint>
 #include <functional>
+#include <span>
 #include <thread>
 #include <vector>
 
@@ -31,6 +32,8 @@ namespace Aquamarine::NDRM::NTiledDRMIntegration { // NOLINT(readability-identif
     using NTiledCrtcAllocator::SCurrentState;
     using NTiledOutputBridge::POutputBridgeObservation;
     using NTiledRuntimeAuthority::IGroupedTestFramebufferSource;
+    using NTiledRuntimeAuthority::SOrdinaryKMSCRTC;
+    using NTiledRuntimeAuthority::SOrdinaryKMSGateResult;
     using NTiledRuntimeAuthority::SGroupedPageFlipResult;
     using NTiledRuntimeAuthority::SGroupedTestModesetResult;
     using NTiledScan::PCommittedScan;
@@ -236,6 +239,8 @@ namespace Aquamarine::NDRM::NTiledDRMIntegration { // NOLINT(readability-identif
                                                                         const std::function<void(const POutputBridgeObservation&)>& callback) noexcept;
         [[nodiscard]] SGroupedTestModesetResult executeCurrentGroupedTestModeset(const CWeakPointer<CDRMBackend>& backend, const POutputBridgeObservation& expected,
                                                                                  const PScanGroupDefinition& group, const CSharedPointer<SOutputMode>& outputMode) noexcept;
+        [[nodiscard]] SOrdinaryKMSGateResult    gateOrdinaryKMS(const CWeakPointer<CDRMBackend>& backend, std::span<const SOrdinaryKMSCRTC> crtcs,
+                                                                const std::function<bool()>& callback) noexcept;
         [[nodiscard]] SGroupedPageFlipResult    routeGroupedPageFlip(const CWeakPointer<CDRMBackend>& backend, uintptr_t transactionId, uint32_t crtcId, uint32_t sequence,
                                                                      uint64_t timestampNs) noexcept;
 
@@ -270,6 +275,8 @@ namespace Aquamarine::NDRM::NTiledDRMIntegration { // NOLINT(readability-identif
                                                                            const std::function<void(const POutputBridgeObservation&)>& callback) noexcept;
         [[nodiscard]] SGroupedTestModesetResult executeCurrentGroupedTestModesetKey(const void* key, const POutputBridgeObservation& expected, const PScanGroupDefinition& group,
                                                                                     const CSharedPointer<SOutputMode>& outputMode) noexcept;
+        [[nodiscard]] SOrdinaryKMSGateResult    gateOrdinaryKMSKey(const void* key, const CWeakPointer<CDRMBackend>& backend, std::span<const SOrdinaryKMSCRTC> crtcs,
+                                                                   const std::function<bool()>& callback) noexcept;
         [[nodiscard]] SGroupedPageFlipResult routeGroupedPageFlipKey(const void* key, uintptr_t transactionId, uint32_t crtcId, uint32_t sequence, uint64_t timestampNs) noexcept;
         void                                 abortConnectorScan(CConnectorScan& scan) noexcept;
 
@@ -313,6 +320,8 @@ namespace Aquamarine::NDRM::NTiledDRMIntegration { // NOLINT(readability-identif
                                                                                              const std::function<void(const CPlan&, const SCurrentState&)>& callback) noexcept;
     [[nodiscard]] AQ_TILED_DRM_PRIVATE bool                      withCurrentOutputBridge(const CWeakPointer<CDRMBackend>& backend, const PAllocationShadow& expected,
                                                                                          const std::function<void(const POutputBridgeObservation&)>& callback) noexcept;
+    [[nodiscard]] AQ_TILED_DRM_PRIVATE SOrdinaryKMSGateResult    gateOrdinaryKMS(const CWeakPointer<CDRMBackend>& backend, std::span<const SOrdinaryKMSCRTC> crtcs,
+                                                                                 const std::function<bool()>& callback) noexcept;
     [[nodiscard]] AQ_TILED_DRM_PRIVATE SGroupedPageFlipResult    routeGroupedPageFlip(const CWeakPointer<CDRMBackend>& backend, uintptr_t transactionId, uint32_t crtcId,
                                                                                       uint32_t sequence, uint64_t timestampNs) noexcept;
     [[nodiscard]] AQ_TILED_DRM_PRIVATE eSessionResult            rotateSession(const CWeakPointer<CDRMBackend>& backend) noexcept;
diff --git a/src/backend/drm/TiledRuntime.cpp b/src/backend/drm/TiledRuntime.cpp
index 4ecb6781f404b2a89b86e53dafff501d900a3ca1..a852b08edac286f4157e67c8a96c3fc28456f3ff 100644
--- a/src/backend/drm/TiledRuntime.cpp
+++ b/src/backend/drm/TiledRuntime.cpp
@@ -119,12 +119,17 @@ eTiledRouteEventResult CTiledFlipReservationRegistry::routeEvent(uintptr_t trans
     }
 
     const auto SLOT = m_crtcSlots.find(crtcId);
-    if (SLOT != m_crtcSlots.end() && (SLOT->second.activeTransaction != 0 || SLOT->second.retiredTransaction == transactionId))
+    if (SLOT != m_crtcSlots.end() && (SLOT->second.activeTransaction == transactionId || SLOT->second.retiredTransaction == transactionId))
         return eTiledRouteEventResult::STALE_TOKEN;
 
     return eTiledRouteEventResult::NOT_OWNED;
 }
 
+bool CTiledFlipReservationRegistry::claimsCrtc(uint32_t crtcId) const noexcept {
+    const auto SLOT = m_crtcSlots.find(crtcId);
+    return crtcId != 0 && SLOT != m_crtcSlots.end() && SLOT->second.activeTransaction != 0;
+}
+
 std::optional<eTiledReservationState> CTiledFlipReservationRegistry::state(const STiledFlipReservation& reservation) const noexcept {
     const auto RECORD = m_transactions.find(reservation.m_transactionId);
     if (RECORD == m_transactions.end() || RECORD->second.reservation != reservation)
diff --git a/src/backend/drm/TiledRuntime.hpp b/src/backend/drm/TiledRuntime.hpp
index 66160dc3aa257c0294f153ed804d4fb19980cb71..b1211943b81a1e63ecac3c79bb1c017b038db3ea 100644
--- a/src/backend/drm/TiledRuntime.hpp
+++ b/src/backend/drm/TiledRuntime.hpp
@@ -169,6 +169,7 @@ namespace Aquamarine::NDRM { // NOLINT(readability-identifier-naming): establish
         [[nodiscard]] bool                                  retire(const STiledFlipReservation& reservation) noexcept;
 
         [[nodiscard]] eTiledRouteEventResult                routeEvent(uintptr_t transactionId, uint32_t crtcId) const noexcept;
+        [[nodiscard]] bool                                  claimsCrtc(uint32_t crtcId) const noexcept;
         [[nodiscard]] std::optional<eTiledReservationState> state(const STiledFlipReservation& reservation) const noexcept;
         [[nodiscard]] size_t                                transactionCount() const noexcept;
         [[nodiscard]] size_t                                claimedCrtcCount() const noexcept;
diff --git a/src/backend/drm/TiledRuntimeAuthority.cpp b/src/backend/drm/TiledRuntimeAuthority.cpp
index 25f14746ab04837e81831a1de021d9d797bf8d99..9e786dabaa72052b16ff22bb302b56c9f054fffa 100644
--- a/src/backend/drm/TiledRuntimeAuthority.cpp
+++ b/src/backend/drm/TiledRuntimeAuthority.cpp
@@ -137,6 +137,10 @@ SGroupedReconcileResult::operator bool() const noexcept {
     return result == eGroupedReconcileResult::RECONCILED || result == eGroupedReconcileResult::RECONCILED_WITH_QUIESCE;
 }
 
+SOrdinaryKMSGateResult::operator bool() const noexcept {
+    return status == eOrdinaryKMSGateStatus::EXECUTED;
+}
+
 SGroupedTestModesetResult::operator bool() const noexcept {
     return status == eGroupedTestModesetStatus::SUCCEEDED;
 }
@@ -614,13 +618,74 @@ SGroupedRetirementResult CGroupedRuntimeAuthority::retire(const POutputDefinitio
     } catch (...) { return {.invariantFailed = true}; }
 }
 
+SOrdinaryKMSGateResult CGroupedRuntimeAuthority::gateOrdinaryKMS(std::span<const SOrdinaryKMSCRTC> crtcs, const std::function<bool()>& callback) noexcept {
+    if (!onEventThread())
+        return {.status = eOrdinaryKMSGateStatus::WRONG_THREAD};
+    if (m_mutationActive)
+        return {.status = eOrdinaryKMSGateStatus::BUSY};
+    if (m_terminal)
+        return {.status = eOrdinaryKMSGateStatus::TERMINAL};
+    if (crtcs.empty() || !callback)
+        return {.status = eOrdinaryKMSGateStatus::INVALID_INPUT};
+
+    for (auto current = crtcs.begin(); current != crtcs.end(); ++current) {
+        const auto& CRTC = *current;
+        if (!CRTC.owner || CRTC.id == 0 || CRTC.owner->id != CRTC.id)
+            return {.status = eOrdinaryKMSGateStatus::INVALID_INPUT};
+        for (auto prior = crtcs.begin(); prior != current; ++prior) {
+            if (sameOwner(CRTC.owner, prior->owner) || CRTC.id == prior->id)
+                return {.status = eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC};
+        }
+    }
+
+    for (const auto& CRTC : crtcs) {
+        size_t liveMatches = 0;
+        for (const auto& [KEY, RECORD] : m_runtimes) {
+            static_cast<void>(KEY);
+            if (!RECORD.runtime)
+                return {.status = eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC};
+            const auto STATE = RECORD.runtime->state();
+            if (STATE == eTiledRuntimeState::STAGED || STATE == eTiledRuntimeState::TEARDOWN)
+                continue;
+            if (STATE != eTiledRuntimeState::PUBLISHED_DISABLED && STATE != eTiledRuntimeState::PUBLISHED_IDLE && STATE != eTiledRuntimeState::PUBLISHED_IN_FLIGHT &&
+                STATE != eTiledRuntimeState::QUIESCED)
+                return {.status = eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC};
+            if (!RECORD.activeRoute)
+                return {.status = eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC};
+
+            for (const auto& MEMBER : RECORD.activeRoute->members()) {
+                if ((sameOwner(MEMBER.crtc, CRTC.owner) && MEMBER.crtcId != CRTC.id) || (MEMBER.crtcId == CRTC.id && !sameOwner(MEMBER.crtc, CRTC.owner)))
+                    return {.status = eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC};
+                if (MEMBER.crtcId == CRTC.id && sameOwner(MEMBER.crtc, CRTC.owner))
+                    ++liveMatches;
+            }
+        }
+        if (liveMatches > 1)
+            return {.status = eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC};
+        if (liveMatches == 1)
+            return {.status = eOrdinaryKMSGateStatus::GROUPED_OWNERSHIP};
+        if (m_reservations.claimsCrtc(CRTC.id))
+            return {.status = eOrdinaryKMSGateStatus::RESERVATION_CLAIMED};
+    }
+
+    m_mutationActive = true;
+    Hyprutils::Utils::CScopeGuard mutationGuard([&] { m_mutationActive = false; });
+    try {
+        return {.status = eOrdinaryKMSGateStatus::EXECUTED, .callbackResult = callback()};
+    } catch (...) { return {.status = eOrdinaryKMSGateStatus::CALLBACK_FAILED}; }
+}
+
+bool CGroupedRuntimeAuthority::claimsPageFlip(uintptr_t transactionId, uint32_t crtcId) const noexcept {
+    return onEventThread() && m_reservations.routeEvent(transactionId, crtcId) != eTiledRouteEventResult::NOT_OWNED;
+}
+
 SGroupedPageFlipResult CGroupedRuntimeAuthority::routePageFlip(uintptr_t transactionId, uint32_t crtcId, uint32_t sequence, uint64_t timestampNs) noexcept {
     if (!onEventThread())
-        return {.claimed = transactionId != 0, .disposition = eGroupedPageFlipDisposition::WRONG_THREAD};
+        return {.disposition = eGroupedPageFlipDisposition::WRONG_THREAD};
     if (m_mutationActive)
-        return {.claimed = transactionId != 0, .disposition = eGroupedPageFlipDisposition::BUSY};
+        return {.claimed = claimsPageFlip(transactionId, crtcId), .disposition = eGroupedPageFlipDisposition::BUSY};
     if (m_terminal)
-        return {.claimed = transactionId != 0, .disposition = eGroupedPageFlipDisposition::TERMINAL};
+        return {.claimed = claimsPageFlip(transactionId, crtcId), .disposition = eGroupedPageFlipDisposition::TERMINAL};
 
     m_mutationActive = true;
     Hyprutils::Utils::CScopeGuard mutationGuard([&] { m_mutationActive = false; });
diff --git a/src/backend/drm/TiledRuntimeAuthority.hpp b/src/backend/drm/TiledRuntimeAuthority.hpp
index ec8945eea276f8faae6d03448a1e8015435e321f..e3efdb46c6516432497bcdc501caeb2f4e55e723 100644
--- a/src/backend/drm/TiledRuntimeAuthority.hpp
+++ b/src/backend/drm/TiledRuntimeAuthority.hpp
@@ -8,6 +8,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <functional>
 #include <map>
 #include <optional>
 #include <span>
@@ -164,6 +165,30 @@ namespace Aquamarine::NDRM::NTiledRuntimeAuthority { // NOLINT(readability-ident
         bool                        reservationRetired = false;
     };
 
+    struct AQ_TILED_RUNTIME_AUTHORITY_PRIVATE SOrdinaryKMSCRTC {
+        CSharedPointer<SDRMCRTC> owner;
+        uint32_t                 id = 0;
+    };
+
+    enum class eOrdinaryKMSGateStatus : uint8_t {
+        EXECUTED = 0,
+        WRONG_THREAD,
+        BUSY,
+        TERMINAL,
+        INVALID_INPUT,
+        AMBIGUOUS_CRTC,
+        GROUPED_OWNERSHIP,
+        RESERVATION_CLAIMED,
+        CALLBACK_FAILED,
+    };
+
+    struct AQ_TILED_RUNTIME_AUTHORITY_PRIVATE SOrdinaryKMSGateResult {
+        eOrdinaryKMSGateStatus status         = eOrdinaryKMSGateStatus::INVALID_INPUT;
+        bool                   callbackResult = false;
+
+        explicit               operator bool() const noexcept;
+    };
+
     enum class eGroupedTestModesetStatus : uint8_t {
         SUCCEEDED = 0,
         WRONG_THREAD,
@@ -240,6 +265,7 @@ namespace Aquamarine::NDRM::NTiledRuntimeAuthority { // NOLINT(readability-ident
 
         [[nodiscard]] eGroupedStageResult      stageCurrent(const POutputBridgeObservation& current, const PResolvedTiledAtomicRoute& route) noexcept;
         [[nodiscard]] SGroupedReconcileResult  reconcileCurrent(const POutputBridgeObservation& current) noexcept;
+        [[nodiscard]] SOrdinaryKMSGateResult   gateOrdinaryKMS(std::span<const SOrdinaryKMSCRTC> crtcs, const std::function<bool()>& callback) noexcept;
         [[nodiscard]] SGroupedPageFlipResult   routePageFlip(uintptr_t transactionId, uint32_t crtcId, uint32_t sequence, uint64_t timestampNs) noexcept;
         [[nodiscard]] SGroupedQuiesceResult    quiesceAll(eTiledQuiesceReason reason) noexcept;
         [[nodiscard]] SGroupedRetirementResult retire(const POutputDefinitionIdentity& identity, eTiledRetirementProof proof) noexcept;
@@ -265,6 +291,7 @@ namespace Aquamarine::NDRM::NTiledRuntimeAuthority { // NOLINT(readability-ident
         [[nodiscard]] eGroupedStageResult      stageRecord(const POutputBridgeObservation& current, const PResolvedTiledAtomicRoute& route);
         [[nodiscard]] bool                     applyReservationEffects(const CGroupedRuntime::SEffects& effects) noexcept;
         [[nodiscard]] bool                     synchronizeRoute(SRuntimeRecord& record) noexcept;
+        [[nodiscard]] bool                     claimsPageFlip(uintptr_t transactionId, uint32_t crtcId) const noexcept;
         [[nodiscard]] SGroupedQuiesceResult    quiesceAllUnguarded(eTiledQuiesceReason reason) noexcept;
         [[nodiscard]] SGroupedRetirementResult retireUnguarded(SRuntimeRecord& record, eTiledRetirementProof proof);
         [[nodiscard]] SRuntimeRecord*          recordFor(const POutputDefinitionIdentity& identity) noexcept;
diff --git a/src/backend/drm/impl/Atomic.cpp b/src/backend/drm/impl/Atomic.cpp
index 7c0aac1be55cd986aec22676b41903292858f516..ffc52ac171c00538f5b9ccc777b1d256cfb6fa8f 100644
--- a/src/backend/drm/impl/Atomic.cpp
+++ b/src/backend/drm/impl/Atomic.cpp
@@ -8,6 +8,7 @@
 #include <sstream>
 #include <optional>
 #include "Shared.hpp"
+#include "../TiledDRMIntegration.hpp"
 #include "aquamarine/output/Output.hpp"
 
 using namespace Aquamarine;
@@ -578,111 +579,142 @@ bool Aquamarine::CDRMAtomicImpl::prepareConnector(Hyprutils::Memory::CSharedPoin
 }
 
 bool Aquamarine::CDRMAtomicImpl::commit(Hyprutils::Memory::CSharedPointer<SDRMConnector> connector, SDRMConnectorCommitData& data) {
-    if (!prepareConnector(connector, data))
-        return false;
+    try {
+        const auto BACKEND = backend.lock();
+        if (!BACKEND || !connector || !connector->crtc)
+            return false;
 
-    CDRMAtomicRequest request(backend);
-
-    request.addConnector(connector, data);
-
-    uint32_t flags = data.flags;
-    if (data.test)
-        flags |= DRM_MODE_ATOMIC_TEST_ONLY;
-    if (data.modeset)
-        flags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
-    if (!data.blocking && !data.test)
-        flags |= DRM_MODE_ATOMIC_NONBLOCK;
-
-    bool ok = request.commit(flags);
-
-    // If the commit failed and max_bpc was actually emitted in this request, retry
-    // without it. Some drivers (notably amdgpu on eDP panels) reject atomic commits
-    // that touch max_bpc. Gate on maxBpcEmitted rather than the staged maxBpc value:
-    // on cached page-flips max_bpc is skipped when unchanged, so an unrelated atomic
-    // failure must not be misattributed to max_bpc and permanently latch maxBpcFailed.
-    if (!ok && data.atomic.maxBpcEmitted && !connector->maxBpcFailed) {
-        connector->backend->backend->log(AQ_LOG_WARNING, "drm: atomic commit failed with max_bpc set, retrying without max_bpc");
-
-        // Re-build the request without max_bpc (addConnector checks maxBpcFailed
-        // and will skip the max_bpc property). The property blobs created in
-        // prepareConnector() are reused as-is, so we must NOT roll back the
-        // original request here or request2 would submit already-destroyed blob
-        // IDs. Blob cleanup is handled by request2.apply()/rollback() below.
-        // maxBpcFailed is latched here so addConnector omits the property, then
-        // cleared on retry failure so a transient error does not permanently
-        // disable max_bpc for this connector.
-        connector->maxBpcFailed   = true;
-        data.atomic.maxBpc        = 0;
-        data.atomic.maxBpcEmitted = false;
-
-        CDRMAtomicRequest request2(backend);
-        request2.addConnector(connector, data);
-
-        ok = request2.commit(flags);
-
-        if (ok) {
-            request2.apply(data);
-            if (!data.test) {
-                connector->atomic.maxBpc      = data.atomic.maxBpc;
-                connector->atomic.colorspace  = data.atomic.colorspace;
-                connector->atomic.contentType = data.atomic.contentType;
-                connector->atomic.crtcID      = data.atomic.crtcID;
-                connector->atomic.propsCached = true;
-                if (data.atomic.ctmd)
-                    connector->crtc->atomic.ctmStateKnown = true;
-
-                if (data.mainFB && data.enabled && (flags & DRM_MODE_PAGE_FLIP_EVENT))
-                    connector->sched.onFrameSubmitted();
+        std::vector<NDRM::NTiledRuntimeAuthority::SOrdinaryKMSCRTC> touchedCRTCs;
+        const auto                                                  ADD_CRTC = [&touchedCRTCs](const SP<SDRMCRTC>& crtc) {
+            if (!crtc || std::ranges::any_of(touchedCRTCs, [&crtc](const auto& candidate) { return candidate.owner.impl_ == crtc.impl_; }))
+                return;
+            touchedCRTCs.push_back({.owner = crtc, .id = crtc->id});
+        };
+        ADD_CRTC(connector->crtc);
+        if (data.modeset) {
+            // The all-head retry may restate any other enabled connector.
+            // Gate that complete exact-owner set before prepareConnector so
+            // every allocation, ioctl, retry, arm, and scheduler effect stays
+            // inside one ordinary/grouped exclusion interval.
+            for (const auto& candidate : BACKEND->connectors) {
+                if (!candidate || !candidate->crtc || !candidate->output || !candidate->output->state->state().enabled)
+                    continue;
+                ADD_CRTC(candidate->crtc);
             }
-        } else {
-            connector->maxBpcFailed = false;
-            request2.rollback(data);
         }
 
-        return ok;
-    }
+        const std::function<bool()> COMMIT = [&]() -> bool {
+            if (!prepareConnector(connector, data))
+                return false;
 
-    // A modeset the driver rejects may still be reachable if every head is
-    // modeset together, since display resources are allocated globally. Retry
-    // once that way before letting the caller fall back to a lesser mode.
-    // See CDRMAtomicRequest::restateConnectors.
-    std::optional<CDRMAtomicRequest> retry;
-    if (!ok && data.modeset) {
-        retry.emplace(backend);
-        retry->addConnector(connector, data);
-        if (retry->restateConnectors(connector)) {
-            ok = retry->commit(flags);
-            if (ok)
-                backend->log(AQ_LOG_DEBUG,
-                             std::format("atomic drm: {} modeset only passed with every head restated; the driver would not "
-                                         "re-allocate for a single connector",
-                                         connector->szName));
-        }
+            CDRMAtomicRequest request(backend);
 
-        if (!ok)
-            retry.reset();
-    }
+            request.addConnector(connector, data);
 
-    // whichever request the kernel accepted owns the blob bookkeeping
-    CDRMAtomicRequest& applied = retry.has_value() ? *retry : request;
+            uint32_t flags = data.flags;
+            if (data.test)
+                flags |= DRM_MODE_ATOMIC_TEST_ONLY;
+            if (data.modeset)
+                flags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
+            if (!data.blocking && !data.test)
+                flags |= DRM_MODE_ATOMIC_NONBLOCK;
 
-    if (ok) {
-        applied.apply(data);
-        if (!data.test) {
-            // remember which connector_state values the kernel last accepted so the
-            // next page-flip can skip emitting unchanged ones (see #265).
-            connector->atomic.maxBpc      = data.atomic.maxBpc;
-            connector->atomic.colorspace  = data.atomic.colorspace;
-            connector->atomic.contentType = data.atomic.contentType;
-            connector->atomic.crtcID      = data.atomic.crtcID;
-            connector->atomic.propsCached = true;
-            if (data.atomic.ctmd)
-                connector->crtc->atomic.ctmStateKnown = true;
-        }
-    } else
-        applied.rollback(data);
+            bool ok = request.commit(flags);
 
-    return ok;
+            // If the commit failed and max_bpc was actually emitted in this request, retry
+            // without it. Some drivers (notably amdgpu on eDP panels) reject atomic commits
+            // that touch max_bpc. Gate on maxBpcEmitted rather than the staged maxBpc value:
+            // on cached page-flips max_bpc is skipped when unchanged, so an unrelated atomic
+            // failure must not be misattributed to max_bpc and permanently latch maxBpcFailed.
+            if (!ok && data.atomic.maxBpcEmitted && !connector->maxBpcFailed) {
+                connector->backend->backend->log(AQ_LOG_WARNING, "drm: atomic commit failed with max_bpc set, retrying without max_bpc");
+
+                // Re-build the request without max_bpc (addConnector checks maxBpcFailed
+                // and will skip the max_bpc property). The property blobs created in
+                // prepareConnector() are reused as-is, so we must NOT roll back the
+                // original request here or request2 would submit already-destroyed blob
+                // IDs. Blob cleanup is handled by request2.apply()/rollback() below.
+                // maxBpcFailed is latched here so addConnector omits the property, then
+                // cleared on retry failure so a transient error does not permanently
+                // disable max_bpc for this connector.
+                connector->maxBpcFailed   = true;
+                data.atomic.maxBpc        = 0;
+                data.atomic.maxBpcEmitted = false;
+
+                CDRMAtomicRequest request2(backend);
+                request2.addConnector(connector, data);
+
+                ok = request2.commit(flags);
+
+                if (ok) {
+                    request2.apply(data);
+                    if (!data.test) {
+                        connector->atomic.maxBpc      = data.atomic.maxBpc;
+                        connector->atomic.colorspace  = data.atomic.colorspace;
+                        connector->atomic.contentType = data.atomic.contentType;
+                        connector->atomic.crtcID      = data.atomic.crtcID;
+                        connector->atomic.propsCached = true;
+                        if (data.atomic.ctmd)
+                            connector->crtc->atomic.ctmStateKnown = true;
+
+                        if (data.mainFB && data.enabled && (flags & DRM_MODE_PAGE_FLIP_EVENT))
+                            connector->sched.onFrameSubmitted();
+                    }
+                } else {
+                    connector->maxBpcFailed = false;
+                    request2.rollback(data);
+                }
+
+                return ok;
+            }
+
+            // A modeset the driver rejects may still be reachable if every head is
+            // modeset together, since display resources are allocated globally. Retry
+            // once that way before letting the caller fall back to a lesser mode.
+            // See CDRMAtomicRequest::restateConnectors.
+            std::optional<CDRMAtomicRequest> retry;
+            if (!ok && data.modeset) {
+                retry.emplace(backend);
+                retry->addConnector(connector, data);
+                if (retry->restateConnectors(connector)) {
+                    ok = retry->commit(flags);
+                    if (ok)
+                        backend->log(AQ_LOG_DEBUG,
+                                     std::format("atomic drm: {} modeset only passed with every head restated; the driver would not "
+                                                 "re-allocate for a single connector",
+                                                 connector->szName));
+                }
+
+                if (!ok)
+                    retry.reset();
+            }
+
+            // whichever request the kernel accepted owns the blob bookkeeping
+            CDRMAtomicRequest& applied = retry.has_value() ? *retry : request;
+
+            if (ok) {
+                applied.apply(data);
+                if (!data.test) {
+                    // remember which connector_state values the kernel last accepted so the
+                    // next page-flip can skip emitting unchanged ones (see #265).
+                    connector->atomic.maxBpc      = data.atomic.maxBpc;
+                    connector->atomic.colorspace  = data.atomic.colorspace;
+                    connector->atomic.contentType = data.atomic.contentType;
+                    connector->atomic.crtcID      = data.atomic.crtcID;
+                    connector->atomic.propsCached = true;
+                    if (data.atomic.ctmd)
+                        connector->crtc->atomic.ctmStateKnown = true;
+                }
+            } else {
+                applied.rollback(data);
+            }
+
+            return ok;
+        };
+
+        const auto GATED = NDRM::NTiledDRMIntegration::gateOrdinaryKMS(backend, touchedCRTCs, COMMIT);
+        return static_cast<bool>(GATED) && GATED.callbackResult;
+    } catch (...) { return false; }
 }
 
 bool Aquamarine::CDRMAtomicImpl::reset() {
diff --git a/src/backend/drm/impl/Legacy.cpp b/src/backend/drm/impl/Legacy.cpp
index 65658fcdd8cb4196cb70d84c12711471a2f7dd4d..748d0e8e8276fcd7c934e9adfc96515cc53b24ef 100644
--- a/src/backend/drm/impl/Legacy.cpp
+++ b/src/backend/drm/impl/Legacy.cpp
@@ -1,5 +1,7 @@
 #include "aquamarine/output/Output.hpp"
+#include "../TiledDRMIntegration.hpp"
 #include <aquamarine/backend/drm/Legacy.hpp>
+#include <array>
 #include <cstring>
 #include <xf86drm.h>
 #include <xf86drmMode.h>
@@ -157,7 +159,15 @@ bool Aquamarine::CDRMLegacyImpl::commit(Hyprutils::Memory::CSharedPointer<SDRMCo
     if (!testInternal(connector, data))
         return false;
 
-    return commitInternal(connector, data);
+    try {
+        if (!connector || !connector->crtc)
+            return false;
+
+        const std::array<NDRM::NTiledRuntimeAuthority::SOrdinaryKMSCRTC, 1> TOUCHED_CRTCS = {{{.owner = connector->crtc, .id = connector->crtc->id}}};
+        const std::function<bool()>                                         COMMIT        = [&]() { return commitInternal(connector, data); };
+        const auto                                                          GATED         = NDRM::NTiledDRMIntegration::gateOrdinaryKMS(backend, TOUCHED_CRTCS, COMMIT);
+        return static_cast<bool>(GATED) && GATED.callbackResult;
+    } catch (...) { return false; }
 }
 
 bool Aquamarine::CDRMLegacyImpl::reset() {
diff --git a/tests/TiledAtomic.cpp b/tests/TiledAtomic.cpp
index 8a5565e49e164903fc7c29da07f0d9d9817cd7b8..63858f638d7d89ba14e3527f16fd8b67fc0a8d8b 100644
--- a/tests/TiledAtomic.cpp
+++ b/tests/TiledAtomic.cpp
@@ -2,6 +2,7 @@
 #include "backend/drm/TiledRuntimeAuthority.hpp"
 
 #include <algorithm>
+#include <array>
 #include <cerrno>
 #include <cstdint>
 #include <iostream>
@@ -186,6 +187,21 @@ namespace Aquamarine::NDRM::NTiledRuntimeAuthorityTest {
             authority.m_mutationActive = false;
             return RESULT;
         }
+
+        static STiledFlipReservationResult reserve(CGroupedRuntimeAuthority& authority, const STiledFlipRoute& route) {
+            return authority.m_reservations.reserveAll(route);
+        }
+
+        static bool rollback(CGroupedRuntimeAuthority& authority, const STiledFlipReservation& reservation) {
+            return authority.m_reservations.rollback(reservation);
+        }
+
+        static SGroupedPageFlipResult routeWhileBusy(CGroupedRuntimeAuthority& authority, uintptr_t transactionId, uint32_t crtcId) {
+            authority.m_mutationActive = true;
+            const auto RESULT          = authority.routePageFlip(transactionId, crtcId, 1, 1);
+            authority.m_mutationActive = false;
+            return RESULT;
+        }
     };
 }
 
@@ -912,6 +928,42 @@ int main() {
         CHECK(teardown(runtime, registry));
     }
 
+    // A live singular pending flip on any exact grouped member is rejected
+    // before reserving a grouped transaction.
+    {
+        SFrameRuntime runtime(resolved.route->runtimePlan<PFrameOwner>());
+        const auto    oldBuffer = frameOwner(350);
+        CHECK(runtime.publishIdle(currentFrames(resolved.route, 850, 750, oldBuffer)));
+        CTiledFlipReservationRegistry               registry;
+        auto                                        frameRecorder          = makeShared<SFrameRecorder>();
+        CSharedPointer<ITiledAtomicFrameOperations> frameOperations        = frameRecorder;
+        auto                                        blockedFramebuffer     = frameOwner(351);
+        auto                                        blockedBuffer          = frameOwner(352);
+        const std::weak_ptr<SFrameOwner>            blockedFramebufferWeak = blockedFramebuffer;
+        const std::weak_ptr<SFrameOwner>            blockedBufferWeak      = blockedBuffer;
+        auto                                        blockedFrame = runtimeFrame(resolved.route->framebuffer().id, 751, std::move(blockedFramebuffer), std::move(blockedBuffer));
+
+        auto&                                       PENDING = resolved.route->members().front().crtc->pendingFlip.id;
+        PENDING                                             = 3'002;
+        const auto transaction = submitTiledFrameTransaction(resolved.route, runtime, registry, std::move(blockedFrame), 3'001, 65'000, 21, frameOperations);
+        PENDING.reset();
+
+        CHECK(!static_cast<bool>(transaction));
+        CHECK(transaction.status == eTiledFrameTransactionStatus::MEMBER_PENDING_FLIP);
+        CHECK(transaction.effects.requestBuilt);
+        CHECK(transaction.effects.runtimePrepared);
+        CHECK(!transaction.effects.reservationReserved);
+        CHECK(!transaction.effects.submitInvoked);
+        CHECK(transaction.effects.runtimeRolledBack);
+        CHECK(registry.transactionCount() == 0);
+        CHECK(registry.claimedCrtcCount() == 0);
+        CHECK(frameRecorder->submits == 0);
+        CHECK(frameRecorder->frees == 1);
+        CHECK(blockedFramebufferWeak.expired());
+        CHECK(blockedBufferWeak.expired());
+        CHECK(teardown(runtime, registry));
+    }
+
     // 3. A conflicting member reservation prevents submit and leaves the
     // existing all-member reservation unchanged.
     {
@@ -1021,7 +1073,89 @@ int main() {
         CGroupedRuntimeAuthority authority;
         CHECK(authority.stageCurrent(installed, resolved.route) == eGroupedStageResult::STAGED);
         CHECK(CFixtureAuthority::nestedStage(authority, installed, resolved.route) == eGroupedStageResult::BUSY);
+
+        std::vector<SOrdinaryKMSCRTC> ordinaryCRTCs;
+        ordinaryCRTCs.reserve(resolved.route->members().size());
+        for (const auto& MEMBER : resolved.route->members())
+            ordinaryCRTCs.push_back({.owner = MEMBER.crtc, .id = MEMBER.crtcId});
+
+        size_t     callbackCount  = 0;
+        const auto CALLBACK_FALSE = authority.gateOrdinaryKMS(ordinaryCRTCs, [&] {
+            ++callbackCount;
+            return false;
+        });
+        CHECK(CALLBACK_FALSE.status == eOrdinaryKMSGateStatus::EXECUTED);
+        CHECK(!CALLBACK_FALSE.callbackResult);
+        CHECK(callbackCount == 1);
+
+        const auto CALLBACK_THROW = authority.gateOrdinaryKMS(ordinaryCRTCs, []() -> bool { throw 1; });
+        CHECK(CALLBACK_THROW.status == eOrdinaryKMSGateStatus::CALLBACK_FAILED);
+
+        const std::array DUPLICATE_CRTCS{ordinaryCRTCs.front(), ordinaryCRTCs.front()};
+        const auto       AMBIGUOUS = authority.gateOrdinaryKMS(DUPLICATE_CRTCS, [&] {
+            ++callbackCount;
+            return true;
+        });
+        CHECK(AMBIGUOUS.status == eOrdinaryKMSGateStatus::AMBIGUOUS_CRTC);
+        CHECK(callbackCount == 1);
+
+        eOrdinaryKMSGateStatus nestedStatus  = eOrdinaryKMSGateStatus::INVALID_INPUT;
+        const auto             CALLBACK_TRUE = authority.gateOrdinaryKMS(ordinaryCRTCs, [&] {
+            const auto NESTED = authority.gateOrdinaryKMS(ordinaryCRTCs, [&] {
+                ++callbackCount;
+                return true;
+            });
+            nestedStatus      = NESTED.status;
+            return true;
+        });
+        CHECK(CALLBACK_TRUE.status == eOrdinaryKMSGateStatus::EXECUTED);
+        CHECK(CALLBACK_TRUE.callbackResult);
+        CHECK(nestedStatus == eOrdinaryKMSGateStatus::BUSY);
+        CHECK(callbackCount == 1);
+
+        const STiledFlipRoute ordinaryBlocker{
+            .transactionId = 5'000,
+            .generation    = resolved.route->generation(),
+            .crtcIds       = {resolved.route->crtcIds().begin(), resolved.route->crtcIds().end()},
+        };
+        const auto BLOCKER = CFixtureAuthority::reserve(authority, ordinaryBlocker);
+        CHECK(static_cast<bool>(BLOCKER));
+        const auto RESERVED_GATE = authority.gateOrdinaryKMS(ordinaryCRTCs, [&] {
+            ++callbackCount;
+            return true;
+        });
+        CHECK(RESERVED_GATE.status == eOrdinaryKMSGateStatus::RESERVATION_CLAIMED);
+        CHECK(callbackCount == 1);
+
+        const auto ORDINARY_WHILE_BUSY = CFixtureAuthority::routeWhileBusy(authority, 55'555, ordinaryCRTCs.front().id);
+        CHECK(!ORDINARY_WHILE_BUSY.claimed);
+        CHECK(ORDINARY_WHILE_BUSY.disposition == eGroupedPageFlipDisposition::BUSY);
+        const auto GROUPED_WHILE_BUSY = CFixtureAuthority::routeWhileBusy(authority, ordinaryBlocker.transactionId, ordinaryCRTCs.front().id);
+        CHECK(GROUPED_WHILE_BUSY.claimed);
+        CHECK(GROUPED_WHILE_BUSY.disposition == eGroupedPageFlipDisposition::BUSY);
+        CHECK(CFixtureAuthority::rollback(authority, BLOCKER.reservation));
+
+        eOrdinaryKMSGateStatus wrongThreadStatus  = eOrdinaryKMSGateStatus::EXECUTED;
+        bool                   wrongThreadInvoked = false;
+        std::thread            wrongThread([&] {
+            const auto WRONG  = authority.gateOrdinaryKMS(ordinaryCRTCs, [&] {
+                wrongThreadInvoked = true;
+                return true;
+            });
+            wrongThreadStatus = WRONG.status;
+        });
+        wrongThread.join();
+        CHECK(wrongThreadStatus == eOrdinaryKMSGateStatus::WRONG_THREAD);
+        CHECK(!wrongThreadInvoked);
+        CHECK(callbackCount == 1);
+
         CHECK(CFixtureAuthority::publishIdle(authority, GROUP.definitionIdentity(), 1'500, 1'100));
+        const auto LIVE_GATE = authority.gateOrdinaryKMS(ordinaryCRTCs, [&] {
+            ++callbackCount;
+            return true;
+        });
+        CHECK(LIVE_GATE.status == eOrdinaryKMSGateStatus::GROUPED_OWNERSHIP);
+        CHECK(callbackCount == 1);
         CHECK(CFixtureAuthority::accept(authority, GROUP.definitionIdentity(), 5'005, 1'101, 90'000));
 
         CHECK(bridge.clear());
@@ -1084,7 +1218,22 @@ int main() {
         const auto QUIESCED = authority.quiesceAll(eTiledQuiesceReason::BACKEND_TEARDOWN);
         CHECK(!QUIESCED.invariantFailed);
         CHECK(QUIESCED.quiesced == 1);
+        const auto QUIESCED_GATE = authority.gateOrdinaryKMS(ordinaryCRTCs, [&] {
+            ++callbackCount;
+            return true;
+        });
+        CHECK(QUIESCED_GATE.status == eOrdinaryKMSGateStatus::GROUPED_OWNERSHIP);
+        CHECK(callbackCount == 1);
         CHECK(authority.terminalShutdown(eTiledRegistryShutdownProof::BACKEND_NO_CALLBACKS, eTiledRetirementProof::BACKEND_TEARDOWN_NO_CALLBACKS));
+        const auto TERMINAL_GATE = authority.gateOrdinaryKMS(ordinaryCRTCs, [&] {
+            ++callbackCount;
+            return true;
+        });
+        CHECK(TERMINAL_GATE.status == eOrdinaryKMSGateStatus::TERMINAL);
+        CHECK(callbackCount == 1);
+        const auto ORDINARY_AFTER_TERMINAL = authority.routePageFlip(66'666, ordinaryCRTCs.front().id, 1, 1);
+        CHECK(!ORDINARY_AFTER_TERMINAL.claimed);
+        CHECK(ORDINARY_AFTER_TERMINAL.disposition == eGroupedPageFlipDisposition::TERMINAL);
         CHECK(authority.readyForDestruction());
     }
 
diff --git a/tests/TiledDRMIntegration.cpp b/tests/TiledDRMIntegration.cpp
index d32e1b0e225b0c0554246e7e0a616848e011f615..cdc6682eedd06f09ea75846e7d517bd4867d1d38 100644
--- a/tests/TiledDRMIntegration.cpp
+++ b/tests/TiledDRMIntegration.cpp
@@ -84,6 +84,10 @@ namespace Aquamarine::NDRM::NTiledDRMIntegrationTest {
             return registry.withCurrentTestOutputBridge(lifetime, expected, callback);
         }
 
+        static SGroupedPageFlipResult routePageFlip(CTiledDRMIntegrationRegistry& registry, const PBackendLifetime& lifetime, uintptr_t transactionId, uint32_t crtcId) {
+            return registry.routeGroupedPageFlipKey(lifetime.impl_, transactionId, crtcId, 1, 1);
+        }
+
         static bool configureTestModeset(CTiledDRMIntegrationRegistry& registry, const PBackendLifetime& lifetime, int drmFD,
                                          const CSharedPointer<ITiledAtomicRequestOperations>& operations, const CSharedPointer<IGroupedTestFramebufferSource>& framebufferSource) {
             return registry.configureInjectedTestModeset(lifetime, drmFD, operations, framebufferSource);
@@ -547,10 +551,11 @@ int main() {
         bool                      nestedCurrent = true;
         eAllocationCaptureResult  nestedCapture = eAllocationCaptureResult::INSTALLED;
         SBeginConnectorScanResult nestedBegin;
-        eSessionResult            nestedRotate = eSessionResult::ROTATED;
-        eDetachResult             nestedDetach = eDetachResult::BEGUN;
+        eSessionResult            nestedRotate   = eSessionResult::ROTATED;
+        eDetachResult             nestedDetach   = eDetachResult::BEGUN;
         eDetachResult             nestedTerminal = eDetachResult::TERMINALIZED;
-        eDetachResult             nestedErase  = eDetachResult::DETACHED;
+        eDetachResult             nestedErase    = eDetachResult::DETACHED;
+        SGroupedPageFlipResult    nestedOrdinaryPageFlip;
         EXPECT(CFixtureAuthority::withCurrent(registry, LIFETIME, SHADOW,
                                               [&](const CPlan&, const SCurrentState& current) {
                                                   ++callbacks;
@@ -562,7 +567,8 @@ int main() {
                                                   nestedDetach  = CFixtureAuthority::beginDetach(registry, LIFETIME, eDetachReason::BACKEND_SHUTDOWN);
                                                   nestedTerminal =
                                                       CFixtureAuthority::terminalize(registry, LIFETIME, eDetachTerminalProof::BACKEND_OBJECT_DESTRUCTING_NO_CALLBACKS);
-                                                  nestedErase   = CFixtureAuthority::erase(registry, LIFETIME);
+                                                  nestedErase            = CFixtureAuthority::erase(registry, LIFETIME);
+                                                  nestedOrdinaryPageFlip = CFixtureAuthority::routePageFlip(registry, LIFETIME, 77'777, 101);
                                               }),
                true);
         EXPECT(callbacks, 1U);
@@ -575,6 +581,8 @@ int main() {
         EXPECT(value(nestedDetach), value(eDetachResult::BUSY));
         EXPECT(value(nestedTerminal), value(eDetachResult::BUSY));
         EXPECT(value(nestedErase), value(eDetachResult::BUSY));
+        EXPECT(nestedOrdinaryPageFlip.claimed, false);
+        EXPECT(nestedOrdinaryPageFlip.disposition == eGroupedPageFlipDisposition::BUSY, true);
         EXPECT(CFixtureAuthority::allocation(registry, LIFETIME) == SHADOW, true);
         EXPECT(CFixtureAuthority::lastComplete(registry, LIFETIME), true);
 
diff --git a/tests/TiledRuntime.cpp b/tests/TiledRuntime.cpp
index 83b716feb79d6f34178e24e5b31a203df91bc117..8e437be3584ce598047ce814607586ac3d95264c 100644
--- a/tests/TiledRuntime.cpp
+++ b/tests/TiledRuntime.cpp
@@ -188,7 +188,7 @@ int main() {
         EXPECT(registry.accept(accepted.reservation), false);
         EXPECT(enumValue(registry.state(accepted.reservation).value_or(eTiledReservationState::RESERVED)), enumValue(eTiledReservationState::IN_FLIGHT));
         EXPECT(enumValue(registry.routeEvent(100, 22)), enumValue(eTiledRouteEventResult::ROUTABLE));
-        EXPECT(enumValue(registry.routeEvent(999, 22)), enumValue(eTiledRouteEventResult::STALE_TOKEN));
+        EXPECT(enumValue(registry.routeEvent(999, 22)), enumValue(eTiledRouteEventResult::NOT_OWNED));
         EXPECT(registry.quiesce(accepted.reservation), true);
         EXPECT(registry.quiesce(accepted.reservation), true);
         EXPECT(enumValue(registry.state(accepted.reservation).value_or(eTiledReservationState::RESERVED)), enumValue(eTiledReservationState::QUIESCED));