Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion include/aquamarine/backend/DRM.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
109 changes: 104 additions & 5 deletions src/backend/drm/DRM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,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");
Expand Down Expand Up @@ -699,6 +720,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;
Expand Down Expand Up @@ -727,14 +754,18 @@ 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;

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);
Expand All @@ -754,11 +785,38 @@ void Aquamarine::CDRMBackend::recheckCRTCs() {
backend->log(AQ_LOG_DEBUG, std::format("drm: slot {} crtc {} unassigned", i, crtcs.at(i)->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
Expand Down Expand Up @@ -884,6 +942,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);
Expand Down Expand Up @@ -1726,13 +1789,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) {
Expand Down Expand Up @@ -1884,8 +1961,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))
Expand Down
6 changes: 5 additions & 1 deletion src/backend/drm/impl/Atomic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 3 additions & 0 deletions src/backend/drm/impl/Legacy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down