drm: CRTC starvation recovery + clear stale page-flip state after suspend (#254)

* drm: handle CRTC starvation recovery when outputs are disabled

When more displays are connected than CRTCs available, connectors that
arrive after all CRTCs are claimed become starved.  The compositor can
free a CRTC by disabling an output, but nothing reclaimed that CRTC for
the starved connector.

Restructure recheckCRTCs() into two passes: first, disabled outputs
release their CRTCs and enabled connectors get priority assignment;
second, any remaining free CRTCs are given to disabled connectors as
backup slots for quick re-enable.

When applyCommit() detects an enabledState transition, schedule
recheckOutputs() via addIdleEvent so starved connectors pick up the
freed CRTC on the next event loop iteration, without reentrancy or
blocking the event loop.

* drm: clear stale page-flip state after suspend/resume

My laptop (i915, 3 external monitors) would go permanently black after
S3 or suspend-then-hibernate. The only recovery was a hard restart.

The root cause: when display hardware powers off during suspend, any
in-flight page-flip completion event is lost. handlePF() never fires,
so isPageFlipPending stays true from the last frame before suspend. On
resume, scheduleFrame() sees the stale flag and bails, commitState()
rejects every frame with "Cannot commit when a page-flip is awaiting",
and nothing ever clears it. Screens stay dark forever.

VT switch doesn't hit this because the kernel preserves DRM state and
queues pending events in the fd buffer. Suspend kills the hardware, so
there's nothing to deliver.

Fix in two places:

1. restoreAfterVT(): clear isPageFlipPending, isFrameRunning, and
   frameEventScheduled for all connectors before recheckOutputs(). For
   VT switch this is harmless (the events arrive anyway and handlePF
   would set them false). For suspend it unblocks frame scheduling.

2. commitState(): record a CLOCK_BOOTTIME timestamp when
   isPageFlipPending is set. If a modeset finds a flip pending for
   >500ms (well past any vblank), treat it as stale and clear the
   flags. CLOCK_BOOTTIME instead of CLOCK_MONOTONIC because MONOTONIC
   freezes during suspend on Linux, making elapsed time look like zero
   after resume.

Timestamp recorded in both atomic and legacy commit paths.

Relates to Hyprland#8312, Hyprland#6289.

---------

Co-authored-by: j4kuuu <j4kuuu>
This commit is contained in:
Sc4r3Cr0w 2026-04-27 22:02:14 +02:00 committed by GitHub
parent cd8321eba2
commit f5cdaa8801
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 114 additions and 7 deletions

View file

@ -312,7 +312,8 @@ namespace Aquamarine {
Hyprutils::Math::Vector2D cursorPos, cursorSize, cursorHotspot;
Hyprutils::Memory::CSharedPointer<CDRMFB> pendingCursorFB;
bool isPageFlipPending = false;
bool isPageFlipPending = false;
uint64_t pageFlipPendingAtMs = 0; // CLOCK_BOOTTIME ms when isPageFlipPending was set
SDRMPageFlip pendingPageFlip;
bool frameEventScheduled = false;
bool isFrameRunning = false;

View file

@ -378,6 +378,27 @@ bool Aquamarine::CDRMBackend::sessionActive() {
void Aquamarine::CDRMBackend::restoreAfterVT() {
backend->log(AQ_LOG_DEBUG, "drm: Restoring after VT switch");
// Clear stale page-flip bookkeeping for all connectors.
// During S3 suspend the display hardware powers off, so any pending
// page-flip completion events are lost. The handlePF() callback that
// normally clears these flags will never fire. Without this reset,
// commitState() rejects every frame with "Cannot commit when a
// page-flip is awaiting" and scheduleFrame() returns early, leaving
// outputs permanently black after resume.
//
// For VT switch this is also safe: pending events from the old session
// are still queued in the fd buffer and will fire handlePF() after
// restore, but isPageFlipPending is already false so the = false
// assignment is a harmless no-op.
for (auto const& c : connectors) {
if (c->isPageFlipPending || c->isFrameRunning) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Clearing stale page-flip state for {}", c->szName));
c->isPageFlipPending = false;
c->isFrameRunning = false;
c->frameEventScheduled = false;
}
}
recheckOutputs();
backend->log(AQ_LOG_DEBUG, "drm: Rescanned connectors");
@ -710,6 +731,12 @@ void Aquamarine::CDRMBackend::recheckCRTCs() {
continue;
}
// disabled outputs release their CRTCs so active outputs get priority
if (c->crtc && c->status == DRM_MODE_CONNECTED && c->output && !c->output->enabledState) {
backend->log(AQ_LOG_DEBUG, std::format("drm: {} is disabled, releasing crtc {}", c->szName, c->crtc->id));
c->crtc.reset();
}
if (c->crtc && c->status == DRM_MODE_CONNECTED) {
backend->log(AQ_LOG_DEBUG, std::format("drm: Skipping connector {}, has crtc {} and is connected", c->szName, c->crtc->id));
continue;
@ -739,7 +766,7 @@ void Aquamarine::CDRMBackend::recheckCRTCs() {
bool assigned = false;
// try to use a connected connector
// try to use a connected, enabled connector
for (auto const& c : recheck) {
if (!(c->possibleCrtcs & (1 << i)))
continue;
@ -747,6 +774,10 @@ void Aquamarine::CDRMBackend::recheckCRTCs() {
if (c->status != DRM_MODE_CONNECTED)
continue;
// Pass 1 only assigns to enabled connectors
if (c->output && !c->output->enabledState)
continue;
// deactivate old output
if (c->output && c->output->state && c->output->state->state().enabled) {
c->output->state->setEnabled(false);
@ -766,11 +797,38 @@ void Aquamarine::CDRMBackend::recheckCRTCs() {
backend->log(AQ_LOG_DEBUG, std::format("drm: slot {} crtc {} unassigned", i, crtc->id));
}
// Pass 2: assign remaining CRTCs to disabled connectors as backup slots
for (size_t i = 0; i < crtcs.size(); ++i) {
bool taken = false;
for (auto const& c : connectors) {
if (c->crtc == crtcs.at(i)) {
taken = true;
break;
}
}
if (taken)
continue;
for (auto const& c : recheck) {
if (!(c->possibleCrtcs & (1 << i)))
continue;
if (c->status != DRM_MODE_CONNECTED)
continue;
backend->log(AQ_LOG_DEBUG, std::format("drm: backup slot {} crtc {} assigned to disabled {}", i, crtcs.at(i)->id, c->szName));
c->crtc = crtcs.at(i);
std::erase(recheck, c);
break;
}
}
for (auto const& c : connectors) {
if (c->status == DRM_MODE_CONNECTED)
continue;
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} is not connected{}", c->szName, c->crtc ? std::format(", removing old crtc {}", c->crtc->id) : ""));
if (c->crtc)
backend->log(AQ_LOG_DEBUG, std::format("drm: {} is not connected, clearing stale crtc {}", c->szName, c->crtc->id));
c->crtc.reset();
}
// tell the user to re-assign a valid mode etc, if needed
@ -896,6 +954,11 @@ void Aquamarine::CDRMBackend::recheckOutputs() {
// now that crtcs are assigned, connect outputs
for (const auto& conn : connectors) {
if (conn->status == DRM_MODE_CONNECTED && !conn->output && !conn->tilingRedundant) {
if (!conn->crtc) {
backend->log(AQ_LOG_DEBUG, std::format("drm: {} has no CRTC, deferring connection", conn->szName));
continue;
}
backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} connected", conn->szName));
auto drmConn = drmModeGetConnector(gpu->fd, conn->id);
@ -1740,13 +1803,27 @@ void Aquamarine::SDRMConnector::applyCommit(const SDRMConnectorCommitData& data)
if (output->state->state().committed & COutputState::AQ_OUTPUT_STATE_MODE)
refresh = calculateRefresh(data.modeInfo);
output->enabledState = output->state->state().enabled;
const bool wasEnabled = output->enabledState;
output->enabledState = output->state->state().enabled;
if (!output->enabledState)
releaseFBReferences();
if (!backend->updateSecondaryRendererState())
backend->backend->log(AQ_LOG_ERROR, std::format("drm: Failed to update renderer state for {} on applyCommit", szName));
if (wasEnabled != output->enabledState) {
auto bk = backend.lock();
if (bk) {
bk->backend->log(AQ_LOG_DEBUG, std::format("drm: Connector {} enabledState changed {} -> {}", szName, wasEnabled, output->enabledState));
auto weak = bk->self;
bk->backend->addIdleEvent(makeShared<std::function<void(void)>>([weak] {
auto b = weak.lock();
if (b)
b->recheckOutputs();
}));
}
}
}
void Aquamarine::SDRMConnector::rollbackCommit(const SDRMConnectorCommitData& data) {
@ -1898,8 +1975,30 @@ bool Aquamarine::CDRMOutput::commitState(bool onlyTest) {
}
if (STATE.enabled && (NEEDS_RECONFIG || (COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_BUFFER)) && connector->isPageFlipPending) {
backend->backend->log(AQ_LOG_ERROR, "drm: Cannot commit when a page-flip is awaiting");
return false;
// Check if the pending page-flip is stale (>500ms — well beyond
// any vblank interval, even at low refresh rates). Stale flips
// occur after S3/S4 suspend when page-flip completion events are
// lost because the display hardware was powered off.
struct timespec ts;
clock_gettime(CLOCK_BOOTTIME, &ts);
uint64_t nowMs = ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000ULL;
bool staleFlip = (nowMs - connector->pageFlipPendingAtMs) > 500;
if (NEEDS_RECONFIG && staleFlip) {
// A blocking modeset uses DRM_MODE_ATOMIC_ALLOW_MODESET which
// fully resets the CRTC, implicitly cancelling any stale
// page-flip at the kernel level. Clear the stale userspace
// bookkeeping to match.
backend->backend->log(AQ_LOG_DEBUG,
std::format("drm: Clearing stale page-flip state for {} during modeset (pending for {}ms)", name,
nowMs - connector->pageFlipPendingAtMs));
connector->isPageFlipPending = false;
connector->isFrameRunning = false;
connector->frameEventScheduled = false;
} else {
backend->backend->log(AQ_LOG_ERROR, "drm: Cannot commit when a page-flip is awaiting");
return false;
}
}
if (STATE.enabled && (COMMITTED & COutputState::eOutputStateProperties::AQ_OUTPUT_STATE_BUFFER))

View file

@ -462,8 +462,12 @@ bool Aquamarine::CDRMAtomicImpl::commit(Hyprutils::Memory::CSharedPointer<SDRMCo
if (ok) {
request.apply(data);
if (!data.test && data.mainFB && connector->output->state->state().enabled && (flags & DRM_MODE_PAGE_FLIP_EVENT))
if (!data.test && data.mainFB && connector->output->state->state().enabled && (flags & DRM_MODE_PAGE_FLIP_EVENT)) {
connector->isPageFlipPending = true;
struct timespec ts;
clock_gettime(CLOCK_BOOTTIME, &ts);
connector->pageFlipPendingAtMs = ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000ULL;
}
} else
request.rollback(data);

View file

@ -138,6 +138,9 @@ bool Aquamarine::CDRMLegacyImpl::commitInternal(Hyprutils::Memory::CSharedPointe
}
connector->isPageFlipPending = true;
struct timespec ts;
clock_gettime(CLOCK_BOOTTIME, &ts);
connector->pageFlipPendingAtMs = ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000ULL;
return true;
}