monitor: centralize solitary and scanout eligibility checks

This commit is contained in:
Pppp1116 2026-04-03 13:26:49 +01:00 committed by Vaxry
parent d5b00bbc17
commit 4c42269ce6
7 changed files with 195 additions and 43 deletions

View file

@ -1,7 +1,50 @@
#include <xf86drm.h>
#include <array>
#include <map>
#include <mutex>
#include <optional>
#include <sys/stat.h>
#include "Drm.hpp"
namespace {
using SDRMNodePair = std::array<dev_t, 2>;
std::optional<SDRMNodePair> getDrmNodePair(int fd1, int fd2) {
const auto DEVA = DRM::devIDFromFD(fd1);
const auto DEVB = DRM::devIDFromFD(fd2);
if (!DEVA || !DEVB)
return std::nullopt;
SDRMNodePair pair = {*DEVA, *DEVB};
if (pair[0] > pair[1])
std::swap(pair[0], pair[1]);
return pair;
}
}
std::optional<dev_t> DRM::devIDFromFD(int fd) {
struct stat stat = {};
if (fstat(fd, &stat) != 0 || !S_ISCHR(stat.st_mode))
return std::nullopt;
return stat.st_rdev;
}
bool DRM::sameGpu(int fd1, int fd2) {
if (fd1 >= 0 && fd1 == fd2)
return true;
static std::mutex cacheMutex;
static std::map<SDRMNodePair, bool> sameGpuCache;
const auto NODEPAIR = getDrmNodePair(fd1, fd2);
if (NODEPAIR) {
std::scoped_lock lock(cacheMutex);
if (const auto it = sameGpuCache.find(*NODEPAIR); it != sameGpuCache.end())
return it->second;
}
drmDevice* devA = nullptr;
drmDevice* devB = nullptr;
@ -16,5 +59,11 @@ bool DRM::sameGpu(int fd1, int fd2) {
drmFreeDevice(&devA);
drmFreeDevice(&devB);
if (NODEPAIR) {
std::scoped_lock lock(cacheMutex);
sameGpuCache[*NODEPAIR] = same;
}
return same;
}

View file

@ -1,5 +1,9 @@
#pragma once
#include <optional>
#include <sys/types.h>
namespace DRM {
bool sameGpu(int fd1, int fd2);
std::optional<dev_t> devIDFromFD(int fd);
bool sameGpu(int fd1, int fd2);
}

View file

@ -1661,7 +1661,25 @@ void CMonitor::setCTM(const Mat3x3& ctm_) {
}
uint32_t CMonitor::isSolitaryBlocked(bool full) {
uint32_t reasons = 0;
uint32_t reasons = 0;
const auto PWORKSPACE = m_activeWorkspace;
if (!PWORKSPACE) {
reasons |= SC_WORKSPACE;
return reasons;
}
if (!PWORKSPACE->m_hasFullscreenWindow) {
reasons |= SC_WINDOWED;
if (!full)
return reasons;
}
if (m_activeSpecialWorkspace) {
reasons |= SC_SPECIAL;
if (!full)
return reasons;
}
if (g_pHyprNotificationOverlay->hasAny()) {
reasons |= SC_NOTIFICATION;
@ -1681,30 +1699,12 @@ uint32_t CMonitor::isSolitaryBlocked(bool full) {
return reasons;
}
const auto PWORKSPACE = m_activeWorkspace;
if (!PWORKSPACE) {
reasons |= SC_WORKSPACE;
return reasons;
}
if (!PWORKSPACE->m_hasFullscreenWindow) {
reasons |= SC_WINDOWED;
if (!full)
return reasons;
}
if (PROTO::data->dndActive()) {
reasons |= SC_DND;
if (!full)
return reasons;
}
if (m_activeSpecialWorkspace) {
reasons |= SC_SPECIAL;
if (!full)
return reasons;
}
if (PWORKSPACE->m_alpha->value() != 1.f) {
reasons |= SC_ALPHA;
if (!full)
@ -1780,10 +1780,15 @@ uint32_t CMonitor::isSolitaryBlocked(bool full) {
void CMonitor::recheckSolitary() {
m_solitaryClient.reset(); // reset it, if we find one it will be set.
const auto PWORKSPACE = m_activeWorkspace;
if (!PWORKSPACE)
return;
if (isSolitaryBlocked())
return;
m_solitaryClient = m_activeWorkspace->getFullscreenWindow();
m_solitaryClient = PWORKSPACE->getFullscreenWindow();
}
uint8_t CMonitor::isTearingBlocked(bool full) {
@ -1847,6 +1852,15 @@ uint16_t CMonitor::isDSBlocked(bool full) {
static auto PDIRECTSCANOUT = CConfigValue<Hyprlang::INT>("render:direct_scanout");
static auto PPASS = CConfigValue<Hyprlang::INT>("render:cm_fs_passthrough");
static auto PNONSHADER = CConfigValue<Hyprlang::INT>("render:non_shader_cm");
const auto PWORKSPACE = m_activeWorkspace;
// Fast reject for the hot render path; full=true callers still collect
// the remaining blockers for hyprctl/debug output below.
if (!canAttemptDirectScanoutFast()) {
reasons |= DS_BLOCK_CANDIDATE;
if (!full)
return reasons;
}
if (*PDIRECTSCANOUT == 0) {
reasons |= DS_BLOCK_USER;
@ -1855,11 +1869,11 @@ uint16_t CMonitor::isDSBlocked(bool full) {
}
if (*PDIRECTSCANOUT == 2) {
if (!m_activeWorkspace || !m_activeWorkspace->m_hasFullscreenWindow || m_activeWorkspace->m_fullscreenMode != FSMODE_FULLSCREEN) {
if (!PWORKSPACE || !PWORKSPACE->m_hasFullscreenWindow || PWORKSPACE->m_fullscreenMode != FSMODE_FULLSCREEN) {
reasons |= DS_BLOCK_WINDOWED;
if (!full)
return reasons;
} else if (m_activeWorkspace->getFullscreenWindow()->getContentType() != CONTENT_TYPE_GAME) {
} else if (PWORKSPACE->getFullscreenWindow()->getContentType() != CONTENT_TYPE_GAME) {
reasons |= DS_BLOCK_CONTENT;
if (!full)
return reasons;
@ -1930,12 +1944,7 @@ bool CMonitor::attemptDirectScanout() {
const auto PCANDIDATE = m_solitaryClient.lock();
const auto PSURFACE = PCANDIDATE->getSolitaryResource();
const auto params = PSURFACE->m_current.buffer->dmabuf();
Log::logger->log(Log::TRACE, "attemptDirectScanout: surface {:x} passed, will attempt, buffer {} fmt: {} -> {} (mod {})", rc<uintptr_t>(PSURFACE.get()),
rc<uintptr_t>(PSURFACE->m_current.buffer.m_buffer.get()), m_drmFormat, params.format, params.modifier);
auto PBUFFER = PSURFACE->m_current.buffer.m_buffer;
auto PBUFFER = PSURFACE->m_current.buffer.m_buffer;
// #TODO this entire bit needs figuring out, vrr goes down the drain without it
if (PBUFFER == m_output->state->state().buffer && *PSAME) {
@ -1963,6 +1972,11 @@ bool CMonitor::attemptDirectScanout() {
return true;
}
const auto params = PSURFACE->m_current.buffer->dmabuf();
Log::logger->log(Log::TRACE, "attemptDirectScanout: surface {:x} passed, will attempt, buffer {} fmt: {} -> {} (mod {})", rc<uintptr_t>(PSURFACE.get()),
rc<uintptr_t>(PSURFACE->m_current.buffer.m_buffer.get()), m_drmFormat, params.format, params.modifier);
// FIXME: make sure the buffer actually follows the available scanout dmabuf formats
// and comes from the appropriate device. This may implode on multi-gpu!!
@ -1990,7 +2004,7 @@ bool CMonitor::attemptDirectScanout() {
m_output->state->addDamage(PSURFACE->m_current.accumulateBufferDamage());
// multigpu needs a fence to trigger fence syncing blits and also committing with the recreated dgpu fence
if (!DRM::sameGpu(m_output->getBackend()->preferredAllocator()->drmFD(), g_pCompositor->m_drm.fd) && g_pHyprRenderer->explicitSyncSupported()) {
if (g_pHyprRenderer->explicitSyncSupported() && isMultiGPU()) {
auto sync = g_pHyprRenderer->createSyncFDManager();
if (sync->fd().isValid()) {
@ -2032,6 +2046,47 @@ bool CMonitor::attemptDirectScanout() {
return true;
}
bool CMonitor::canAttemptDirectScanoutFast() const {
return !m_solitaryClient.expired() || !m_lastScanout.expired() || m_directScanoutIsActive;
}
bool CMonitor::isMultiGPU() {
if (!m_output || !g_pCompositor)
return false;
const auto PREFERREDALLOCATOR = m_output->getBackend()->preferredAllocator();
const int allocatorFD = PREFERREDALLOCATOR ? PREFERREDALLOCATOR->drmFD() : -1;
const int compositorFD = g_pCompositor->m_drm.fd;
if (allocatorFD < 0 || compositorFD < 0) {
m_cachedAllocatorDRMDev.reset();
m_cachedCompositorDRMDev.reset();
m_cachedAllocatorDRMFD = allocatorFD;
m_cachedCompositorDRMFD = compositorFD;
m_cachedSameGPU = true;
return false;
}
const auto allocatorDev = DRM::devIDFromFD(allocatorFD);
const auto compositorDev = DRM::devIDFromFD(compositorFD);
// AQ can reopen DRM nodes for refcounting, so raw fd numbers are not a stable cache key.
const bool useDeviceIDCache = allocatorDev.has_value() && compositorDev.has_value();
const bool cacheStale = !m_cachedSameGPU ||
(useDeviceIDCache ? m_cachedAllocatorDRMDev != allocatorDev || m_cachedCompositorDRMDev != compositorDev :
m_cachedAllocatorDRMFD != allocatorFD || m_cachedCompositorDRMFD != compositorFD);
if (cacheStale) {
m_cachedAllocatorDRMDev = allocatorDev;
m_cachedCompositorDRMDev = compositorDev;
m_cachedAllocatorDRMFD = allocatorFD;
m_cachedCompositorDRMFD = compositorFD;
m_cachedSameGPU = DRM::sameGpu(allocatorFD, compositorFD);
}
return !*m_cachedSameGPU;
}
bool CMonitor::shouldUseSoftwareCursors() {
static auto PNOHW = CConfigValue<Hyprlang::INT>("cursor:no_hardware_cursors");
static auto PINVISIBLE = CConfigValue<Hyprlang::INT>("cursor:invisible");

View file

@ -25,6 +25,7 @@
#include <aquamarine/output/Output.hpp>
#include <aquamarine/allocator/Swapchain.hpp>
#include <hyprutils/os/FileDescriptor.hpp>
#include <sys/types.h>
#include "../helpers/TransferFunction.hpp"
#include "../config/shared/monitor/MonitorRule.hpp"
@ -323,6 +324,8 @@ class CMonitor {
bool updateTearing();
uint16_t isDSBlocked(bool full = false);
bool attemptDirectScanout();
bool canAttemptDirectScanoutFast() const;
bool isMultiGPU();
void setCTM(const Mat3x3& ctm);
void onCursorMovedOnMonitor();
void setDPMS(bool on);
@ -370,6 +373,12 @@ class CMonitor {
PHLWINDOWREF m_previousFSWindow;
bool m_needsHDRupdate = false;
std::optional<dev_t> m_cachedAllocatorDRMDev;
std::optional<dev_t> m_cachedCompositorDRMDev;
int m_cachedAllocatorDRMFD = -1;
int m_cachedCompositorDRMFD = -1;
std::optional<bool> m_cachedSameGPU;
NColorManagement::PImageDescription m_imageDescription = NColorManagement::CImageDescription::from(NColorManagement::SImageDescription{});
bool m_noShaderCTM = false; // sets drm CTM, restore needed

View file

@ -455,7 +455,7 @@ SP<Aquamarine::IBuffer> CPointerManager::renderHWCursorBuffer(SP<CPointerManager
options.length = 2;
options.scanout = true;
options.cursor = true;
options.multigpu = !DRM::sameGpu(state->monitor->m_output->getBackend()->preferredAllocator()->drmFD(), g_pCompositor->m_drm.fd);
options.multigpu = state->monitor->isMultiGPU();
// We do not set the format (unless shm). If it's unset (DRM_FORMAT_INVALID) then the swapchain will pick for us,
// but if it's set, we don't wanna change it.
if (shouldUseCpuBuffer)

View file

@ -1901,21 +1901,24 @@ void IHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) {
return;
// tearing and DS first
bool shouldTear = pMonitor->updateTearing();
bool shouldTear = pMonitor->updateTearing();
const bool canAttemptDirectScanout = pMonitor->canAttemptDirectScanoutFast();
if (pMonitor->attemptDirectScanout()) {
pMonitor->m_directScanoutIsActive = true;
return;
} else if (!pMonitor->m_lastScanout.expired() || pMonitor->m_directScanoutIsActive) {
Log::logger->log(Log::DEBUG, "Left a direct scanout.");
pMonitor->m_lastScanout.reset();
pMonitor->m_directScanoutIsActive = false;
if (canAttemptDirectScanout) {
if (pMonitor->attemptDirectScanout()) {
pMonitor->m_directScanoutIsActive = true;
return;
} else if (!pMonitor->m_lastScanout.expired() || pMonitor->m_directScanoutIsActive) {
Log::logger->log(Log::DEBUG, "Left a direct scanout.");
pMonitor->m_lastScanout.reset();
pMonitor->m_directScanoutIsActive = false;
// reset DRM format, but only if needed since it might modeset
if (pMonitor->m_output->state->state().drmFormat != pMonitor->m_prevDrmFormat)
pMonitor->m_output->state->setFormat(pMonitor->m_prevDrmFormat);
// reset DRM format, but only if needed since it might modeset
if (pMonitor->m_output->state->state().drmFormat != pMonitor->m_prevDrmFormat)
pMonitor->m_output->state->setFormat(pMonitor->m_prevDrmFormat);
pMonitor->m_drmFormat = pMonitor->m_prevDrmFormat;
pMonitor->m_drmFormat = pMonitor->m_prevDrmFormat;
}
}
Event::bus()->m_events.render.pre.emit(pMonitor);

32
tests/helpers/Drm.cpp Normal file
View file

@ -0,0 +1,32 @@
#include <helpers/Drm.hpp>
#include <gtest/gtest.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
TEST(Helpers, drmDevIDFromFDCharacterDevice) {
const int FD = open("/dev/null", O_RDONLY | O_CLOEXEC);
ASSERT_GE(FD, 0);
struct stat stat = {};
ASSERT_EQ(fstat(FD, &stat), 0);
const auto devID = DRM::devIDFromFD(FD);
EXPECT_TRUE(devID.has_value());
EXPECT_EQ(*devID, stat.st_rdev);
close(FD);
}
TEST(Helpers, drmDevIDFromFDRejectsRegularFiles) {
char path[] = "/tmp/hyprland-drm-testXXXXXX";
const int FD = mkstemp(path);
ASSERT_GE(FD, 0);
EXPECT_FALSE(DRM::devIDFromFD(FD).has_value());
close(FD);
unlink(path);
}