Files
RedBear-OS/local/recipes/kde/kwin/source/autotests/integration/drm_test.cpp
T

720 lines
26 KiB
C++

/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2025 Xaver Hugl <xaver.hugl@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "backends/drm/drm_gpu.h"
#include "backends/drm/drm_object.h"
#include "backends/drm/drm_output.h"
#include "backends/drm/drm_pointer.h"
#include "compositor.h"
#include "core/drmdevice.h"
#include "core/graphicsbuffer.h"
#include "core/outputbackend.h"
#include "core/outputconfiguration.h"
#include "core/outputlayer.h"
#include "core/renderbackend.h"
#include "kwin_wayland_test.h"
#include "scene/surfaceitem.h"
#include "wayland-client/linuxdmabuf.h"
#include "wayland-client/viewporter.h"
#include "wayland_server.h"
#include "workspace.h"
#include <KWayland/Client/pointer.h>
#include <KWayland/Client/seat.h>
#include <ranges>
#include <xf86drmMode.h>
using namespace std::chrono_literals;
namespace KWin
{
static const QString s_socketName = QStringLiteral("test_drm");
class DrmTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void init();
void initTestCase();
void cleanup();
void testModesets();
void testPresentation();
void testCursorLayer();
void testDirectScanout_data();
void testDirectScanout();
void testOverlay_data();
void testOverlay();
void testDpms();
};
struct DrmCrtcState
{
bool active;
QSize modeSize;
uint32_t refreshRateMilliHertz;
static std::optional<DrmCrtcState> read(int fd, uint32_t id);
};
std::optional<DrmCrtcState> DrmCrtcState::read(int fd, uint32_t id)
{
DrmUniquePtr<drmModeCrtc> crtc(drmModeGetCrtc(fd, id));
if (!crtc) {
return std::nullopt;
}
auto props = DrmObject::queryProperties(fd, id, DRM_MODE_OBJECT_CRTC);
const auto active = props.takeProperty(QByteArrayLiteral("ACTIVE"));
if (!active.has_value()) {
return std::nullopt;
}
return DrmCrtcState{
.active = active->second == 1,
.modeSize = crtc->mode_valid ? QSize(crtc->mode.hdisplay, crtc->mode.vdisplay) : QSize(),
.refreshRateMilliHertz = crtc->mode_valid ? DrmConnector::refreshRateForMode(&crtc->mode) : 0,
};
}
struct DrmPlaneState
{
uint32_t crtcId;
Rect destinationRect;
uint32_t frambufferId;
std::array<uint32_t, 4> framebufferGemNames;
static std::optional<DrmPlaneState> read(int fd, uint32_t id);
};
std::optional<DrmPlaneState> DrmPlaneState::read(int fd, uint32_t id)
{
auto props = DrmObject::queryProperties(fd, id, DRM_MODE_OBJECT_PLANE);
const auto crtcId = props.takeProperty(QByteArrayLiteral("CRTC_ID"));
const auto crtcX = props.takeProperty(QByteArrayLiteral("CRTC_X"));
const auto crtcY = props.takeProperty(QByteArrayLiteral("CRTC_Y"));
const auto crtcW = props.takeProperty(QByteArrayLiteral("CRTC_W"));
const auto crtcH = props.takeProperty(QByteArrayLiteral("CRTC_H"));
const auto fbId = props.takeProperty(QByteArrayLiteral("FB_ID"));
if (!crtcId || !crtcX || !crtcY || !crtcW || !crtcH || !fbId) {
return std::nullopt;
}
std::array<uint32_t, 4> framebufferGemNames = {0};
if (auto ptr = drmModeGetFB2(fd, fbId->second)) {
std::unordered_set<uint32_t> handles;
const auto guard = qScopeGuard([&]() {
// NOTE that drmModeGetFB2 always creates new handles
for (uint32_t handle : handles) {
drmCloseBufferHandle(fd, handle);
}
drmModeFreeFB2(ptr);
});
for (int i = 0; i < 4; i++) {
if (ptr->handles[i] == 0) {
break;
}
handles.insert(ptr->handles[i]);
drm_gem_flink flink{
.handle = ptr->handles[i],
.name = 0,
};
if (drmIoctl(fd, DRM_IOCTL_GEM_FLINK, &flink) != 0) {
return std::nullopt;
}
framebufferGemNames[i] = flink.name;
}
}
return DrmPlaneState{
.crtcId = uint32_t(crtcId->second),
.destinationRect = Rect(crtcX->second, crtcY->second, crtcW->second, crtcH->second),
.frambufferId = uint32_t(fbId->second),
.framebufferGemNames = framebufferGemNames,
};
}
struct DrmConnectorState
{
uint32_t crtcId;
static std::optional<DrmConnectorState> read(int fd, uint32_t id);
};
std::optional<DrmConnectorState> DrmConnectorState::read(int fd, uint32_t id)
{
auto props = DrmObject::queryProperties(fd, id, DRM_MODE_OBJECT_CONNECTOR);
const auto crtcId = props.takeProperty(QByteArrayLiteral("CRTC_ID"));
if (!crtcId.has_value()) {
return std::nullopt;
}
return DrmConnectorState{
.crtcId = uint32_t(crtcId->second),
};
}
struct DrmState
{
std::unordered_map<uint32_t, DrmCrtcState> crtcs;
std::unordered_map<uint32_t, DrmPlaneState> planes;
std::unordered_map<uint32_t, DrmConnectorState> connectors;
static std::optional<DrmState> read(int fd);
};
std::optional<DrmState> DrmState::read(int fd)
{
DrmUniquePtr<drmModeRes> resources(drmModeGetResources(fd));
DrmUniquePtr<drmModePlaneRes> planeResources(drmModeGetPlaneResources(fd));
if (!resources || !planeResources) {
return std::nullopt;
}
DrmState ret;
for (uint32_t id : std::span(resources->connectors, resources->count_connectors)) {
const auto state = DrmConnectorState::read(fd, id);
if (!state) {
return std::nullopt;
}
ret.connectors[id] = *state;
}
for (uint32_t id : std::span(resources->crtcs, resources->count_crtcs)) {
const auto state = DrmCrtcState::read(fd, id);
if (!state) {
return std::nullopt;
}
ret.crtcs[id] = *state;
}
for (uint32_t id : std::span(planeResources->planes, planeResources->count_planes)) {
const auto state = DrmPlaneState::read(fd, id);
if (!state) {
return std::nullopt;
}
ret.planes[id] = *state;
}
return ret;
}
struct DrmOutputState
{
DrmConnectorState connector;
std::optional<DrmCrtcState> crtc;
std::unordered_map<uint32_t, DrmPlaneState> planes;
static std::optional<DrmOutputState> read(BackendOutput *output);
};
std::optional<DrmOutputState> DrmOutputState::read(BackendOutput *output)
{
const auto drmOutput = static_cast<DrmOutput *>(output);
const auto allState = DrmState::read(drmOutput->connector()->gpu()->fd());
if (!allState) {
return std::nullopt;
}
const auto connector = allState->connectors.find(drmOutput->connector()->id());
if (connector == allState->connectors.end()) {
return std::nullopt;
}
DrmOutputState ret;
ret.connector = connector->second;
if (connector->second.crtcId == 0) {
return ret;
}
const auto crtc = allState->crtcs.find(connector->second.crtcId);
if (crtc == allState->crtcs.end()) {
return std::nullopt;
}
ret.crtc = crtc->second;
ret.planes = allState->planes | std::views::filter([crtcId = connector->second.crtcId](const auto &state) {
return state.second.crtcId == crtcId;
}) | std::ranges::to<std::unordered_map>();
return ret;
}
// TODO move this stuff into XdgToplevelWindow instead?
class DmabufWindow : public QObject
{
Q_OBJECT
public:
explicit DmabufWindow(std::function<void(Test::XdgToplevel *toplevel)> setup = [](Test::XdgToplevel *toplevel) { })
: m_surface(Test::createSurface())
, m_shellSurface(Test::createXdgToplevelSurface(m_surface.get(), setup))
, m_device(DrmDevice::open(Test::linuxDmabuf()->mainDevice()))
, m_surfaceFeedback(Test::linuxDmabuf()->getSurfaceFeedback(*m_surface))
{
connect(m_surfaceFeedback.get(), &WaylandClient::LinuxDmabufFeedbackV1::changed, this, &DmabufWindow::reactToDmabufFeedback);
}
bool renderAndWaitForShown(const QSize &size)
{
const auto formats = m_surfaceFeedback->formats().empty() ? Test::linuxDmabuf()->formats() : m_surfaceFeedback->formats();
m_buffer = m_device->allocator()->allocate(GraphicsBufferOptions{
.size = size,
.format = DRM_FORMAT_XRGB8888,
.modifiers = formats[DRM_FORMAT_XRGB8888],
.software = false,
});
auto wlbuffer = Test::linuxDmabuf()->importBuffer(m_buffer.buffer());
m_surface->attachBuffer(wlbuffer);
m_surface->damage(QRect(QPoint(0, 0), size));
m_surface->commit(KWayland::Client::Surface::CommitFlag::None);
wl_buffer_destroy(wlbuffer);
m_window = Test::waitForWaylandWindowShown();
return m_window;
}
bool presentWait()
{
wl_buffer *wlbuffer = nullptr;
if (m_needsRealloc) {
const auto tranches = m_surfaceFeedback->tranches();
for (const auto &tranche : tranches) {
if (!tranche.formats.contains(m_buffer->dmabufAttributes()->format)) {
continue;
}
// scanout flag is currently implicit in GraphicsBufferOptions
m_buffer = m_device->allocator()->allocate(GraphicsBufferOptions{
.size = m_buffer->size(),
.format = m_buffer->dmabufAttributes()->format,
.modifiers = tranche.formats[m_buffer->dmabufAttributes()->format],
.software = false,
});
auto wlbuffer = Test::linuxDmabuf()->importBuffer(m_buffer.buffer());
m_surface->attachBuffer(wlbuffer);
m_surface->damage(QRect(QPoint(0, 0), m_buffer->size()));
m_needsRealloc = false;
break;
}
}
auto feedback = std::make_unique<Test::WpPresentationFeedback>(Test::presentationTime()->feedback(*m_surface));
m_surface->commit(KWayland::Client::Surface::CommitFlag::None);
if (wlbuffer) {
wl_buffer_destroy(wlbuffer);
}
QSignalSpy spy(feedback.get(), &Test::WpPresentationFeedback::presented);
return spy.wait();
}
void reactToDmabufFeedback()
{
m_needsRealloc = bool(m_buffer);
}
std::unique_ptr<KWayland::Client::Surface> m_surface;
std::unique_ptr<Test::XdgToplevel> m_shellSurface;
std::unique_ptr<DrmDevice> m_device;
std::unique_ptr<WaylandClient::LinuxDmabufFeedbackV1> m_surfaceFeedback;
GraphicsBufferRef m_buffer;
bool m_needsRealloc = false;
Window *m_window = nullptr;
};
void DrmTest::init()
{
QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PresentationTime
| Test::AdditionalWaylandInterface::Seat
| Test::AdditionalWaylandInterface::CursorShapeV1
| Test::AdditionalWaylandInterface::LinuxDmabuf
| Test::AdditionalWaylandInterface::Viewporter));
}
void DrmTest::initTestCase()
{
if (!Test::primaryNodeAvailable()) {
QSKIP("no primary node available");
return;
}
#ifdef FORCE_DRM_LEGACY
qputenv("KWIN_DRM_NO_AMS", "1");
#endif
#ifdef FORCE_NO_DRM_MODIFIERS
qputenv("KWIN_DRM_USE_MODIFIERS", "0");
#endif
// make sure overlays are allowed
qputenv("KWIN_USE_OVERLAYS", "1");
QVERIFY(waylandServer()->init(s_socketName));
kwinApp()->start();
const auto allOutputs = kwinApp()->outputBackend()->outputs();
QVERIFY(!allOutputs.isEmpty());
// Create a simple setup that's useful for most tests.
OutputConfiguration cfg;
cfg.changeSet(allOutputs.front())->enabled = true;
cfg.changeSet(allOutputs.front())->scaleSetting = 1;
for (BackendOutput *output : allOutputs | std::views::drop(1)) {
cfg.changeSet(output)->enabled = false;
}
QCOMPARE(workspace()->applyOutputConfiguration(cfg), OutputConfigurationError::None);
const auto state = DrmOutputState::read(allOutputs.front());
QVERIFY(state.has_value());
QVERIFY(state->crtc.has_value());
for (BackendOutput *output : allOutputs | std::views::drop(1)) {
const auto state = DrmOutputState::read(output);
QVERIFY(state.has_value());
QVERIFY(!state->crtc.has_value());
}
}
void DrmTest::cleanup()
{
Test::destroyWaylandConnection();
}
void DrmTest::testModesets()
{
// verify that modesetting works as expected
// and doesn't cause crashes or issues with presentation time
Test::XdgToplevelWindow window;
QVERIFY(window.show());
BackendOutput *output = kwinApp()->outputBackend()->outputs().front();
window.m_window->move(output->position());
// verify that we can cycle through modes
for (const auto &mode : output->modes()) {
OutputConfiguration cfg;
cfg.changeSet(output)->mode = mode;
QCOMPARE(workspace()->applyOutputConfiguration(cfg), OutputConfigurationError::None);
QVERIFY(window.presentWait());
const auto state = DrmOutputState::read(output);
QVERIFY(state);
QVERIFY(state->crtc.has_value());
QCOMPARE(state->crtc->modeSize, mode->size());
QCOMPARE(state->crtc->refreshRateMilliHertz, mode->refreshRate());
}
}
void DrmTest::testPresentation()
{
// verify that normal presentation works as expected
Test::XdgToplevelWindow window;
QVERIFY(window.show());
BackendOutput *output = kwinApp()->outputBackend()->outputs().front();
window.m_window->move(output->position());
for (int i = 0; i < 5; i++) {
QVERIFY(window.presentWait());
}
}
void DrmTest::testCursorLayer()
{
// verify that the cursor layer is used, if it is available
uint32_t time = 0;
BackendOutput *output = kwinApp()->outputBackend()->outputs().front();
const auto layers = Compositor::self()->backend()->compatibleOutputLayers(output);
if (layers.size() == 1) {
QSKIP("The driver only advertises a primary plane");
}
Test::XdgToplevelWindow window;
QVERIFY(window.show());
window.m_window->move(output->position());
// if there's a visible cursor, a non-primary plane should be used to present it
std::unique_ptr<KWayland::Client::Pointer> pointer{Test::waylandSeat()->createPointer()};
QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
Test::pointerMotion(window.m_window->frameGeometry().center(), time++);
QVERIFY(enteredSpy.wait());
auto cursorShapeDevice = Test::createCursorShapeDeviceV1(pointer.get());
cursorShapeDevice->set_shape(enteredSpy.last().at(0).value<quint32>(), Test::CursorShapeDeviceV1::shape_default);
QVERIFY(window.presentWait());
const int enabledLayers = std::ranges::count_if(layers, [](OutputLayer *layer) {
return layer->isEnabled();
});
QCOMPARE(enabledLayers, 2);
#ifndef FORCE_DRM_LEGACY
// NOTE that this *should* in principle work, but in practice doesn't work
// with all drivers in legacy modesetting. VKMS doesn't report the cursor
// plane as enabled if it was enabled through the legacy interface
const auto state = DrmOutputState::read(output);
QVERIFY(state);
QVERIFY(state->crtc.has_value());
auto enabledPlanes = state->planes | std::views::values | std::views::filter([](const auto &state) {
return state.crtcId != 0;
});
QCOMPARE(std::distance(enabledPlanes.begin(), enabledPlanes.end()), 2);
#endif
}
static std::array<uint32_t, 4> gemNames(int fd, const DmaBufAttributes *attributes)
{
std::array<uint32_t, 4> ret = {0, 0, 0, 0};
for (int i = 0; i < attributes->planeCount; i++) {
uint32_t handle = 0;
if (drmPrimeFDToHandle(fd, attributes->fd[i].get(), &handle) != 0) {
return {0};
}
drm_gem_flink flink{
.handle = handle,
.name = 0,
};
if (drmIoctl(fd, DRM_IOCTL_GEM_FLINK, &flink) != 0) {
return {0};
}
ret[i] = flink.name;
drmCloseBufferHandle(fd, handle);
}
return ret;
}
bool findBufferOnPlane(BackendOutput *output, GraphicsBuffer *buffer)
{
const auto state = DrmOutputState::read(output);
if (!state || !state->crtc.has_value()) {
return false;
}
const int gpuFd = static_cast<DrmOutput *>(output)->connector()->gpu()->fd();
const auto framebufferGemNames = gemNames(gpuFd, buffer->dmabufAttributes());
return std::ranges::any_of(state->planes | std::views::values, [&framebufferGemNames](const DrmPlaneState &state) {
return state.framebufferGemNames == framebufferGemNames;
});
}
void DrmTest::testDirectScanout_data()
{
BackendOutput *output = kwinApp()->outputBackend()->outputs().front();
QTest::addColumn<double>("outputScale");
QTest::addColumn<QSize>("bufferSize");
QTest::addColumn<Rect>("sourceViewport");
QTest::addRow("no scaling") << 1.0 << QSize(output->pixelSize()) << Rect(QPoint(), output->pixelSize());
QTest::addRow("no effective scaling") << 2.0 << QSize(output->pixelSize()) << Rect(QPoint(), output->pixelSize());
#ifndef FORCE_DRM_LEGACY
// TODO maybe also test that direct scanout does *not* happen with these cases and legacy modesetting?
QTest::addRow("scaled up by 2x") << 2.0 << QSize(output->pixelSize() / 2) << Rect(QPoint(), output->pixelSize() / 2);
QTest::addRow("scaled up by 2x + partial source") << 2.0 << output->pixelSize() << Rect(QPoint(output->pixelSize().width() / 4, output->pixelSize().height() / 2), output->pixelSize() / 2);
QTest::addRow("scaled down by 2x") << 2.0 << QSize(output->pixelSize() * 2) << Rect(QPoint(), output->pixelSize() * 2);
#endif
}
void DrmTest::testDirectScanout()
{
#ifdef FORCE_DRM_LEGACY
QSKIP("This test is known to be broken with legacy modesetting");
#endif
QVERIFY2(Test::linuxDmabuf(), "This test needs dmabuf support");
uint32_t time = 0;
BackendOutput *output = kwinApp()->outputBackend()->outputs().front();
QFETCH(double, outputScale);
QFETCH(QSize, bufferSize);
QFETCH(Rect, sourceViewport);
{
OutputConfiguration cfg;
cfg.changeSet(output)->scaleSetting = outputScale;
QCOMPARE(workspace()->applyOutputConfiguration(cfg), OutputConfigurationError::None);
}
const auto layers = Compositor::self()->backend()->compatibleOutputLayers(output);
if (layers.size() == 1) {
QSKIP("The driver only advertises a primary plane");
}
DmabufWindow window{[](Test::XdgToplevel *toplevel) {
toplevel->set_fullscreen(nullptr);
}};
auto viewport = Test::viewporter()->createViewport(*window.m_surface);
viewport->setSource(sourceViewport);
viewport->setDestination(output->pixelSize() / outputScale);
QVERIFY(window.renderAndWaitForShown(bufferSize));
// if there's a visible cursor, a non-primary plane should be used to present it
std::unique_ptr<KWayland::Client::Pointer> pointer{Test::waylandSeat()->createPointer()};
QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
Test::pointerMotion(window.m_window->frameGeometry().center(), time++);
QVERIFY(enteredSpy.wait());
auto cursorShapeDevice = Test::createCursorShapeDeviceV1(pointer.get());
cursorShapeDevice->set_shape(enteredSpy.last().at(0).value<quint32>(), Test::CursorShapeDeviceV1::shape_default);
// NOTE that a single presentWait is not enough, as reallocation may
// be necessary to put the buffer on a drm plane
QElapsedTimer timer;
timer.start();
while (timer.durationElapsed() < 5s) {
window.m_surface->damageBuffer(QRect(QPoint(), QSize(100, 100)));
QVERIFY(window.presentWait());
if (findBufferOnPlane(output, window.m_buffer.buffer())) {
break;
}
}
QCOMPARE_LE(timer.durationElapsed(), 5s);
const int enabledLayers = std::ranges::count_if(layers, [](OutputLayer *layer) {
return layer->isEnabled();
});
QCOMPARE(enabledLayers, 2);
const auto state = DrmOutputState::read(output);
QVERIFY(state);
QVERIFY(state->crtc.has_value());
#ifndef FORCE_DRM_LEGACY
auto enabledPlanes = state->planes | std::views::values | std::views::filter([](const DrmPlaneState &state) {
return state.crtcId != 0;
});
// NOTE that this *should* in principle work, but in practice doesn't work
// with all drivers in legacy modesetting. VKMS doesn't report the cursor
// plane as enabled if it was enabled through the legacy interface
QCOMPARE(std::distance(enabledPlanes.begin(), enabledPlanes.end()), 2);
#endif
}
void DrmTest::testOverlay_data()
{
QTest::addColumn<bool>("occluded");
QTest::addColumn<double>("scale");
QTest::addColumn<Rect>("windowGeometry");
QTest::addColumn<Rect>("planeGeometry");
QTest::addRow("overlay") << false << 1.0 << Rect(51, 51, 100, 100) << Rect(0, 0, 100, 100);
QTest::addRow("underlay") << true << 1.0 << Rect(51, 51, 100, 100) << Rect(0, 0, 100, 100);
// this case verifies (among other things) that output position is properly rounded for the plane position
QTest::addRow("scaling + overlay") << false << 1.6 << Rect(52, 52, 63, 63) << Rect(1, 1, 101, 101);
// TODO also add a test case for occluded == false + SSD with rounded corners?
}
void DrmTest::testOverlay()
{
QVERIFY2(Test::linuxDmabuf(), "This test needs dmabuf support");
uint32_t time = 0;
BackendOutput *output = kwinApp()->outputBackend()->outputs().front();
const auto layers = Compositor::self()->backend()->compatibleOutputLayers(output);
if (layers.size() < 3) {
QSKIP("The driver doesn't advertise an overlay plane");
}
QFETCH(bool, occluded);
QFETCH(double, scale);
QFETCH(Rect, windowGeometry);
QFETCH(Rect, planeGeometry);
Test::XdgToplevelWindow dummy;
QVERIFY(dummy.show());
dummy.m_window->move(windowGeometry.topLeft() + QPoint(50, 50));
{
OutputConfiguration cfg;
cfg.changeSet(output)->scaleSetting = scale;
cfg.changeSet(output)->pos = QPoint(51, 51);
QCOMPARE(workspace()->applyOutputConfiguration(cfg), OutputConfigurationError::None);
}
DmabufWindow window;
auto viewport = Test::viewporter()->createViewport(*window.m_surface);
viewport->setSource(planeGeometry.size());
viewport->setDestination(windowGeometry.size());
QVERIFY(window.renderAndWaitForShown(planeGeometry.size()));
window.m_window->move(windowGeometry.topLeft());
if (occluded) {
workspace()->raiseWindow(dummy.m_window);
}
// if there's a visible cursor, a non-primary plane should be used to present it
std::unique_ptr<KWayland::Client::Pointer> pointer{Test::waylandSeat()->createPointer()};
QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
Test::pointerMotion(window.m_window->frameGeometry().center(), time++);
QVERIFY(enteredSpy.wait());
auto cursorShapeDevice = Test::createCursorShapeDeviceV1(pointer.get());
cursorShapeDevice->set_shape(enteredSpy.last().at(0).value<quint32>(), Test::CursorShapeDeviceV1::shape_default);
// NOTE that a single presentWait is not enough, as reallocation may
// be necessary to put the buffer on a drm plane
QElapsedTimer timer;
timer.start();
while (timer.durationElapsed() < 5s) {
window.m_surface->damageBuffer(QRect(QPoint(), planeGeometry.size()));
QVERIFY(window.presentWait());
if (findBufferOnPlane(output, window.m_buffer.buffer())) {
break;
}
}
QCOMPARE_LE(timer.durationElapsed(), 5s);
const int enabledLayers = std::ranges::count_if(layers, [](OutputLayer *layer) {
return layer->isEnabled();
});
QCOMPARE(enabledLayers, 3);
const auto state = DrmOutputState::read(output);
QVERIFY(state);
QVERIFY(state->crtc.has_value());
auto enabledPlanes = state->planes | std::views::values | std::views::filter([](const DrmPlaneState &state) {
return state.crtcId != 0;
});
QCOMPARE(std::distance(enabledPlanes.begin(), enabledPlanes.end()), 3);
const auto sceneIt = std::ranges::find_if(enabledPlanes, [output](const DrmPlaneState &state) {
return state.destinationRect == Rect(QPoint(), output->modeSize());
});
QVERIFY(sceneIt != enabledPlanes.end());
const int gpuFd = static_cast<DrmOutput *>(output)->connector()->gpu()->fd();
const auto framebufferGemNames = gemNames(gpuFd, window.m_buffer->dmabufAttributes());
const auto overlayIt = std::ranges::find_if(enabledPlanes, [&framebufferGemNames](const DrmPlaneState &state) {
return state.framebufferGemNames == framebufferGemNames;
});
QVERIFY(overlayIt != enabledPlanes.end());
const auto &overlayPlane = *overlayIt;
QCOMPARE(overlayPlane.destinationRect, planeGeometry);
// TODO uncomment this once vkms supports the zpos property
// QCOMPARE_GE(overlayPlane.zpos, (*sceneIt).zpos);
}
void DrmTest::testDpms()
{
// the output should be enabled
BackendOutput *output = kwinApp()->outputBackend()->outputs().front();
auto state = DrmOutputState::read(output);
QVERIFY(state.has_value());
QVERIFY(state->crtc.has_value());
Test::XdgToplevelWindow dummy;
QVERIFY(dummy.show());
workspace()->requestDpmsState(Workspace::DpmsState::Off);
QSignalSpy stateChange(workspace(), &Workspace::dpmsStateChanged);
// requesting dpms off multiple times must not mess up renderloop inhibition
// or cause further state changes!
workspace()->requestDpmsState(Workspace::DpmsState::Off);
workspace()->requestDpmsState(Workspace::DpmsState::Off);
QCOMPARE(stateChange.count(), 0);
QVERIFY(stateChange.wait());
#ifndef FORCE_DRM_LEGACY
QCOMPARE(workspace()->dpmsState(), Workspace::DpmsState::TurningOff);
QVERIFY(stateChange.wait());
#endif
QCOMPARE(workspace()->dpmsState(), Workspace::DpmsState::Off);
state = DrmOutputState::read(output);
QVERIFY(state.has_value());
QVERIFY(!state->crtc.has_value() || !state->crtc->active);
workspace()->requestDpmsState(Workspace::DpmsState::On);
QVERIFY(dummy.presentWait());
state = DrmOutputState::read(output);
QVERIFY(state.has_value());
QVERIFY(state->crtc.has_value());
QVERIFY(state->crtc->active);
}
}
WAYLAND_DRM_TEST_MAIN(KWin::DrmTest)
#include "drm_test.moc"