Subject: [PATCH 24/24] drm: stop clearing MODE_ID on grouped tiled DPMS disable

From: Justin Youens <[email-masked]>
Date: Tue, 18 Aug 2026 22:24:03 -0500

Every live disable on the UltraFine was rejected, so Hyprland only
faded to black and left the backlight on. Match stock Aquamarine
DPMS-off first (ACTIVE=0, detach planes, leave MODE_ID), then fall
back through DPMS-only and the old MODE_ID=0 request. Log the ioctl
status when it still fails.
---
 src/backend/drm/DRM.cpp                   |  5 +-
 src/backend/drm/TiledDRMIntegration.cpp   | 10 ++-
 src/backend/drm/TiledDRMIntegration.hpp   | 13 ++--
 src/backend/drm/TiledRuntimeAuthority.cpp | 87 ++++++++++++++++-------
 src/backend/drm/TiledRuntimeAuthority.hpp |  8 ++-
 tests/TiledGroupedStartupCtm.cpp          |  8 +++
 6 files changed, 94 insertions(+), 37 deletions(-)

diff --git a/src/backend/drm/DRM.cpp b/src/backend/drm/DRM.cpp
index fc50175e06a34c81cb51def517679e4f0cc43e4f..64dfc97b54ff0714b0d02dceac5c431dcd336ce4 100644
--- a/src/backend/drm/DRM.cpp
+++ b/src/backend/drm/DRM.cpp
@@ -3228,7 +3228,10 @@ bool Aquamarine::CDRMOutput::commitState(bool onlyTest) {
                 const auto RESULT = NDRM::NTiledDRMIntegration::commitGroupedStartup(
                     BACKEND, connector, MODE, {}, STATE.buffer && BUFFER_COMMITTED ? STATE.buffer : CSharedPointer<IBuffer>{}, onlyTest, false, 0, 0, {}, CTM);
                 if (!RESULT.claimed || !RESULT.success) {
-                    BACKEND->log(AQ_LOG_ERROR, std::format("drm: AQ_DRM_TILED_STARTUP=1 disable {} rejected claimed={} success={}", name, RESULT.claimed, RESULT.success));
+                    BACKEND->log(AQ_LOG_ERROR,
+                                 std::format("drm: AQ_DRM_TILED_STARTUP=1 disable {} rejected claimed={} success={} busy={} attempted={} filled={} submitted={} submit={}", name,
+                                             RESULT.claimed, RESULT.success, RESULT.disableBusy, RESULT.disableAttempted, RESULT.disableFilled, RESULT.disableSubmitted,
+                                             RESULT.disableSubmitStatus));
                     return false;
                 }
                 if (onlyTest)
diff --git a/src/backend/drm/TiledDRMIntegration.cpp b/src/backend/drm/TiledDRMIntegration.cpp
index 273df157f70f8defd5a87af3f2ae24f60fc6e649..7e760ce17a658f3a07e0875a0a1c68c95b728429 100644
--- a/src/backend/drm/TiledDRMIntegration.cpp
+++ b/src/backend/drm/TiledDRMIntegration.cpp
@@ -1729,9 +1729,15 @@ SGroupedStartupCommitResult CTiledDRMIntegrationRegistry::commitGroupedStartupKe
     };
 
     if (!enabled) {
-        if (STARTUP.members.empty() || m_impl->mutationBusy(ENTRY->second) || !ENTRY->second.groupedRuntime)
+        if (STARTUP.members.empty() || m_impl->mutationBusy(ENTRY->second) || !ENTRY->second.groupedRuntime) {
+            result.disableBusy = m_impl->mutationBusy(ENTRY->second);
             return result;
-        const auto DISABLE = ENTRY->second.groupedRuntime->submitDisable(STARTUP.members, onlyTest);
+        }
+        const auto DISABLE         = ENTRY->second.groupedRuntime->submitDisable(STARTUP.members, onlyTest);
+        result.disableAttempted    = DISABLE.attempted;
+        result.disableSubmitted    = DISABLE.submitted;
+        result.disableFilled       = DISABLE.filled;
+        result.disableSubmitStatus = DISABLE.submitStatus;
         if (DISABLE.attempted && !DISABLE.success)
             return result;
         if (onlyTest) {
diff --git a/src/backend/drm/TiledDRMIntegration.hpp b/src/backend/drm/TiledDRMIntegration.hpp
index a5b43f54efadecaa3f893a7852d449e59e236547..64ef7baa7c0b0dad9b6917adeb7cb4ece63c7f08 100644
--- a/src/backend/drm/TiledDRMIntegration.hpp
+++ b/src/backend/drm/TiledDRMIntegration.hpp
@@ -100,10 +100,15 @@ namespace Aquamarine::NDRM::NTiledDRMIntegration { // NOLINT(readability-identif
     };
 
     struct AQ_TILED_DRM_PRIVATE SGroupedStartupCommitResult {
-        bool                       claimed         = false;
-        bool                       success         = false;
-        bool                       initial         = false;
-        bool                       terminalFailure = false;
+        bool                       claimed             = false;
+        bool                       success             = false;
+        bool                       initial             = false;
+        bool                       terminalFailure     = false;
+        bool                       disableBusy         = false;
+        bool                       disableAttempted    = false;
+        bool                       disableSubmitted    = false;
+        bool                       disableFilled       = false;
+        int                        disableSubmitStatus = 0;
         SGroupedTestModesetResult  test;
         SGroupedInitialFrameResult initialFrame;
         SGroupedFrameResult        frame;
diff --git a/src/backend/drm/TiledRuntimeAuthority.cpp b/src/backend/drm/TiledRuntimeAuthority.cpp
index e1c0342665a9ed954ed926a8b7720a0edb315471..c878e397e6d95aba61093cb1964201a4cd659723 100644
--- a/src/backend/drm/TiledRuntimeAuthority.cpp
+++ b/src/backend/drm/TiledRuntimeAuthority.cpp
@@ -1294,40 +1294,73 @@ namespace {
         uint32_t flags = DRM_MODE_ATOMIC_ALLOW_MODESET;
         if (onlyTest)
             flags |= DRM_MODE_ATOMIC_TEST_ONLY;
-        if (fill(operations, request) && operations->submit(drmFD, request, flags, nullptr) == 0) {
-            result.submitted = true;
-            result.success   = true;
+        result.filled = fill(operations, request);
+        if (result.filled) {
+            result.submitStatus = operations->submit(drmFD, request, flags, nullptr);
+            if (result.submitStatus == 0) {
+                result.submitted = true;
+                result.success   = true;
+            }
         }
         operations->freeRequest(request);
         return result;
     }
+
+    bool addGroupedDisableProperty(const CSharedPointer<ITiledAtomicRequestOperations>& operations, drmModeAtomicReqPtr request, uint32_t objectId, uint32_t propertyId,
+                                   uint64_t value) noexcept {
+        return propertyId == 0 || operations->addProperty(request, objectId, propertyId, value) >= 0;
+    }
+
+    bool disableGroupedPlane(const CSharedPointer<ITiledAtomicRequestOperations>& operations, drmModeAtomicReqPtr request, const CSharedPointer<SDRMPlane>& plane,
+                             std::set<uint32_t>& seen) noexcept {
+        if (!plane || plane->id == 0 || !seen.insert(plane->id).second)
+            return true;
+        return addGroupedDisableProperty(operations, request, plane->id, plane->props.values.fb_id, 0) &&
+            addGroupedDisableProperty(operations, request, plane->id, plane->props.values.crtc_id, 0) &&
+            addGroupedDisableProperty(operations, request, plane->id, plane->props.values.crtc_x, 0) &&
+            addGroupedDisableProperty(operations, request, plane->id, plane->props.values.crtc_y, 0);
+    }
+
+    bool fillGroupedDisableRequest(const CSharedPointer<ITiledAtomicRequestOperations>& operations, drmModeAtomicReqPtr request,
+                                   std::span<const CSharedPointer<SDRMConnector>> members, bool detachConnectors, bool clearMode, bool setDpmsOff) noexcept {
+        bool               ok = true;
+        std::set<uint32_t> seenPlanes;
+        for (const auto& MEMBER : members) {
+            if (detachConnectors)
+                ok = ok && addGroupedDisableProperty(operations, request, MEMBER->id, MEMBER->props.values.crtc_id, 0);
+            if (setDpmsOff)
+                ok = ok && addGroupedDisableProperty(operations, request, MEMBER->id, MEMBER->props.values.dpms, DRM_MODE_DPMS_OFF);
+            if (!MEMBER->crtc)
+                continue;
+            ok = ok && disableGroupedPlane(operations, request, MEMBER->crtc->primary, seenPlanes);
+            ok = ok && disableGroupedPlane(operations, request, MEMBER->crtc->cursor, seenPlanes);
+            if (clearMode)
+                ok = ok && addGroupedDisableProperty(operations, request, MEMBER->crtc->id, MEMBER->crtc->props.values.mode_id, 0);
+            ok = ok && addGroupedDisableProperty(operations, request, MEMBER->crtc->id, MEMBER->crtc->props.values.active, 0);
+            ok = ok && addGroupedDisableProperty(operations, request, MEMBER->crtc->id, MEMBER->crtc->props.values.vrr_enabled, 0);
+        }
+        return ok;
+    }
 }
 
 CGroupedRuntimeAuthority::SGroupedDisableResult CGroupedRuntimeAuthority::submitDisable(std::span<const CSharedPointer<SDRMConnector>> members, bool onlyTest) noexcept {
-    return submitGroupedMemberProperties(onEventThread(), m_mutationActive, m_terminal, m_testOperations, m_testDrmFD, members, onlyTest,
-                                         [&](const CSharedPointer<ITiledAtomicRequestOperations>& operations, drmModeAtomicReqPtr request) {
-                                             const auto add = [&](uint32_t objectId, uint32_t propertyId, uint64_t value) {
-                                                 return propertyId == 0 || operations->addProperty(request, objectId, propertyId, value) >= 0;
-                                             };
-                                             const auto disablePlane = [&](const CSharedPointer<SDRMPlane>& plane, std::set<uint32_t>& seen) {
-                                                 if (!plane || plane->id == 0 || !seen.insert(plane->id).second)
-                                                     return true;
-                                                 return add(plane->id, plane->props.values.fb_id, 0) && add(plane->id, plane->props.values.crtc_id, 0);
-                                             };
-                                             bool               ok = true;
-                                             std::set<uint32_t> seenPlanes;
-                                             for (const auto& MEMBER : members) {
-                                                 ok = ok && add(MEMBER->id, MEMBER->props.values.crtc_id, 0);
-                                                 ok = ok && add(MEMBER->id, MEMBER->props.values.dpms, DRM_MODE_DPMS_OFF);
-                                                 if (!MEMBER->crtc)
-                                                     continue;
-                                                 ok = ok && disablePlane(MEMBER->crtc->primary, seenPlanes);
-                                                 ok = ok && disablePlane(MEMBER->crtc->cursor, seenPlanes);
-                                                 ok = ok && add(MEMBER->crtc->id, MEMBER->crtc->props.values.mode_id, 0);
-                                                 ok = ok && add(MEMBER->crtc->id, MEMBER->crtc->props.values.active, 0);
-                                             }
-                                             return ok;
-                                         });
+    // Stock Aquamarine DPMS-off does not clear MODE_ID. The previous grouped
+    // request always did, and every live disable on this panel was rejected, so
+    // Hyprland's fade-to-black was all the user ever saw. Try the accepted
+    // disable shapes in order.
+    const bool            detachConnectors[] = {true, true, false, true};
+    const bool            clearMode[]        = {false, false, false, true};
+    const bool            setDpmsOff[]       = {true, false, true, true};
+    SGroupedDisableResult last;
+    for (size_t i = 0; i < 4; ++i) {
+        last = submitGroupedMemberProperties(onEventThread(), m_mutationActive, m_terminal, m_testOperations, m_testDrmFD, members, onlyTest,
+                                             [&](const CSharedPointer<ITiledAtomicRequestOperations>& operations, drmModeAtomicReqPtr request) {
+                                                 return fillGroupedDisableRequest(operations, request, members, detachConnectors[i], clearMode[i], setDpmsOff[i]);
+                                             });
+        if (last.success)
+            return last;
+    }
+    return last;
 }
 
 CGroupedRuntimeAuthority::SGroupedDisableResult CGroupedRuntimeAuthority::submitRestore(std::span<const CSharedPointer<SDRMConnector>> members, bool onlyTest) noexcept {
diff --git a/src/backend/drm/TiledRuntimeAuthority.hpp b/src/backend/drm/TiledRuntimeAuthority.hpp
index c21603998464a3852576e0d5ba95aeef0aca2182..59ac32092cb22623f9823f00e26e808f2d772e78 100644
--- a/src/backend/drm/TiledRuntimeAuthority.hpp
+++ b/src/backend/drm/TiledRuntimeAuthority.hpp
@@ -479,9 +479,11 @@ namespace Aquamarine::NDRM::NTiledRuntimeAuthority { // NOLINT(readability-ident
         [[nodiscard]] SGroupedRetirementResult   retire(const POutputDefinitionIdentity& identity, eTiledRetirementProof proof) noexcept;
 
         struct SGroupedDisableResult {
-            bool attempted = false;
-            bool submitted = false;
-            bool success   = false;
+            bool attempted    = false;
+            bool submitted    = false;
+            bool success      = false;
+            bool filled       = false;
+            int  submitStatus = 0;
         };
 
         struct SGroupedCtmResult {
diff --git a/tests/TiledGroupedStartupCtm.cpp b/tests/TiledGroupedStartupCtm.cpp
index 52dfe2796f55315de388be9cbdde3b21c928fe85..e6924e63894e2088a7cc1a0238b2b192caeb1e47 100644
--- a/tests/TiledGroupedStartupCtm.cpp
+++ b/tests/TiledGroupedStartupCtm.cpp
@@ -325,6 +325,14 @@ int main() {
         EXPECT(powerOff.size() == 2 && !powerOff[0].enabled && !powerOff[1].enabled, true);
         EXPECT((operations->submittedFlags & DRM_MODE_ATOMIC_ALLOW_MODESET) != 0, true);
         EXPECT((operations->submittedFlags & DRM_MODE_ATOMIC_TEST_ONLY) == 0, true);
+        EXPECT(countWrites(operations->submitWrites, 10, 4001), 0U);
+        EXPECT(countWrites(operations->submitWrites, 11, 4011), 0U);
+        EXPECT(writeValue(operations->submitWrites, 10, 4002), 0ULL);
+        EXPECT(writeValue(operations->submitWrites, 11, 4012), 0ULL);
+        EXPECT(writeValue(operations->submitWrites, 101, 1002), static_cast<uint64_t>(DRM_MODE_DPMS_OFF));
+        EXPECT(writeValue(operations->submitWrites, 202, 1012), static_cast<uint64_t>(DRM_MODE_DPMS_OFF));
+        EXPECT(writeValue(operations->submitWrites, 101, 1000), 0ULL);
+        EXPECT(writeValue(operations->submitWrites, 202, 1010), 0ULL);
 
         auto restore = CFixtureAuthority::commit(registry, LIFETIME, NO_BACKEND, left, false, true);
         EXPECT(restore.claimed, true);