/*
    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"
