Commit b9381012 authored by Vlad Zahorodnii's avatar Vlad Zahorodnii
Browse files

Add support for the wp_viewporter protocol

The wp_viewporter compositor extension allows clients to crop and scale
their surface. It can be useful for applications such as video players
because it may potentially reduce their power usage.
parent 5041e02f
......@@ -64,3 +64,14 @@ target_link_libraries(testKeyboardShortcutsInhibitorInterface Qt5::Test Plasma::
add_test(NAME kwayland-testKeyboardShortcutsInhibitorInterface COMMAND testKeyboardShortcutsInhibitorInterface)
ecm_mark_as_test(testKeyboardShortcutsInhibitorInterface)
########################################################
# Test Viewporter Interface
########################################################
ecm_add_qtwayland_client_protocol(VIEWPORTER_SRCS
PROTOCOL ${WaylandProtocols_DATADIR}/stable/viewporter/viewporter.xml
BASENAME viewporter
)
add_executable(testViewporterInterface test_viewporter_interface.cpp ${VIEWPORTER_SRCS})
target_link_libraries(testViewporterInterface Qt5::Test Plasma::KWaylandServer KF5::WaylandClient Wayland::Client)
add_test(NAME kwayland-testViewporterInterface COMMAND testViewporterInterface)
ecm_mark_as_test(testViewporterInterface)
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include <QThread>
#include <QtTest>
#include "../../src/server/compositor_interface.h"
#include "../../src/server/display.h"
#include "../../src/server/surface_interface.h"
#include "../../src/server/viewporter_interface.h"
#include "KWayland/Client/compositor.h"
#include "KWayland/Client/connection_thread.h"
#include "KWayland/Client/event_queue.h"
#include "KWayland/Client/registry.h"
#include "KWayland/Client/shm_pool.h"
#include "KWayland/Client/surface.h"
#include "qwayland-viewporter.h"
using namespace KWaylandServer;
class Viewporter : public QtWayland::wp_viewporter
{
};
class Viewport : public QtWayland::wp_viewport
{
};
class TestViewporterInterface : public QObject
{
Q_OBJECT
public:
~TestViewporterInterface() override;
private Q_SLOTS:
void initTestCase();
void testCropScale();
private:
KWayland::Client::ConnectionThread *m_connection;
KWayland::Client::EventQueue *m_queue;
KWayland::Client::Compositor *m_clientCompositor;
KWayland::Client::ShmPool *m_shm;
QThread *m_thread;
Display m_display;
CompositorInterface *m_serverCompositor;
Viewporter *m_viewporter;
};
static const QString s_socketName = QStringLiteral("kwin-wayland-server-viewporter-test-0");
void TestViewporterInterface::initTestCase()
{
m_display.setSocketName(s_socketName);
m_display.start();
QVERIFY(m_display.isRunning());
m_display.createShm();
m_display.createViewporter();
m_serverCompositor = m_display.createCompositor(this);
m_serverCompositor->create();
QVERIFY(m_serverCompositor->isValid());
m_connection = new KWayland::Client::ConnectionThread;
QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected);
m_connection->setSocketName(s_socketName);
m_thread = new QThread(this);
m_connection->moveToThread(m_thread);
m_thread->start();
m_connection->initConnection();
QVERIFY(connectedSpy.wait());
QVERIFY(!m_connection->connections().isEmpty());
m_queue = new KWayland::Client::EventQueue(this);
QVERIFY(!m_queue->isValid());
m_queue->setup(m_connection);
QVERIFY(m_queue->isValid());
auto registry = new KWayland::Client::Registry(this);
connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 id, quint32 version) {
if (interface == QByteArrayLiteral("wp_viewporter")) {
m_viewporter = new Viewporter();
m_viewporter->init(*registry, id, version);
}
});
QSignalSpy allAnnouncedSpy(registry, &KWayland::Client::Registry::interfaceAnnounced);
QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced);
QSignalSpy shmSpy(registry, &KWayland::Client::Registry::shmAnnounced);
registry->setEventQueue(m_queue);
registry->create(m_connection->display());
QVERIFY(registry->isValid());
registry->setup();
QVERIFY(allAnnouncedSpy.wait());
m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value<quint32>(),
compositorSpy.first().last().value<quint32>(), this);
QVERIFY(m_clientCompositor->isValid());
m_shm = registry->createShmPool(shmSpy.first().first().value<quint32>(),
shmSpy.first().last().value<quint32>(), this);
QVERIFY(m_shm->isValid());
}
TestViewporterInterface::~TestViewporterInterface()
{
if (m_viewporter) {
delete m_viewporter;
m_viewporter = nullptr;
}
if (m_shm) {
delete m_shm;
m_shm = nullptr;
}
if (m_queue) {
delete m_queue;
m_queue = nullptr;
}
if (m_thread) {
m_thread->quit();
m_thread->wait();
delete m_thread;
m_thread = nullptr;
}
m_connection->deleteLater();
m_connection = nullptr;
}
void TestViewporterInterface::testCropScale()
{
// Create a test surface.
QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated);
QVERIFY(serverSurfaceCreatedSpy.isValid());
QScopedPointer<KWayland::Client::Surface> clientSurface(m_clientCompositor->createSurface(this));
QVERIFY(serverSurfaceCreatedSpy.wait());
SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value<SurfaceInterface *>();
QVERIFY(serverSurface);
QSignalSpy serverSurfaceMappedSpy(serverSurface, &SurfaceInterface::mapped);
QVERIFY(serverSurfaceMappedSpy.isValid());
QSignalSpy serverSurfaceSizeChangedSpy(serverSurface, &SurfaceInterface::sizeChanged);
QVERIFY(serverSurfaceSizeChangedSpy.isValid());
// Map the surface.
QImage image(QSize(100, 50), QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::black);
KWayland::Client::Buffer::Ptr buffer = m_shm->createBuffer(image);
clientSurface->attachBuffer(buffer);
clientSurface->damage(image.rect());
clientSurface->commit(KWayland::Client::Surface::CommitFlag::None);
QVERIFY(serverSurfaceMappedSpy.wait());
QCOMPARE(serverSurface->size(), QSize(100, 50));
QCOMPARE(serverSurface->viewport(), QRectF(0, 0, 100, 50));
// Create a viewport for the surface.
QScopedPointer<Viewport> clientViewport(new Viewport);
clientViewport->init(m_viewporter->get_viewport(*clientSurface));
// Crop the surface.
clientViewport->set_source(wl_fixed_from_double(10), wl_fixed_from_double(10),
wl_fixed_from_double(30), wl_fixed_from_double(20));
clientSurface->commit(KWayland::Client::Surface::CommitFlag::None);
QVERIFY(serverSurfaceSizeChangedSpy.wait());
QCOMPARE(serverSurface->size(), QSize(30, 20));
QCOMPARE(serverSurface->viewport(), QRectF(10, 10, 30, 20));
// Scale the surface.
clientViewport->set_destination(500, 250);
clientSurface->commit(KWayland::Client::Surface::CommitFlag::None);
QVERIFY(serverSurfaceSizeChangedSpy.wait());
QCOMPARE(serverSurface->size(), QSize(500, 250));
QCOMPARE(serverSurface->viewport(), QRectF(10, 10, 30, 20));
// If the viewport is destroyed, the crop and scale state will be unset on a next commit.
clientViewport->destroy();
clientSurface->commit(KWayland::Client::Surface::CommitFlag::None);
QVERIFY(serverSurfaceSizeChangedSpy.wait());
QCOMPARE(serverSurface->size(), QSize(100, 50));
QCOMPARE(serverSurface->viewport(), QRectF(0, 0, 100, 50));
}
QTEST_GUILESS_MAIN(TestViewporterInterface)
#include "test_viewporter_interface.moc"
......@@ -58,6 +58,7 @@ set(SERVER_LIB_SRCS
textinput_interface_v0.cpp
textinput_interface_v2.cpp
touch_interface.cpp
viewporter_interface.cpp
xdgdecoration_v1_interface.cpp
xdgforeign_interface.cpp
xdgforeign_v2_interface.cpp
......@@ -238,6 +239,11 @@ ecm_add_qtwayland_server_protocol(SERVER_LIB_SRCS
BASENAME keyboard-shortcuts-inhibit-unstable-v1
)
ecm_add_qtwayland_server_protocol(SERVER_LIB_SRCS
PROTOCOL ${WaylandProtocols_DATADIR}/stable/viewporter/viewporter.xml
BASENAME viewporter
)
set(SERVER_GENERATED_SRCS
${CMAKE_CURRENT_BINARY_DIR}/wayland-blur-client-protocol.h
${CMAKE_CURRENT_BINARY_DIR}/wayland-blur-server-protocol.h
......@@ -386,6 +392,7 @@ set(SERVER_LIB_HEADERS
tablet_interface.h
textinput_interface.h
touch_interface.h
viewporter_interface.h
xdgdecoration_v1_interface.h
xdgforeign_interface.h
xdgoutput_interface.h
......
......@@ -39,6 +39,7 @@
#include "subcompositor_interface.h"
#include "tablet_interface.h"
#include "textinput_interface_p.h"
#include "viewporter_interface.h"
#include "xdgdecoration_v1_interface.h"
#include "xdgforeign_interface.h"
#include "xdgoutput_interface.h"
......@@ -516,6 +517,13 @@ KeyboardShortcutsInhibitManagerV1Interface *Display::createKeyboardShortcutsInhi
return d;
}
ViewporterInterface *Display::createViewporter(QObject *parent)
{
auto viewporter = new ViewporterInterface(this, parent);
connect(this, &Display::aboutToTerminate, viewporter, [viewporter] { delete viewporter; });
return viewporter;
}
void Display::createShm()
{
Q_ASSERT(d->display);
......
......@@ -79,6 +79,7 @@ class LinuxDmabufUnstableV1Interface;
class TabletManagerInterface;
class DataControlDeviceManagerV1Interface;
class KeyboardShortcutsInhibitManagerV1Interface;
class ViewporterInterface;
/**
* @brief Class holding the Wayland server display loop.
......@@ -327,6 +328,11 @@ public:
*/
KeyboardShortcutsInhibitManagerV1Interface *createKeyboardShortcutsInhibitManagerV1(QObject *parent = nullptr);
/**
* Creates the viewporter compositor extension.
*/
ViewporterInterface *createViewporter(QObject *parent = nullptr);
/**
* Gets the ClientConnection for the given @p client.
* If there is no ClientConnection yet for the given @p client, it will be created.
......
......@@ -329,16 +329,13 @@ void SurfaceInterface::Private::swapStates(State *source, State *target, bool em
const bool slideChanged = source->slideIsSet;
const bool childrenChanged = source->childrenChanged;
const bool visibilityChanged = bufferChanged && (bool(source->buffer) != bool(target->buffer));
bool sizeChanged = false;
auto buffer = target->buffer;
const QRectF oldViewport = target->viewport;
const QSize oldSize = target->size;
if (bufferChanged) {
// TODO: is the reffing correct for subsurfaces?
QSize oldSize;
if (target->buffer) {
oldSize = target->buffer->size();
if (emitChanged) {
target->buffer->unref();
QObject::disconnect(target->buffer, &BufferInterface::sizeChanged, q, &SurfaceInterface::sizeChanged);
} else {
delete target->buffer;
target->buffer = nullptr;
......@@ -347,21 +344,22 @@ void SurfaceInterface::Private::swapStates(State *source, State *target, bool em
if (source->buffer) {
if (emitChanged) {
source->buffer->ref();
QObject::connect(source->buffer, &BufferInterface::sizeChanged, q, &SurfaceInterface::sizeChanged);
}
const QSize newSize = source->buffer->size();
sizeChanged = newSize.isValid() && newSize != oldSize;
}
buffer = source->buffer;
}
// copy values
if (bufferChanged) {
target->buffer = buffer;
target->buffer = source->buffer;
target->offset = source->offset;
target->damage = source->damage;
target->bufferDamage = source->bufferDamage;
target->bufferIsSet = source->bufferIsSet;
}
if (source->sourceGeometryIsSet) {
target->sourceGeometry = source->sourceGeometry;
target->sourceGeometryIsSet = true;
}
if (source->destinationSizeIsSet) {
target->destinationSize = source->destinationSize;
target->destinationSizeIsSet = true;
}
if (childrenChanged) {
target->childrenChanged = source->childrenChanged;
target->children = source->children;
......@@ -414,6 +412,21 @@ void SurfaceInterface::Private::swapStates(State *source, State *target, bool em
if (!emitChanged) {
return;
}
if (target->buffer) {
if (target->sourceGeometry.isValid()) {
target->viewport = target->sourceGeometry;
} else {
target->viewport = QRectF(QPointF(0, 0), invertBufferTransform(target, target->buffer->size()));
}
if (target->destinationSize.isValid()) {
target->size = target->destinationSize;
} else {
target->size = target->viewport.size().toSize();
}
} else {
target->viewport = QRectF();
target->size = QSize();
}
if (opaqueRegionChanged) {
emit q->opaqueChanged(target->opaque);
}
......@@ -422,9 +435,6 @@ void SurfaceInterface::Private::swapStates(State *source, State *target, bool em
}
if (scaleFactorChanged) {
emit q->scaleChanged(target->scale);
if (buffer && !sizeChanged) {
emit q->sizeChanged();
}
}
if (transformChanged) {
emit q->transformChanged(target->transform);
......@@ -457,9 +467,12 @@ void SurfaceInterface::Private::swapStates(State *source, State *target, bool em
}
}
}
if (sizeChanged) {
if (target->size != oldSize) {
emit q->sizeChanged();
}
if (target->viewport != oldViewport) {
emit q->viewportChanged();
}
if (shadowChanged) {
emit q->shadowChanged();
}
......@@ -742,11 +755,13 @@ QPointer< SubSurfaceInterface > SurfaceInterface::subSurface() const
QSize SurfaceInterface::size() const
{
Q_D();
// TODO: apply transform to the buffer size
if (d->current.buffer) {
return d->current.buffer->size() / scale();
}
return QSize();
return d->current.size;
}
QRectF SurfaceInterface::viewport() const
{
Q_D();
return d->current.viewport;
}
QRect SurfaceInterface::boundingRect() const
......@@ -1084,10 +1099,55 @@ QSizeF SurfaceInterface::Private::invertBufferTransform(const State *state, cons
return transformed;
}
QPointF SurfaceInterface::Private::viewportTransform(const State *state, const QPointF &point) const
{
if (!viewportExtension)
return point;
qreal x = state->size.width() * (point.x() - state->viewport.x()) / state->viewport.width();
qreal y = state->size.height() * (point.y() - state->viewport.y()) / state->viewport.height();
return QPointF(x, y);
}
QPointF SurfaceInterface::Private::invertViewportTransform(const State *state, const QPointF &point) const
{
if (!viewportExtension)
return point;
qreal x = point.x() * state->viewport.width() / state->size.width() + state->viewport.x();
qreal y = point.y() * state->viewport.height() / state->size.height() + state->viewport.y();
return QPointF(x, y);
}
QSizeF SurfaceInterface::Private::viewportTransform(const State *state, const QSizeF &size) const
{
if (!viewportExtension)
return size;
qreal width = size.width() * state->size.width() / state->viewport.width();
qreal height = size.height() * state->size.height() / state->viewport.height();
return QSizeF(width, height);
}
QSizeF SurfaceInterface::Private::invertViewportTransform(const State *state, const QSizeF &size) const
{
if (!viewportExtension)
return size;
qreal width = size.width() * state->viewport.width() / state->size.width();
qreal height = size.height() * state->viewport.height() / state->size.height();
return QSize(width, height);
}
QPointF SurfaceInterface::Private::mapToBuffer(const State *state, const QPointF &point) const
{
QPointF transformed = point;
transformed = invertViewportTransform(state, transformed);
transformed = bufferTransform(state, transformed);
return transformed;
......@@ -1098,6 +1158,7 @@ QPointF SurfaceInterface::Private::mapFromBuffer(const State *state, const QPoin
QPointF transformed = point;
transformed = invertBufferTransform(state, transformed);
transformed = viewportTransform(state, transformed);
return transformed;
}
......@@ -1106,6 +1167,7 @@ QSizeF SurfaceInterface::Private::mapToBuffer(const State *state, const QSizeF &
{
QSizeF transformed = size;
transformed = invertViewportTransform(state, transformed);
transformed = bufferTransform(state, transformed);
return transformed;
......@@ -1116,6 +1178,7 @@ QSizeF SurfaceInterface::Private::mapFromBuffer(const State *state, const QSizeF
QSizeF transformed = size;
transformed = invertBufferTransform(state, transformed);
transformed = viewportTransform(state, transformed);
return transformed;
}
......
......@@ -185,11 +185,17 @@ public:
BufferInterface *buffer();
QPoint offset() const;
/**
* The size of the Surface in global compositor space.
* @see For buffer size use BufferInterface::size
* from SurfaceInterface::buffer
* @since 5.3
**/
* Returns the rectangle that indicates what area of the attached buffer is displayed.
*
* The size of the viewport rectangle doesn't correspond to the size of the surface.
*/
QRectF viewport() const;
/**
* Returns the current size of the surface, in surface coordinates.
*
* Note that there is no direct relationship between the surface size and the buffer size.
* In order to determine the size of the currently attached buffer, use buffer()->size().
*/
QSize size() const;
/**
* Returns the rectangle that bounds this surface and all of its sub-surfaces.
......@@ -378,7 +384,19 @@ Q_SIGNALS:
void damaged(const QRegion&);
void opaqueChanged(const QRegion&);
void inputChanged(const QRegion&);
/**
* This signal is emitted when the scale of the attached buffer has changed.
*
* Note that the compositor has to re-compute the texture coordinates after the scale
* of the buffer has been changed.
*/
void scaleChanged(qint32);
/**
* This signal is emitted when the buffer transform has changed.
*
* Note that the compositor has to re-compute the texture coordinates after the buffer
* transform has been changed.
*/
void transformChanged(KWaylandServer::OutputInterface::Transform);
/**
* Emitted when the Surface becomes visible, i.e. a non-null buffer has been attached.
......@@ -389,9 +407,21 @@ Q_SIGNALS:
**/
void unmapped();
/**
* This signal is emitted when the surface size has changed.
*
* Note that the compositor has to re-compute the texture coordinates after the surface
* size has been changed.
*
* @since 5.3
**/
*/
void sizeChanged();
/**
* This signal is emitted when the viewport rectangle has changed.
*
* Note that the compositor has to re-compute the texture coordinates after the viewport
* rectangle has been changed.
*/
void viewportChanged();
/**
* @since 5.4
**/
......@@ -462,6 +492,7 @@ private:
friend class IdleInhibitManagerUnstableV1Interface;
friend class PointerConstraintsUnstableV1Interface;
friend class SurfaceRole;
friend class ViewportInterface;
explicit SurfaceInterface(CompositorInterface *parent, wl_resource *parentResource);
class Private;
......
......@@ -19,6 +19,7 @@ namespace KWaylandServer
class IdleInhibitorInterface;
class SurfaceRole;
class ViewportInterface;
class SurfaceInterface::Private : public Resource::Private
{
......@@ -28,6 +29,12 @@ public:
QRegion bufferDamage = QRegion();
QRegion opaque = QRegion();
QRegion input = QRegion();
QRectF sourceGeometry = QRectF();
QSize destinationSize = QSize();
QRectF viewport = QRectF();
QSize size = QSize();
bool sourceGeometryIsSet = false;
bool destinationSizeIsSet = false;
bool inputIsSet = false;
bool opaqueIsSet = false;
bool bufferIsSet = false;
......@@ -84,6 +91,10 @@ public:
QPointF invertBufferTransform(const State *state, const QPointF &point) const;
QSizeF bufferTransform(const State *state, const QSizeF &size) const;
QSizeF invertBufferTransform(const State *state, const QSizeF &size) const;
QPointF viewportTransform(const State *state, const QPointF &point) const;
QPointF invertViewportTransform(const State *state, const QPointF &point) const;
QSizeF viewportTransform(const State *state, const QSizeF &size) const;
QSizeF invertViewportTransform(const State *state, const QSizeF &size) const;
SurfaceRole *role = nullptr;
......@@ -105,7 +116,7 @@ public:
QPointer<ConfinedPointerInterface> confinedPointer;
QHash<OutputInterface*, QMetaObject::Connection> outputDestroyedConnections;
QVector<IdleInhibitorInterface*> idleInhibitors;
ViewportInterface *viewportExtension = nullptr;
SurfaceInterface *dataProxy = nullptr;
private:
......
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "viewporter_interface.h"
#include "display.h"
#include "surface_interface_p.h"
#include "viewporter_interface_p.h"
static const int s_version = 1;
namespace KWaylandServer
{
class ViewporterInterfacePrivate : public QtWaylandServer::wp_viewporter
{
protected:
void wp_viewporter_destroy(Resource *resource) override;
void wp_viewporter_get_viewport(Resource *resource, uint32_t id, struct ::wl_resource *surface) override;
};