summaryrefslogtreecommitdiffstats
path: root/xbmc/windowing/wayland
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 18:07:22 +0000
commitc04dcc2e7d834218ef2d4194331e383402495ae1 (patch)
tree7333e38d10d75386e60f336b80c2443c1166031d /xbmc/windowing/wayland
parentInitial commit. (diff)
downloadkodi-c04dcc2e7d834218ef2d4194331e383402495ae1.tar.xz
kodi-c04dcc2e7d834218ef2d4194331e383402495ae1.zip
Adding upstream version 2:20.4+dfsg.upstream/2%20.4+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xbmc/windowing/wayland')
-rw-r--r--xbmc/windowing/wayland/CMakeLists.txt67
-rw-r--r--xbmc/windowing/wayland/Connection.cpp39
-rw-r--r--xbmc/windowing/wayland/Connection.h39
-rw-r--r--xbmc/windowing/wayland/InputProcessorKeyboard.cpp170
-rw-r--r--xbmc/windowing/wayland/InputProcessorKeyboard.h78
-rw-r--r--xbmc/windowing/wayland/InputProcessorPointer.cpp136
-rw-r--r--xbmc/windowing/wayland/InputProcessorPointer.h76
-rw-r--r--xbmc/windowing/wayland/InputProcessorTouch.cpp130
-rw-r--r--xbmc/windowing/wayland/InputProcessorTouch.h82
-rw-r--r--xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.cpp52
-rw-r--r--xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.h39
-rw-r--r--xbmc/windowing/wayland/OptionalsReg.cpp136
-rw-r--r--xbmc/windowing/wayland/OptionalsReg.h37
-rw-r--r--xbmc/windowing/wayland/Output.cpp145
-rw-r--r--xbmc/windowing/wayland/Output.h152
-rw-r--r--xbmc/windowing/wayland/Registry.cpp164
-rw-r--r--xbmc/windowing/wayland/Registry.h162
-rw-r--r--xbmc/windowing/wayland/Seat.cpp275
-rw-r--r--xbmc/windowing/wayland/Seat.h203
-rw-r--r--xbmc/windowing/wayland/SeatInputProcessing.cpp83
-rw-r--r--xbmc/windowing/wayland/SeatInputProcessing.h135
-rw-r--r--xbmc/windowing/wayland/SeatSelection.cpp199
-rw-r--r--xbmc/windowing/wayland/SeatSelection.h49
-rw-r--r--xbmc/windowing/wayland/ShellSurface.cpp35
-rw-r--r--xbmc/windowing/wayland/ShellSurface.h92
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceWlShell.cpp101
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceWlShell.h63
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceXdgShell.cpp151
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceXdgShell.h73
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp152
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h77
-rw-r--r--xbmc/windowing/wayland/Signals.h151
-rw-r--r--xbmc/windowing/wayland/Util.cpp88
-rw-r--r--xbmc/windowing/wayland/Util.h54
-rw-r--r--xbmc/windowing/wayland/VideoSyncWpPresentation.cpp83
-rw-r--r--xbmc/windowing/wayland/VideoSyncWpPresentation.h47
-rw-r--r--xbmc/windowing/wayland/WinEventsWayland.cpp277
-rw-r--r--xbmc/windowing/wayland/WinEventsWayland.h49
-rw-r--r--xbmc/windowing/wayland/WinSystemWayland.cpp1568
-rw-r--r--xbmc/windowing/wayland/WinSystemWayland.h300
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContext.cpp150
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContext.h55
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.cpp125
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.h47
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.cpp114
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.h47
-rw-r--r--xbmc/windowing/wayland/WindowDecorationHandler.h41
-rw-r--r--xbmc/windowing/wayland/WindowDecorator.cpp1043
-rw-r--r--xbmc/windowing/wayland/WindowDecorator.h262
-rw-r--r--xbmc/windowing/wayland/XkbcommonKeymap.cpp333
-rw-r--r--xbmc/windowing/wayland/XkbcommonKeymap.h140
51 files changed, 8366 insertions, 0 deletions
diff --git a/xbmc/windowing/wayland/CMakeLists.txt b/xbmc/windowing/wayland/CMakeLists.txt
new file mode 100644
index 0000000..5628ed8
--- /dev/null
+++ b/xbmc/windowing/wayland/CMakeLists.txt
@@ -0,0 +1,67 @@
+# from ArchSetup.cmake
+set_source_files_properties(${WAYLAND_EXTRA_PROTOCOL_GENERATED_DIR}/wayland-extra-protocols.cpp
+ ${WAYLAND_EXTRA_PROTOCOL_GENERATED_DIR}/wayland-extra-protocols.hpp
+ PROPERTIES GENERATED TRUE)
+
+set(SOURCES Connection.cpp
+ OptionalsReg.cpp
+ Output.cpp
+ InputProcessorKeyboard.h
+ InputProcessorPointer.h
+ InputProcessorTouch.h
+ OSScreenSaverIdleInhibitUnstableV1.cpp
+ Registry.cpp
+ Seat.cpp
+ SeatInputProcessing.cpp
+ SeatSelection.cpp
+ ShellSurface.cpp
+ ShellSurfaceWlShell.cpp
+ ShellSurfaceXdgShell.cpp
+ ShellSurfaceXdgShellUnstableV6.cpp
+ Util.cpp
+ VideoSyncWpPresentation.cpp
+ ${WAYLAND_EXTRA_PROTOCOL_GENERATED_DIR}/wayland-extra-protocols.cpp
+ WindowDecorator.cpp
+ WinEventsWayland.cpp
+ WinSystemWayland.cpp
+ XkbcommonKeymap.cpp)
+
+set(HEADERS Connection.h
+ OptionalsReg.h
+ Output.h
+ InputProcessorKeyboard.cpp
+ InputProcessorPointer.cpp
+ InputProcessorTouch.cpp
+ OSScreenSaverIdleInhibitUnstableV1.h
+ Registry.h
+ Seat.h
+ SeatInputProcessing.h
+ SeatSelection.h
+ ShellSurface.h
+ ShellSurfaceWlShell.h
+ ShellSurfaceXdgShell.h
+ ShellSurfaceXdgShellUnstableV6.h
+ Signals.h
+ VideoSyncWpPresentation.h
+ ${WAYLAND_EXTRA_PROTOCOL_GENERATED_DIR}/wayland-extra-protocols.hpp
+ WindowDecorator.h
+ WinEventsWayland.h
+ WinSystemWayland.h
+ XkbcommonKeymap.h)
+
+if(EGL_FOUND)
+ list(APPEND SOURCES WinSystemWaylandEGLContext.cpp)
+ list(APPEND HEADERS WinSystemWaylandEGLContext.h)
+endif()
+
+if(OPENGL_FOUND)
+ list(APPEND SOURCES WinSystemWaylandEGLContextGL.cpp)
+ list(APPEND HEADERS WinSystemWaylandEGLContextGL.h)
+endif()
+if(OPENGLES_FOUND)
+ list(APPEND SOURCES WinSystemWaylandEGLContextGLES.cpp)
+ list(APPEND HEADERS WinSystemWaylandEGLContextGLES.h)
+endif()
+
+
+core_add_library(windowing_WAYLAND)
diff --git a/xbmc/windowing/wayland/Connection.cpp b/xbmc/windowing/wayland/Connection.cpp
new file mode 100644
index 0000000..9d45be6
--- /dev/null
+++ b/xbmc/windowing/wayland/Connection.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "Connection.h"
+
+#include "utils/log.h"
+
+#include <cassert>
+#include <stdexcept>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+CConnection::CConnection()
+{
+ try
+ {
+ m_display = std::make_unique<wayland::display_t>();
+ }
+ catch (const std::exception& err)
+ {
+ CLog::Log(LOGERROR, "Wayland connection error: {}", err.what());
+ }
+}
+
+bool CConnection::HasDisplay() const
+{
+ return static_cast<bool>(m_display);
+}
+
+wayland::display_t& CConnection::GetDisplay()
+{
+ assert(m_display);
+ return *m_display;
+}
diff --git a/xbmc/windowing/wayland/Connection.h b/xbmc/windowing/wayland/Connection.h
new file mode 100644
index 0000000..b1badd4
--- /dev/null
+++ b/xbmc/windowing/wayland/Connection.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <wayland-client.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Connection to Wayland compositor
+ */
+class CConnection
+{
+public:
+ CConnection();
+
+ bool HasDisplay() const;
+ wayland::display_t& GetDisplay();
+
+private:
+ std::unique_ptr<wayland::display_t> m_display;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/InputProcessorKeyboard.cpp b/xbmc/windowing/wayland/InputProcessorKeyboard.cpp
new file mode 100644
index 0000000..37b0240
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorKeyboard.cpp
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "InputProcessorKeyboard.h"
+
+#include "utils/log.h"
+
+#include <cassert>
+#include <limits>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+// Offset between keyboard codes of Wayland (effectively evdev) and xkb_keycode_t
+constexpr int WL_KEYBOARD_XKB_CODE_OFFSET{8};
+}
+
+CInputProcessorKeyboard::CInputProcessorKeyboard(IInputHandlerKeyboard& handler)
+: m_handler{handler}, m_keyRepeatTimer{std::bind(&CInputProcessorKeyboard::KeyRepeatTimeout, this)}
+{
+}
+
+void CInputProcessorKeyboard::OnKeyboardKeymap(CSeat* seat, wayland::keyboard_keymap_format format, std::string const &keymap)
+{
+ if (format != wayland::keyboard_keymap_format::xkb_v1)
+ {
+ CLog::Log(LOGWARNING,
+ "Wayland compositor sent keymap in format {}, but we only understand xkbv1 - "
+ "keyboard input will not work",
+ static_cast<unsigned int>(format));
+ return;
+ }
+
+ m_keyRepeatTimer.Stop();
+
+ try
+ {
+ if (!m_xkbContext)
+ {
+ // Lazily initialize XkbcommonContext
+ m_xkbContext.reset(new CXkbcommonContext);
+ }
+
+ m_keymap = m_xkbContext->KeymapFromString(keymap);
+ }
+ catch(std::exception const& e)
+ {
+ CLog::Log(LOGERROR, "Could not parse keymap from compositor: {} - continuing without keymap",
+ e.what());
+ }
+}
+
+void CInputProcessorKeyboard::OnKeyboardEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ const wayland::array_t& keys)
+{
+ m_handler.OnKeyboardEnter();
+}
+
+void CInputProcessorKeyboard::OnKeyboardLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface)
+{
+ m_keyRepeatTimer.Stop();
+ m_handler.OnKeyboardLeave();
+}
+
+void CInputProcessorKeyboard::OnKeyboardKey(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t key, wayland::keyboard_key_state state)
+{
+ if (!m_keymap)
+ {
+ CLog::Log(LOGWARNING, "Key event for code {} without valid keymap, ignoring", key);
+ return;
+ }
+
+ ConvertAndSendKey(key, state == wayland::keyboard_key_state::pressed);
+}
+
+void CInputProcessorKeyboard::OnKeyboardModifiers(CSeat* seat, std::uint32_t serial, std::uint32_t modsDepressed, std::uint32_t modsLatched, std::uint32_t modsLocked, std::uint32_t group)
+{
+ if (!m_keymap)
+ {
+ CLog::Log(LOGWARNING, "Modifier event without valid keymap, ignoring");
+ return;
+ }
+
+ m_keyRepeatTimer.Stop();
+ m_keymap->UpdateMask(modsDepressed, modsLatched, modsLocked, group);
+}
+
+void CInputProcessorKeyboard::OnKeyboardRepeatInfo(CSeat* seat, std::int32_t rate, std::int32_t delay)
+{
+ CLog::Log(LOGDEBUG, "Key repeat rate: {} cps, delay {} ms", rate, delay);
+ // rate is in characters per second, so convert to msec interval
+ m_keyRepeatInterval = (rate != 0) ? static_cast<int> (1000.0f / rate) : 0;
+ m_keyRepeatDelay = delay;
+}
+
+void CInputProcessorKeyboard::ConvertAndSendKey(std::uint32_t scancode, bool pressed)
+{
+ std::uint32_t xkbCode{scancode + WL_KEYBOARD_XKB_CODE_OFFSET};
+ XBMCKey xbmcKey{m_keymap->XBMCKeyForKeycode(xkbCode)};
+ std::uint32_t utf32{m_keymap->UnicodeCodepointForKeycode(xkbCode)};
+
+ if (utf32 > std::numeric_limits<std::uint16_t>::max())
+ {
+ // Kodi event system only supports UTF16, so ignore the codepoint if
+ // it does not fit
+ utf32 = 0;
+ }
+ if (scancode > std::numeric_limits<unsigned char>::max())
+ {
+ // Kodi scancodes are limited to unsigned char, pretend the scancode is unknown
+ // on overflow
+ scancode = 0;
+ }
+
+ XBMC_Event event{SendKey(scancode, xbmcKey, static_cast<std::uint16_t> (utf32), pressed)};
+
+ if (pressed && m_keymap->ShouldKeycodeRepeat(xkbCode) && m_keyRepeatInterval > 0)
+ {
+ // Can't modify keyToRepeat until we're sure the thread isn't accessing it
+ m_keyRepeatTimer.Stop(true);
+ // Update/Set key
+ m_keyToRepeat = event;
+ // Start timer with initial delay
+ m_keyRepeatTimer.Start(std::chrono::milliseconds(m_keyRepeatDelay), false);
+ }
+ else
+ {
+ m_keyRepeatTimer.Stop();
+ }
+}
+
+XBMC_Event CInputProcessorKeyboard::SendKey(unsigned char scancode, XBMCKey key, std::uint16_t unicodeCodepoint, bool pressed)
+{
+ assert(m_keymap);
+
+ XBMC_Event event{};
+ event.type = pressed ? XBMC_KEYDOWN : XBMC_KEYUP;
+ event.key.keysym =
+ {
+ .scancode = scancode,
+ .sym = key,
+ .mod = m_keymap->ActiveXBMCModifiers(),
+ .unicode = unicodeCodepoint
+ };
+ m_handler.OnKeyboardEvent(event);
+ // Return created event for convenience (key repeat)
+ return event;
+}
+
+void CInputProcessorKeyboard::KeyRepeatTimeout()
+{
+ // Reset ourselves
+ m_keyRepeatTimer.RestartAsync(std::chrono::milliseconds(m_keyRepeatInterval));
+ // Simulate repeat: Key up and down
+ XBMC_Event event = m_keyToRepeat;
+ event.type = XBMC_KEYUP;
+ m_handler.OnKeyboardEvent(event);
+ event.type = XBMC_KEYDOWN;
+ m_handler.OnKeyboardEvent(event);
+}
diff --git a/xbmc/windowing/wayland/InputProcessorKeyboard.h b/xbmc/windowing/wayland/InputProcessorKeyboard.h
new file mode 100644
index 0000000..73a37ce
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorKeyboard.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Seat.h"
+#include "XkbcommonKeymap.h"
+#include "input/XBMC_keysym.h"
+#include "threads/Timer.h"
+#include "windowing/XBMC_events.h"
+
+#include <atomic>
+#include <cstdint>
+#include <memory>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class IInputHandlerKeyboard
+{
+public:
+ virtual void OnKeyboardEnter() {}
+ virtual void OnKeyboardLeave() {}
+ virtual void OnKeyboardEvent(XBMC_Event& event) = 0;
+ virtual ~IInputHandlerKeyboard() = default;
+};
+
+class CInputProcessorKeyboard final : public IRawInputHandlerKeyboard
+{
+public:
+ CInputProcessorKeyboard(IInputHandlerKeyboard& handler);
+
+ void OnKeyboardKeymap(CSeat* seat, wayland::keyboard_keymap_format format, std::string const& keymap) override;
+ void OnKeyboardEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ const wayland::array_t& keys) override;
+ void OnKeyboardLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface) override;
+ void OnKeyboardKey(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t key, wayland::keyboard_key_state state) override;
+ void OnKeyboardModifiers(CSeat* seat, std::uint32_t serial, std::uint32_t modsDepressed, std::uint32_t modsLatched, std::uint32_t modsLocked, std::uint32_t group) override;
+ void OnKeyboardRepeatInfo(CSeat* seat, std::int32_t rate, std::int32_t delay) override;
+
+private:
+ CInputProcessorKeyboard(CInputProcessorKeyboard const& other) = delete;
+ CInputProcessorKeyboard& operator=(CInputProcessorKeyboard const& other) = delete;
+
+ void ConvertAndSendKey(std::uint32_t scancode, bool pressed);
+ XBMC_Event SendKey(unsigned char scancode, XBMCKey key, std::uint16_t unicodeCodepoint, bool pressed);
+ void KeyRepeatTimeout();
+
+ IInputHandlerKeyboard& m_handler;
+
+ std::unique_ptr<CXkbcommonContext> m_xkbContext;
+ std::unique_ptr<CXkbcommonKeymap> m_keymap;
+ // Default values are used if compositor does not send any
+ std::atomic<int> m_keyRepeatDelay{1000};
+ std::atomic<int> m_keyRepeatInterval{50};
+ // Save complete XBMC_Event so no keymap lookups which might not be thread-safe
+ // are needed in the repeat callback
+ XBMC_Event m_keyToRepeat;
+
+ CTimer m_keyRepeatTimer;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/InputProcessorPointer.cpp b/xbmc/windowing/wayland/InputProcessorPointer.cpp
new file mode 100644
index 0000000..6a2fe04
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorPointer.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "InputProcessorPointer.h"
+
+#include "input/mouse/MouseStat.h"
+
+#include <cmath>
+
+#include <linux/input-event-codes.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+int WaylandToXbmcButton(std::uint32_t button)
+{
+ // Wayland button is evdev code
+ switch (button)
+ {
+ case BTN_LEFT:
+ return XBMC_BUTTON_LEFT;
+ case BTN_MIDDLE:
+ return XBMC_BUTTON_MIDDLE;
+ case BTN_RIGHT:
+ return XBMC_BUTTON_RIGHT;
+ default:
+ return -1;
+ }
+}
+
+}
+
+CInputProcessorPointer::CInputProcessorPointer(wayland::surface_t const& surface, IInputHandlerPointer& handler)
+: m_surface{surface}, m_handler{handler}
+{
+}
+
+void CInputProcessorPointer::OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY)
+{
+ if (surface == m_surface)
+ {
+ m_pointerOnSurface = true;
+ m_handler.OnPointerEnter(seat->GetGlobalName(), serial);
+ SetMousePosFromSurface({surfaceX, surfaceY});
+ SendMouseMotion();
+ }
+}
+
+void CInputProcessorPointer::OnPointerLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface)
+{
+ if (m_pointerOnSurface)
+ {
+ m_handler.OnPointerLeave();
+ m_pointerOnSurface = false;
+ }
+}
+
+void CInputProcessorPointer::OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY)
+{
+ if (m_pointerOnSurface)
+ {
+ SetMousePosFromSurface({surfaceX, surfaceY});
+ SendMouseMotion();
+ }
+}
+
+void CInputProcessorPointer::OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state)
+{
+ if (m_pointerOnSurface)
+ {
+ int xbmcButton = WaylandToXbmcButton(button);
+ if (xbmcButton < 0)
+ {
+ // Button is unmapped
+ return;
+ }
+
+ bool pressed = (state == wayland::pointer_button_state::pressed);
+ SendMouseButton(xbmcButton, pressed);
+ }
+}
+
+void CInputProcessorPointer::OnPointerAxis(CSeat* seat, std::uint32_t time, wayland::pointer_axis axis, double value)
+{
+ if (m_pointerOnSurface)
+ {
+ // For axis events we only care about the vector direction
+ // and not the scalar magnitude. Every axis event callback
+ // generates one scroll button event for XBMC
+
+ // Negative is up
+ auto xbmcButton = static_cast<unsigned char> ((value < 0.0) ? XBMC_BUTTON_WHEELUP : XBMC_BUTTON_WHEELDOWN);
+ // Simulate a single click of the wheel-equivalent "button"
+ SendMouseButton(xbmcButton, true);
+ SendMouseButton(xbmcButton, false);
+ }
+}
+
+std::uint16_t CInputProcessorPointer::ConvertMouseCoordinate(double coord) const
+{
+ return static_cast<std::uint16_t> (std::round(coord * m_coordinateScale));
+}
+
+void CInputProcessorPointer::SetMousePosFromSurface(CPointGen<double> position)
+{
+ m_pointerPosition = {ConvertMouseCoordinate(position.x), ConvertMouseCoordinate(position.y)};
+}
+
+void CInputProcessorPointer::SendMouseMotion()
+{
+ XBMC_Event event{};
+ event.type = XBMC_MOUSEMOTION;
+ event.motion = {m_pointerPosition.x, m_pointerPosition.y};
+ m_handler.OnPointerEvent(event);
+}
+
+void CInputProcessorPointer::SendMouseButton(unsigned char button, bool pressed)
+{
+ XBMC_Event event{};
+ event.type = pressed ? XBMC_MOUSEBUTTONDOWN : XBMC_MOUSEBUTTONUP;
+ event.button = {button, m_pointerPosition.x, m_pointerPosition.y};
+ m_handler.OnPointerEvent(event);
+}
diff --git a/xbmc/windowing/wayland/InputProcessorPointer.h b/xbmc/windowing/wayland/InputProcessorPointer.h
new file mode 100644
index 0000000..ce44601
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorPointer.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Seat.h"
+#include "input/XBMC_keysym.h"
+#include "utils/Geometry.h"
+#include "windowing/XBMC_events.h"
+
+#include <cstdint>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class IInputHandlerPointer
+{
+public:
+ virtual void OnPointerEnter(std::uint32_t seatGlobalName, std::uint32_t serial) {}
+ virtual void OnPointerLeave() {}
+ virtual void OnPointerEvent(XBMC_Event& event) = 0;
+protected:
+ ~IInputHandlerPointer() = default;
+};
+
+class CInputProcessorPointer final : public IRawInputHandlerPointer
+{
+public:
+ CInputProcessorPointer(wayland::surface_t const& surface, IInputHandlerPointer& handler);
+ void SetCoordinateScale(std::int32_t scale) { m_coordinateScale = scale; }
+
+ void OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY) override;
+ void OnPointerLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface) override;
+ void OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY) override;
+ void OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state) override;
+ void OnPointerAxis(CSeat* seat, std::uint32_t time, wayland::pointer_axis axis, double value) override;
+
+private:
+ CInputProcessorPointer(CInputProcessorPointer const& other) = delete;
+ CInputProcessorPointer& operator=(CInputProcessorPointer const& other) = delete;
+
+ std::uint16_t ConvertMouseCoordinate(double coord) const;
+ void SetMousePosFromSurface(CPointGen<double> position);
+ void SendMouseMotion();
+ void SendMouseButton(unsigned char button, bool pressed);
+
+ wayland::surface_t m_surface;
+ IInputHandlerPointer& m_handler;
+
+ bool m_pointerOnSurface{};
+
+ // Pointer position in *scaled* coordinates
+ CPointGen<std::uint16_t> m_pointerPosition;
+ std::int32_t m_coordinateScale{1};
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/InputProcessorTouch.cpp b/xbmc/windowing/wayland/InputProcessorTouch.cpp
new file mode 100644
index 0000000..a664e32
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorTouch.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "InputProcessorTouch.h"
+
+#include "input/touch/generic/GenericTouchInputHandler.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+CInputProcessorTouch::CInputProcessorTouch(wayland::surface_t const& surface)
+: m_surface{surface}
+{
+}
+
+void CInputProcessorTouch::OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y)
+{
+ if (surface != m_surface)
+ {
+ return;
+ }
+
+ // Find free Kodi pointer number
+ int kodiPointer{-1};
+ // Not optimal, but irrelevant for the small number of iterations
+ for (int testPointer{0}; testPointer < CGenericTouchInputHandler::MAX_POINTERS; testPointer++)
+ {
+ if (std::all_of(m_touchPoints.cbegin(), m_touchPoints.cend(),
+ [=](decltype(m_touchPoints)::value_type const& pair)
+ {
+ return (pair.second.kodiPointerNumber != testPointer);
+ }))
+ {
+ kodiPointer = testPointer;
+ break;
+ }
+ }
+
+ if (kodiPointer != -1)
+ {
+ auto it = m_touchPoints.emplace(std::piecewise_construct, std::forward_as_tuple(id), std::forward_as_tuple(time, kodiPointer, x * m_coordinateScale, y * m_coordinateScale, 0.0f)).first;
+ SendTouchPointEvent(TouchInputDown, it->second);
+ }
+}
+
+void CInputProcessorTouch::OnTouchUp(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::int32_t id)
+{
+ auto it = m_touchPoints.find(id);
+ if (it != m_touchPoints.end())
+ {
+ auto& point = it->second;
+ point.lastEventTime = time;
+ SendTouchPointEvent(TouchInputUp, point);
+ m_touchPoints.erase(it);
+ }
+}
+
+void CInputProcessorTouch::OnTouchMotion(CSeat* seat, std::uint32_t time, std::int32_t id, double x, double y)
+{
+ auto it = m_touchPoints.find(id);
+ if (it != m_touchPoints.end())
+ {
+ auto& point = it->second;
+ point.x = x * m_coordinateScale;
+ point.y = y * m_coordinateScale;
+ point.lastEventTime = time;
+ SendTouchPointEvent(TouchInputMove, point);
+ }
+}
+
+void CInputProcessorTouch::OnTouchCancel(CSeat* seat)
+{
+ AbortTouches();
+}
+
+void CInputProcessorTouch::OnTouchShape(CSeat* seat, std::int32_t id, double major, double minor)
+{
+ auto it = m_touchPoints.find(id);
+ if (it != m_touchPoints.end())
+ {
+ auto& point = it->second;
+ // Kodi only supports size without shape, so use average of both axes
+ point.size = ((major + minor) / 2.0) * m_coordinateScale;
+ UpdateTouchPoint(point);
+ }
+}
+
+CInputProcessorTouch::~CInputProcessorTouch() noexcept
+{
+ AbortTouches();
+}
+
+void CInputProcessorTouch::AbortTouches()
+{
+ // TouchInputAbort aborts for all pointers, so it does not matter which is specified
+ if (!m_touchPoints.empty())
+ {
+ SendTouchPointEvent(TouchInputAbort, m_touchPoints.begin()->second);
+ }
+ m_touchPoints.clear();
+}
+
+void CInputProcessorTouch::SendTouchPointEvent(TouchInput event, const TouchPoint& point)
+{
+ if (event == TouchInputMove)
+ {
+ for (auto const& point : m_touchPoints)
+ {
+ // Contrary to the docs, this must be called before HandleTouchInput or the
+ // position will not be updated and gesture detection will not work
+ UpdateTouchPoint(point.second);
+ }
+ }
+ CGenericTouchInputHandler::GetInstance().HandleTouchInput(event, point.x, point.y, point.lastEventTime * INT64_C(1000000), point.kodiPointerNumber, point.size);
+}
+
+void CInputProcessorTouch::UpdateTouchPoint(const TouchPoint& point)
+{
+ CGenericTouchInputHandler::GetInstance().UpdateTouchPointer(point.kodiPointerNumber, point.x, point.y, point.lastEventTime * INT64_C(1000000), point.size);
+}
diff --git a/xbmc/windowing/wayland/InputProcessorTouch.h b/xbmc/windowing/wayland/InputProcessorTouch.h
new file mode 100644
index 0000000..fa6f606
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorTouch.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Seat.h"
+#include "input/touch/ITouchInputHandler.h"
+
+#include <cstdint>
+#include <map>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Touch input processor
+ *
+ * Events go directly to \ref CGenericTouchInputHandler, so no callbacks here
+ */
+class CInputProcessorTouch final : public IRawInputHandlerTouch
+{
+public:
+ CInputProcessorTouch(wayland::surface_t const& surface);
+ ~CInputProcessorTouch() noexcept;
+ void SetCoordinateScale(std::int32_t scale) { m_coordinateScale = scale; }
+
+ void OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y) override;
+ void OnTouchUp(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::int32_t id) override;
+ void OnTouchMotion(CSeat* seat, std::uint32_t time, std::int32_t id, double x, double y) override;
+ void OnTouchCancel(CSeat* seat) override;
+ void OnTouchShape(CSeat* seat, std::int32_t id, double major, double minor) override;
+
+private:
+ CInputProcessorTouch(CInputProcessorTouch const& other) = delete;
+ CInputProcessorTouch& operator=(CInputProcessorTouch const& other) = delete;
+
+ struct TouchPoint
+ {
+ std::uint32_t lastEventTime;
+ /// Pointer number passed to \ref ITouchInputHandler
+ std::int32_t kodiPointerNumber;
+ /**
+ * Last coordinates - needed for TouchInputUp events where Wayland does not
+ * send new coordinates but Kodi needs them anyway
+ */
+ float x, y, size;
+ TouchPoint(std::uint32_t initialEventTime, std::int32_t kodiPointerNumber, float x, float y, float size)
+ : lastEventTime{initialEventTime}, kodiPointerNumber{kodiPointerNumber}, x{x}, y{y}, size{size}
+ {}
+ };
+
+ void SendTouchPointEvent(TouchInput event, TouchPoint const& point);
+ void UpdateTouchPoint(TouchPoint const& point);
+ void AbortTouches();
+
+ wayland::surface_t m_surface;
+ std::int32_t m_coordinateScale{1};
+
+ /// Map of wl_touch point id to data
+ std::map<std::int32_t, TouchPoint> m_touchPoints;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.cpp b/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.cpp
new file mode 100644
index 0000000..32dbbc6
--- /dev/null
+++ b/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "OSScreenSaverIdleInhibitUnstableV1.h"
+
+#include "Registry.h"
+
+#include <cassert>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+COSScreenSaverIdleInhibitUnstableV1* COSScreenSaverIdleInhibitUnstableV1::TryCreate(CConnection& connection, wayland::surface_t const& inhibitSurface)
+{
+ wayland::zwp_idle_inhibit_manager_v1_t manager;
+ CRegistry registry{connection};
+ registry.RequestSingleton(manager, 1, 1, false);
+ registry.Bind();
+
+ if (manager)
+ {
+ return new COSScreenSaverIdleInhibitUnstableV1(manager, inhibitSurface);
+ }
+ else
+ {
+ return nullptr;
+ }
+}
+
+COSScreenSaverIdleInhibitUnstableV1::COSScreenSaverIdleInhibitUnstableV1(const wayland::zwp_idle_inhibit_manager_v1_t& manager, const wayland::surface_t& inhibitSurface)
+: m_manager{manager}, m_surface{inhibitSurface}
+{
+ assert(m_manager);
+ assert(m_surface);
+}
+
+void COSScreenSaverIdleInhibitUnstableV1::Inhibit()
+{
+ if (!m_inhibitor)
+ {
+ m_inhibitor = m_manager.create_inhibitor(m_surface);
+ }
+}
+
+void COSScreenSaverIdleInhibitUnstableV1::Uninhibit()
+{
+ m_inhibitor.proxy_release();
+}
diff --git a/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.h b/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.h
new file mode 100644
index 0000000..0303d60
--- /dev/null
+++ b/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "../OSScreenSaver.h"
+#include "Connection.h"
+
+#include <wayland-extra-protocols.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class COSScreenSaverIdleInhibitUnstableV1 : public IOSScreenSaver
+{
+public:
+ COSScreenSaverIdleInhibitUnstableV1(wayland::zwp_idle_inhibit_manager_v1_t const& manager, wayland::surface_t const& inhibitSurface);
+ static COSScreenSaverIdleInhibitUnstableV1* TryCreate(CConnection& connection, wayland::surface_t const& inhibitSurface);
+ void Inhibit() override;
+ void Uninhibit() override;
+
+private:
+ wayland::zwp_idle_inhibit_manager_v1_t m_manager;
+ wayland::zwp_idle_inhibitor_v1_t m_inhibitor;
+ wayland::surface_t m_surface;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/OptionalsReg.cpp b/xbmc/windowing/wayland/OptionalsReg.cpp
new file mode 100644
index 0000000..5b456eb
--- /dev/null
+++ b/xbmc/windowing/wayland/OptionalsReg.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "OptionalsReg.h"
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+#if defined(HAVE_LIBVA) && defined(HAS_EGL)
+#include <va/va_wayland.h>
+#include "cores/VideoPlayer/DVDCodecs/Video/VAAPI.h"
+#if defined(HAS_GL)
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.h"
+#endif
+#if defined(HAS_GLES)
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.h"
+#endif
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CVaapiProxy : public VAAPI::IVaapiWinSystem
+{
+public:
+ CVaapiProxy() = default;
+ virtual ~CVaapiProxy() = default;
+ VADisplay GetVADisplay() override { return vaGetDisplayWl(dpy); };
+ void *GetEGLDisplay() override { return eglDisplay; };
+
+ wl_display *dpy;
+ void *eglDisplay;
+};
+
+CVaapiProxy* VaapiProxyCreate()
+{
+ return new CVaapiProxy();
+}
+
+void VaapiProxyDelete(CVaapiProxy* proxy)
+{
+ delete proxy;
+}
+
+void VaapiProxyConfig(CVaapiProxy* proxy, void* dpy, void* eglDpy)
+{
+ proxy->dpy = static_cast<wl_display*>(dpy);
+ proxy->eglDisplay = eglDpy;
+}
+
+void VAAPIRegister(CVaapiProxy* winSystem, bool deepColor)
+{
+ VAAPI::CDecoder::Register(winSystem, deepColor);
+}
+
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+ EGLDisplay eglDpy = winSystem->eglDisplay;
+ VADisplay vaDpy = vaGetDisplayWl(winSystem->dpy);
+ CRendererVAAPIGL::Register(winSystem, vaDpy, eglDpy, general, deepColor);
+}
+#endif
+
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+ EGLDisplay eglDpy = winSystem->eglDisplay;
+ VADisplay vaDpy = vaGetDisplayWl(winSystem->dpy);
+ CRendererVAAPIGLES::Register(winSystem, vaDpy, eglDpy, general, deepColor);
+}
+#endif
+
+} // namespace WAYLAND
+} // namespace WINDOWING
+} // namespace KODI
+
+#else
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CVaapiProxy
+{
+};
+
+CVaapiProxy* VaapiProxyCreate()
+{
+ return nullptr;
+}
+
+void VaapiProxyDelete(CVaapiProxy* proxy)
+{
+}
+
+void VaapiProxyConfig(CVaapiProxy* proxy, void* dpy, void* eglDpy)
+{
+
+}
+
+void VAAPIRegister(CVaapiProxy* winSystem, bool deepColor)
+{
+
+}
+
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+
+}
+#endif
+
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+}
+#endif
+
+} // namespace WAYLAND
+} // namespace WINDOWING
+} // namespace KODI
+
+#endif
+
diff --git a/xbmc/windowing/wayland/OptionalsReg.h b/xbmc/windowing/wayland/OptionalsReg.h
new file mode 100644
index 0000000..293c9c3
--- /dev/null
+++ b/xbmc/windowing/wayland/OptionalsReg.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+class CVaapiProxy;
+
+CVaapiProxy* VaapiProxyCreate();
+void VaapiProxyDelete(CVaapiProxy *proxy);
+void VaapiProxyConfig(CVaapiProxy *proxy, void *dpy, void *eglDpy);
+void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor);
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor);
+#endif
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor);
+#endif
+
+} // namespace WAYLAND
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/wayland/Output.cpp b/xbmc/windowing/wayland/Output.cpp
new file mode 100644
index 0000000..5dd840a
--- /dev/null
+++ b/xbmc/windowing/wayland/Output.cpp
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "Output.h"
+
+#include <cassert>
+#include <cmath>
+#include <mutex>
+#include <set>
+#include <stdexcept>
+#include <utility>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+COutput::COutput(std::uint32_t globalName,
+ wayland::output_t const& output,
+ std::function<void()> doneHandler)
+ : m_globalName{globalName}, m_output{output}, m_doneHandler{std::move(doneHandler)}
+{
+ assert(m_output);
+
+ m_output.on_geometry() = [this](std::int32_t x, std::int32_t y, std::int32_t physWidth,
+ std::int32_t physHeight, wayland::output_subpixel,
+ std::string const& make, std::string const& model,
+ const wayland::output_transform&)
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ m_position = {x, y};
+ // Some monitors report invalid (negative) values that would cause an exception
+ // with CSizeInt
+ if (physWidth < 0 || physHeight < 0)
+ m_physicalSize = {};
+ else
+ m_physicalSize = {physWidth, physHeight};
+ m_make = make;
+ m_model = model;
+ };
+ m_output.on_mode() = [this](const wayland::output_mode& flags, std::int32_t width,
+ std::int32_t height, std::int32_t refresh) {
+ // std::set.emplace returns pair of iterator to the (possibly) inserted
+ // element and boolean information whether the element was actually added
+ // which we do not need
+ auto modeIterator = m_modes.emplace(CSizeInt{width, height}, refresh).first;
+ std::unique_lock<CCriticalSection> lock(m_iteratorCriticalSection);
+ // Remember current and preferred mode
+ // Current mode is the last one that was sent with current flag set
+ if (flags & wayland::output_mode::current)
+ {
+ m_currentMode = modeIterator;
+ }
+ if (flags & wayland::output_mode::preferred)
+ {
+ m_preferredMode = modeIterator;
+ }
+ };
+ m_output.on_scale() = [this](std::int32_t scale)
+ {
+ m_scale = scale;
+ };
+
+ m_output.on_done() = [this]()
+ {
+ m_doneHandler();
+ };
+}
+
+COutput::~COutput() noexcept
+{
+ // Reset event handlers - someone might still hold a reference to the output_t,
+ // causing events to be dispatched. They should not go to a deleted class.
+ m_output.on_geometry() = nullptr;
+ m_output.on_mode() = nullptr;
+ m_output.on_done() = nullptr;
+ m_output.on_scale() = nullptr;
+}
+
+const COutput::Mode& COutput::GetCurrentMode() const
+{
+ std::unique_lock<CCriticalSection> lock(m_iteratorCriticalSection);
+ if (m_currentMode == m_modes.end())
+ {
+ throw std::runtime_error("Current mode not set");
+ }
+ return *m_currentMode;
+}
+
+const COutput::Mode& COutput::GetPreferredMode() const
+{
+ std::unique_lock<CCriticalSection> lock(m_iteratorCriticalSection);
+ if (m_preferredMode == m_modes.end())
+ {
+ throw std::runtime_error("Preferred mode not set");
+ }
+ return *m_preferredMode;
+}
+
+float COutput::GetPixelRatioForMode(const Mode& mode) const
+{
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ if (m_physicalSize.IsZero() || mode.size.IsZero())
+ {
+ return 1.0f;
+ }
+ else
+ {
+ return (
+ (static_cast<float> (m_physicalSize.Width()) / static_cast<float> (mode.size.Width()))
+ /
+ (static_cast<float> (m_physicalSize.Height()) / static_cast<float> (mode.size.Height()))
+ );
+ }
+}
+
+float COutput::GetDpiForMode(const Mode& mode) const
+{
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ if (m_physicalSize.IsZero())
+ {
+ // We really have no idea, so use a "sane" default
+ return 96.0;
+ }
+ else
+ {
+ constexpr float INCH_MM_RATIO{25.4f};
+
+ float diagonalPixels = std::sqrt(mode.size.Width() * mode.size.Width() + mode.size.Height() * mode.size.Height());
+ // physicalWidth/physicalHeight is in millimeters
+ float diagonalInches =
+ std::sqrt(static_cast<float>(m_physicalSize.Width() * m_physicalSize.Width() +
+ m_physicalSize.Height() * m_physicalSize.Height())) /
+ INCH_MM_RATIO;
+
+ return diagonalPixels / diagonalInches;
+ }
+}
+
+float COutput::GetCurrentDpi() const
+{
+ return GetDpiForMode(GetCurrentMode());
+}
diff --git a/xbmc/windowing/wayland/Output.h b/xbmc/windowing/wayland/Output.h
new file mode 100644
index 0000000..2408a6f
--- /dev/null
+++ b/xbmc/windowing/wayland/Output.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+#include "utils/Geometry.h"
+
+#include <atomic>
+#include <cstdint>
+#include <mutex>
+#include <set>
+#include <tuple>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * wl_output handler that collects information from the compositor and then
+ * passes it on when everything is available
+ */
+class COutput
+{
+public:
+ COutput(std::uint32_t globalName, wayland::output_t const & output, std::function<void()> doneHandler);
+ ~COutput() noexcept;
+
+ wayland::output_t const& GetWaylandOutput() const
+ {
+ return m_output;
+ }
+ std::uint32_t GetGlobalName() const
+ {
+ return m_globalName;
+ }
+ /**
+ * Get output position in compositor coordinate space
+ * \return (x, y) tuple of output position
+ */
+ CPointInt GetPosition() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ return m_position;
+ }
+ /**
+ * Get output physical size in millimeters
+ * \return (width, height) tuple of output physical size in millimeters
+ */
+ CSizeInt GetPhysicalSize() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ return m_physicalSize;
+ }
+ std::string const& GetMake() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ return m_make;
+ }
+ std::string const& GetModel() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ return m_model;
+ }
+ std::int32_t GetScale() const
+ {
+ return m_scale;
+ }
+
+ struct Mode
+ {
+ CSizeInt size;
+ std::int32_t refreshMilliHz;
+ Mode(CSizeInt size, std::int32_t refreshMilliHz)
+ : size{size}, refreshMilliHz(refreshMilliHz)
+ {}
+
+ float GetRefreshInHz() const
+ {
+ return refreshMilliHz / 1000.0f;
+ }
+
+ std::tuple<std::int32_t, std::int32_t, std::int32_t> AsTuple() const
+ {
+ return std::make_tuple(size.Width(), size.Height(), refreshMilliHz);
+ }
+
+ // Comparison operator needed for std::set
+ bool operator<(const Mode& right) const
+ {
+ return AsTuple() < right.AsTuple();
+ }
+
+ bool operator==(const Mode& right) const
+ {
+ return AsTuple() == right.AsTuple();
+ }
+
+ bool operator!=(const Mode& right) const
+ {
+ return !(*this == right);
+ }
+ };
+
+ std::set<Mode> const& GetModes() const
+ {
+ return m_modes;
+ }
+ Mode const& GetCurrentMode() const;
+ Mode const& GetPreferredMode() const;
+
+ float GetPixelRatioForMode(Mode const& mode) const;
+ float GetDpiForMode(Mode const& mode) const;
+ float GetCurrentDpi() const;
+
+private:
+ COutput(COutput const& other) = delete;
+ COutput& operator=(COutput const& other) = delete;
+
+ std::uint32_t m_globalName;
+ wayland::output_t m_output;
+ std::function<void()> m_doneHandler;
+
+ mutable CCriticalSection m_geometryCriticalSection;
+ mutable CCriticalSection m_iteratorCriticalSection;
+
+ CPointInt m_position;
+ CSizeInt m_physicalSize;
+ std::string m_make, m_model;
+ std::atomic<std::int32_t> m_scale{1}; // default scale of 1 if no wl_output::scale is sent
+
+ std::set<Mode> m_modes;
+ // For std::set, insertion never invalidates existing iterators, and modes are
+ // never removed, so the usage of iterators is safe
+ std::set<Mode>::iterator m_currentMode{m_modes.end()};
+ std::set<Mode>::iterator m_preferredMode{m_modes.end()};
+};
+
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/Registry.cpp b/xbmc/windowing/wayland/Registry.cpp
new file mode 100644
index 0000000..68cd745
--- /dev/null
+++ b/xbmc/windowing/wayland/Registry.cpp
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "Registry.h"
+
+#include "WinEventsWayland.h"
+#include "utils/log.h"
+
+#include <wayland-client-protocol.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+void TryBind(wayland::registry_t& registry, wayland::proxy_t& target, std::uint32_t name, std::string const& interface, std::uint32_t minVersion, std::uint32_t maxVersion, std::uint32_t offeredVersion)
+{
+ if (offeredVersion < minVersion)
+ {
+ CLog::Log(LOGWARNING,
+ "Not binding Wayland protocol {} because server has only version {} (we need at "
+ "least version {})",
+ interface, offeredVersion, minVersion);
+ }
+ else
+ {
+ // Binding below the offered version is OK
+ auto bindVersion = std::min(maxVersion, offeredVersion);
+ CLog::Log(LOGDEBUG, "Binding Wayland protocol {} version {} (server has version {})", interface,
+ bindVersion, offeredVersion);
+ registry.bind(name, target, bindVersion);
+ }
+}
+
+}
+
+CRegistry::CRegistry(CConnection& connection)
+: m_connection{connection}
+{
+}
+
+void CRegistry::RequestSingletonInternal(wayland::proxy_t& target, std::string const& interfaceName, std::uint32_t minVersion, std::uint32_t maxVersion, bool required)
+{
+ if (m_registry)
+ {
+ throw std::logic_error("Cannot request more binds from registry after binding has started");
+ }
+ m_singletonBinds.emplace(std::piecewise_construct, std::forward_as_tuple(interfaceName), std::forward_as_tuple(target, minVersion, maxVersion, required));
+}
+
+void CRegistry::RequestInternal(std::function<wayland::proxy_t()> constructor, const std::string& interfaceName, std::uint32_t minVersion, std::uint32_t maxVersion, AddHandler addHandler, RemoveHandler removeHandler)
+{
+ if (m_registry)
+ {
+ throw std::logic_error("Cannot request more binds from registry after binding has started");
+ }
+ m_binds.emplace(std::piecewise_construct, std::forward_as_tuple(interfaceName), std::forward_as_tuple(constructor, minVersion, maxVersion, addHandler, removeHandler));
+}
+
+void CRegistry::Bind()
+{
+ if (m_registry)
+ {
+ throw std::logic_error("Cannot start binding on registry twice");
+ }
+
+ // We want to block in this function until we have received the global interfaces
+ // from the compositor - no matter whether the global event pump is running
+ // or not.
+ // If it is running, we have to take special precautions not to drop events between
+ // the creation of the registry and attaching event handlers, so we create
+ // an extra queue and use that to dispatch the singleton globals. Then
+ // we switch back to the global queue for further dispatch of interfaces
+ // added/removed dynamically.
+
+ auto registryRoundtripQueue = m_connection.GetDisplay().create_queue();
+
+ auto displayProxy = m_connection.GetDisplay().proxy_create_wrapper();
+ displayProxy.set_queue(registryRoundtripQueue);
+
+ m_registry = displayProxy.get_registry();
+
+ m_registry.on_global() = [this](std::uint32_t name, const std::string& interface,
+ std::uint32_t version) {
+ {
+ auto it = m_singletonBinds.find(interface);
+ if (it != m_singletonBinds.end())
+ {
+ auto& bind = it->second;
+ auto registryProxy = m_registry.proxy_create_wrapper();
+ // Events on the bound global should always go to the main queue
+ registryProxy.set_queue(wayland::event_queue_t());
+ TryBind(registryProxy, bind.target, name, interface, bind.minVersion, bind.maxVersion, version);
+ return;
+ }
+ }
+
+ {
+ auto it = m_binds.find(interface);
+ if (it != m_binds.end())
+ {
+ auto& bind = it->second;
+ wayland::proxy_t target{bind.constructor()};
+ auto registryProxy = m_registry.proxy_create_wrapper();
+ // Events on the bound global should always go to the main queue
+ registryProxy.set_queue(wayland::event_queue_t());
+ TryBind(registryProxy, target, name, interface, bind.minVersion, bind.maxVersion, version);
+ if (target)
+ {
+ m_boundNames.emplace(name, bind);
+ bind.addHandler(name, std::move(target));
+ }
+ return;
+ }
+ }
+ };
+
+ m_registry.on_global_remove() = [this] (std::uint32_t name)
+ {
+ auto it = m_boundNames.find(name);
+ if (it != m_boundNames.end())
+ {
+ it->second.get().removeHandler(name);
+ m_boundNames.erase(it);
+ }
+ };
+
+ CLog::Log(LOGDEBUG, "Wayland connection: Waiting for global interfaces");
+ m_connection.GetDisplay().roundtrip_queue(registryRoundtripQueue);
+ CLog::Log(LOGDEBUG, "Wayland connection: Roundtrip complete");
+
+ CheckRequired();
+
+ // Now switch it to the global queue for further runtime binds
+ m_registry.set_queue(wayland::event_queue_t());
+ // Roundtrip extra queue one last time in case something got queued up there.
+ // Do it on the event thread so it does not race/run in parallel with the
+ // dispatch of newly arrived registry messages in the default queue.
+ CWinEventsWayland::RoundtripQueue(registryRoundtripQueue);
+}
+
+void CRegistry::UnbindSingletons()
+{
+ for (auto& bind : m_singletonBinds)
+ {
+ bind.second.target.proxy_release();
+ }
+}
+
+void CRegistry::CheckRequired()
+{
+ for (auto const& bind : m_singletonBinds)
+ {
+ if (bind.second.required && !bind.second.target)
+ {
+ throw std::runtime_error(std::string("Missing required ") + bind.first + " protocol");
+ }
+ }
+} \ No newline at end of file
diff --git a/xbmc/windowing/wayland/Registry.h b/xbmc/windowing/wayland/Registry.h
new file mode 100644
index 0000000..f440782
--- /dev/null
+++ b/xbmc/windowing/wayland/Registry.h
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Connection.h"
+
+#include <cstdint>
+#include <functional>
+#include <map>
+#include <utility>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Handle Wayland globals
+ *
+ * Request singletons (bound once) with \ref RequestSingleton, non-singletons
+ * such as wl_output with \ref Request, then call \ref Bind once.
+ *
+ * Make sure to destroy all registries before destroying the \ref CConnection.
+ */
+class CRegistry
+{
+public:
+ explicit CRegistry(CConnection& connection);
+
+ /**
+ * Request a static singleton global to be bound to a proxy
+ *
+ * You should only use this if the singleton is announced at registry bind time
+ * (not dynamically) and you do not need to catch events that are sent immediately
+ * in response to the bind. Use \ref Request in that case, even if there is
+ * ever only one instance of the object at maximum.
+ *
+ * Cannot be called after \ref Bind has been called.
+ *
+ * \param target target of waylandpp proxy type
+ * \param minVersion minimum version to bind
+ * \param maxVersion maximum version to bind
+ * \param required whether to throw an exception when the bind cannot be satisfied
+ * by the compositor - exception is thrown in \ref Bind
+ */
+ template<typename T>
+ void RequestSingleton(T& target, std::uint32_t minVersion, std::uint32_t maxVersion, bool required = true)
+ {
+ RequestSingletonInternal(target, T::interface_name, minVersion, maxVersion, required);
+ }
+ using AddHandler = std::function<void(std::uint32_t /* name */, wayland::proxy_t&& /* object */)>;
+ using RemoveHandler = std::function<void(std::uint32_t) /* name */>;
+ /**
+ * Request a callback when a dynamic global appears or disappears
+ *
+ * The callbacks may be called from the thread that calls \ref Bind or the
+ * global Wayland message pump thread during \ref Bind (but never at the same
+ * time) and only from the global thread after \ref Bind returns.
+ *
+ * Events that occur immediately upon binding are only delivered reliably
+ * if \ref Bind is called from the Wayland message pump thread.
+ *
+ * Cannot be called after \ref Bind has been called.
+ *
+ * \param minVersion minimum version to bind
+ * \param maxVersion maximum version to bind
+ * \param addHandler function that is called when a global of the requested
+ * type is added
+ * \param removeHandler function that is called when a global of the requested
+ * type is removed
+ */
+ template<typename T>
+ void Request(std::uint32_t minVersion,
+ std::uint32_t maxVersion,
+ const AddHandler& addHandler,
+ const RemoveHandler& removeHandler)
+ {
+ RequestInternal([]{ return T(); }, T::interface_name, minVersion, maxVersion, addHandler, removeHandler);
+ }
+
+ /**
+ * Create a registry object at the compositor and do an roundtrip to bind
+ * objects
+ *
+ * This function blocks until the initial roundtrip is complete. All statically
+ * requested singletons that were available will be bound then.
+ *
+ * Neither statically nor dynamically requested proxies will be bound before this
+ * function is called.
+ *
+ * May throw std::runtime_error if a required global was not found.
+ *
+ * Can only be called once for the same \ref CRegistry object.
+ */
+ void Bind();
+ /**
+ * Unbind all singletons requested with \ref RequestSingleton
+ */
+ void UnbindSingletons();
+
+private:
+ CRegistry(CRegistry const& other) = delete;
+ CRegistry& operator=(CRegistry const& other) = delete;
+
+ void RequestSingletonInternal(wayland::proxy_t& target, std::string const& interfaceName, std::uint32_t minVersion, std::uint32_t maxVersion, bool required);
+ void RequestInternal(std::function<wayland::proxy_t()> constructor, std::string const& interfaceName, std::uint32_t minVersion, std::uint32_t maxVersion, AddHandler addHandler, RemoveHandler removeHandler);
+ void CheckRequired();
+
+ CConnection& m_connection;
+ wayland::registry_t m_registry;
+
+ struct SingletonBindInfo
+ {
+ wayland::proxy_t& target;
+ // Throw exception if trying to bind below this version and required
+ std::uint32_t minVersion;
+ // Limit bind version to the minimum of this and compositor version
+ std::uint32_t maxVersion;
+ bool required;
+ SingletonBindInfo(wayland::proxy_t& target, std::uint32_t minVersion, std::uint32_t maxVersion, bool required)
+ : target{target}, minVersion{minVersion}, maxVersion{maxVersion}, required{required}
+ {}
+ };
+ std::map<std::string, SingletonBindInfo> m_singletonBinds;
+
+ struct BindInfo
+ {
+ std::function<wayland::proxy_t()> constructor;
+ std::uint32_t minVersion;
+ std::uint32_t maxVersion;
+ AddHandler addHandler;
+ RemoveHandler removeHandler;
+ BindInfo(std::function<wayland::proxy_t()> constructor,
+ std::uint32_t minVersion,
+ std::uint32_t maxVersion,
+ AddHandler addHandler,
+ RemoveHandler removeHandler)
+ : constructor{std::move(constructor)},
+ minVersion{minVersion},
+ maxVersion{maxVersion},
+ addHandler{std::move(addHandler)},
+ removeHandler{std::move(removeHandler)}
+ {}
+ };
+ std::map<std::string, BindInfo> m_binds;
+
+ std::map<std::uint32_t, std::reference_wrapper<BindInfo>> m_boundNames;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/Seat.cpp b/xbmc/windowing/wayland/Seat.cpp
new file mode 100644
index 0000000..56709b3
--- /dev/null
+++ b/xbmc/windowing/wayland/Seat.cpp
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "Seat.h"
+
+#include "utils/log.h"
+
+#include "platform/posix/utils/FileHandle.h"
+#include "platform/posix/utils/Mmap.h"
+
+#include <cassert>
+#include <utility>
+
+#include <unistd.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+
+namespace
+{
+
+/**
+ * Handle change of availability of a wl_seat input capability
+ *
+ * This checks whether the capability is currently available with the wl_seat
+ * and whether it was bound to a protocol object. If there is a mismatch between
+ * these two, the protocol proxy is released if a capability was removed or bound
+ * if a capability was added.
+ *
+ * \param caps new capabilities
+ * \param cap capability to check for
+ * \param seatName human-readable name of the seat for log messages
+ * \param capName human-readable name of the capability for log messages
+ * \param proxy proxy object that should be filled with a new instance or reset
+ * \param instanceProvider function that functions as factory for the Wayland
+ * protocol instance if the capability has been added
+ */
+template<typename T, typename InstanceProviderT>
+bool HandleCapabilityChange(const wayland::seat_capability& caps,
+ const wayland::seat_capability& cap,
+ std::string const& seatName,
+ std::string const& capName,
+ T& proxy,
+ InstanceProviderT instanceProvider)
+{
+ bool hasCapability = caps & cap;
+
+ if ((!!proxy) != hasCapability)
+ {
+ // Capability changed
+
+ if (hasCapability)
+ {
+ // The capability was added
+ CLog::Log(LOGDEBUG, "Wayland seat {} gained capability {}", seatName, capName);
+ proxy = instanceProvider();
+ return true;
+ }
+ else
+ {
+ // The capability was removed
+ CLog::Log(LOGDEBUG, "Wayland seat {} lost capability {}", seatName, capName);
+ proxy.proxy_release();
+ }
+ }
+
+ return false;
+}
+
+}
+
+CSeat::CSeat(std::uint32_t globalName, wayland::seat_t const& seat, CConnection& connection)
+: m_globalName{globalName}, m_seat{seat}, m_selection{connection, seat}
+{
+ m_seat.on_name() = [this](std::string name) { m_name = std::move(name); };
+ m_seat.on_capabilities() = std::bind(&CSeat::HandleOnCapabilities, this, std::placeholders::_1);
+}
+
+CSeat::~CSeat() noexcept = default;
+
+void CSeat::AddRawInputHandlerKeyboard(KODI::WINDOWING::WAYLAND::IRawInputHandlerKeyboard *rawKeyboardHandler)
+{
+ assert(rawKeyboardHandler);
+ m_rawKeyboardHandlers.emplace(rawKeyboardHandler);
+}
+
+void CSeat::RemoveRawInputHandlerKeyboard(KODI::WINDOWING::WAYLAND::IRawInputHandlerKeyboard *rawKeyboardHandler)
+{
+ m_rawKeyboardHandlers.erase(rawKeyboardHandler);
+}
+
+void CSeat::AddRawInputHandlerPointer(IRawInputHandlerPointer* rawPointerHandler)
+{
+ assert(rawPointerHandler);
+ m_rawPointerHandlers.emplace(rawPointerHandler);
+}
+
+void CSeat::RemoveRawInputHandlerPointer(KODI::WINDOWING::WAYLAND::IRawInputHandlerPointer *rawPointerHandler)
+{
+ m_rawPointerHandlers.erase(rawPointerHandler);
+}
+
+void CSeat::AddRawInputHandlerTouch(IRawInputHandlerTouch* rawTouchHandler)
+{
+ assert(rawTouchHandler);
+ m_rawTouchHandlers.emplace(rawTouchHandler);
+}
+
+void CSeat::RemoveRawInputHandlerTouch(KODI::WINDOWING::WAYLAND::IRawInputHandlerTouch *rawTouchHandler)
+{
+ m_rawTouchHandlers.erase(rawTouchHandler);
+}
+
+void CSeat::HandleOnCapabilities(const wayland::seat_capability& caps)
+{
+ if (HandleCapabilityChange(caps, wayland::seat_capability::keyboard, GetName(), "keyboard", m_keyboard, std::bind(&wayland::seat_t::get_keyboard, m_seat)))
+ {
+ HandleKeyboardCapability();
+ }
+ if (HandleCapabilityChange(caps, wayland::seat_capability::pointer, GetName(), "pointer", m_pointer, std::bind(&wayland::seat_t::get_pointer, m_seat)))
+ {
+ HandlePointerCapability();
+ }
+ if (HandleCapabilityChange(caps, wayland::seat_capability::touch, GetName(), "touch", m_touch, std::bind(&wayland::seat_t::get_touch, m_seat)))
+ {
+ HandleTouchCapability();
+ }
+}
+
+void CSeat::SetCursor(std::uint32_t serial, wayland::surface_t const &surface, std::int32_t hotspotX, std::int32_t hotspotY)
+{
+ if (m_pointer)
+ {
+ m_pointer.set_cursor(serial, surface, hotspotX, hotspotY);
+ }
+}
+
+void CSeat::HandleKeyboardCapability()
+{
+ m_keyboard.on_keymap() = [this](wayland::keyboard_keymap_format format, int fd, std::uint32_t size)
+ {
+ KODI::UTILS::POSIX::CFileHandle fdGuard{fd};
+ KODI::UTILS::POSIX::CMmap mmap{nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0};
+ std::string keymap{static_cast<const char*> (mmap.Data()), size};
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardKeymap(this, format, keymap);
+ }
+ };
+ m_keyboard.on_enter() = [this](std::uint32_t serial, const wayland::surface_t& surface,
+ const wayland::array_t& keys) {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardEnter(this, serial, surface, keys);
+ }
+ };
+ m_keyboard.on_leave() = [this](std::uint32_t serial, const wayland::surface_t& surface) {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardLeave(this, serial, surface);
+ }
+ };
+ m_keyboard.on_key() = [this](std::uint32_t serial, std::uint32_t time, std::uint32_t key, wayland::keyboard_key_state state)
+ {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardKey(this, serial, time, key, state);
+ }
+ };
+ m_keyboard.on_modifiers() = [this](std::uint32_t serial, std::uint32_t modsDepressed, std::uint32_t modsLatched, std::uint32_t modsLocked, std::uint32_t group)
+ {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardModifiers(this, serial, modsDepressed, modsLatched, modsLocked, group);
+ }
+ };
+ m_keyboard.on_repeat_info() = [this](std::int32_t rate, std::int32_t delay)
+ {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardRepeatInfo(this, rate, delay);
+ }
+ };
+}
+
+
+void CSeat::HandlePointerCapability()
+{
+ m_pointer.on_enter() = [this](std::uint32_t serial, const wayland::surface_t& surface,
+ double surfaceX, double surfaceY) {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerEnter(this, serial, surface, surfaceX, surfaceY);
+ }
+ };
+ m_pointer.on_leave() = [this](std::uint32_t serial, const wayland::surface_t& surface) {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerLeave(this, serial, surface);
+ }
+ };
+ m_pointer.on_motion() = [this](std::uint32_t time, double surfaceX, double surfaceY)
+ {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerMotion(this, time, surfaceX, surfaceY);
+ }
+ };
+ m_pointer.on_button() = [this](std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state)
+ {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerButton(this, serial, time, button, state);
+ }
+ };
+ m_pointer.on_axis() = [this](std::uint32_t time, wayland::pointer_axis axis, double value)
+ {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerAxis(this, time, axis, value);
+ }
+ };
+ // Wayland groups pointer events, but right now there is no benefit in
+ // treating them in groups. The main use case for doing so seems to be
+ // multi-axis (i.e. diagonal) scrolling, but we do not support this anyway.
+ /*m_pointer.on_frame() = [this]()
+ {
+
+ };*/
+}
+
+void CSeat::HandleTouchCapability()
+{
+ m_touch.on_down() = [this](std::uint32_t serial, std::uint32_t time,
+ const wayland::surface_t& surface, std::int32_t id, double x,
+ double y) {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchDown(this, serial, time, surface, id, x, y);
+ }
+ };
+ m_touch.on_up() = [this](std::uint32_t serial, std::uint32_t time, std::int32_t id)
+ {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchUp(this, serial, time, id);
+ }
+ };
+ m_touch.on_motion() = [this](std::uint32_t time, std::int32_t id, double x, double y)
+ {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchMotion(this, time, id, x, y);
+ }
+ };
+ m_touch.on_cancel() = [this]()
+ {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchCancel(this);
+ }
+ };
+ m_touch.on_shape() = [this](std::int32_t id, double major, double minor)
+ {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchShape(this, id, major, minor);
+ }
+ };
+}
diff --git a/xbmc/windowing/wayland/Seat.h b/xbmc/windowing/wayland/Seat.h
new file mode 100644
index 0000000..095f18d
--- /dev/null
+++ b/xbmc/windowing/wayland/Seat.h
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "SeatSelection.h"
+
+#include <cstdint>
+#include <set>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CSeat;
+
+/**
+ * Handler for raw wl_keyboard events
+ *
+ * All functions are identical to wl_keyboard, except for the keymap which is
+ * retrieved from its fd and put into a string
+ */
+class IRawInputHandlerKeyboard
+{
+public:
+ virtual void OnKeyboardKeymap(CSeat* seat, wayland::keyboard_keymap_format format, std::string const& keymap) {}
+ virtual void OnKeyboardEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ const wayland::array_t& keys)
+ {
+ }
+ virtual void OnKeyboardLeave(CSeat* seat, std::uint32_t serial, const wayland::surface_t& surface)
+ {
+ }
+ virtual void OnKeyboardKey(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t key, wayland::keyboard_key_state state) {}
+ virtual void OnKeyboardModifiers(CSeat* seat, std::uint32_t serial, std::uint32_t modsDepressed, std::uint32_t modsLatched, std::uint32_t modsLocked, std::uint32_t group) {}
+ virtual void OnKeyboardRepeatInfo(CSeat* seat, std::int32_t rate, std::int32_t delay) {}
+protected:
+ ~IRawInputHandlerKeyboard() = default;
+};
+
+/**
+ * Handler for raw wl_pointer events
+ *
+ * All functions are identical to wl_pointer
+ */
+class IRawInputHandlerPointer
+{
+public:
+ virtual void OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY)
+ {
+ }
+ virtual void OnPointerLeave(CSeat* seat, std::uint32_t serial, const wayland::surface_t& surface)
+ {
+ }
+ virtual void OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY) {}
+ virtual void OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state) {}
+ virtual void OnPointerAxis(CSeat* seat, std::uint32_t time, wayland::pointer_axis axis, double value) {}
+protected:
+ ~IRawInputHandlerPointer() = default;
+};
+
+/**
+ * Handler for raw wl_touch events
+ *
+ * All functions are identical to wl_touch
+ */
+class IRawInputHandlerTouch
+{
+public:
+ virtual void OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y)
+ {
+ }
+ virtual void OnTouchUp(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::int32_t id) {}
+ virtual void OnTouchMotion(CSeat* seat, std::uint32_t time, std::int32_t id, double x, double y) {}
+ virtual void OnTouchCancel(CSeat* seat) {}
+ virtual void OnTouchShape(CSeat* seat, std::int32_t id, double major, double minor) {}
+protected:
+ ~IRawInputHandlerTouch() = default;
+};
+
+/**
+ * Handle all events and requests related to one seat (including input and selection)
+ *
+ * The primary purpose of this class is to act as entry point of Wayland events into
+ * the Kodi world and distribute them further as necessary.
+ * Input events are forwarded to (potentially multiple) handlers. As the Wayland
+ * protocol is not very specific on having multiple wl_seat/wl_pointer instances
+ * and how they interact, having one central instance and then handling everything
+ * in Kodi with multiple handlers is better than each handler having its own
+ * protocol object instance.
+ */
+class CSeat
+{
+public:
+ /**
+ * Construct seat handler
+ * \param globalName Wayland numeric global name of the seat
+ * \param seat bound seat_t instance
+ * \param connection connection for retrieving additional globals
+ */
+ CSeat(std::uint32_t globalName, wayland::seat_t const& seat, CConnection& connection);
+ ~CSeat() noexcept;
+
+ void AddRawInputHandlerKeyboard(IRawInputHandlerKeyboard* rawKeyboardHandler);
+ void RemoveRawInputHandlerKeyboard(IRawInputHandlerKeyboard* rawKeyboardHandler);
+ void AddRawInputHandlerPointer(IRawInputHandlerPointer* rawPointerHandler);
+ void RemoveRawInputHandlerPointer(IRawInputHandlerPointer* rawPointerHandler);
+ void AddRawInputHandlerTouch(IRawInputHandlerTouch* rawTouchHandler);
+ void RemoveRawInputHandlerTouch(IRawInputHandlerTouch* rawTouchHandler);
+
+ std::uint32_t GetGlobalName() const
+ {
+ return m_globalName;
+ }
+ std::string const& GetName() const
+ {
+ return m_name;
+ }
+ bool HasPointerCapability() const
+ {
+ return !!m_pointer;
+ }
+ bool HasKeyboardCapability() const
+ {
+ return !!m_keyboard;
+ }
+ bool HasTouchCapability() const
+ {
+ return !!m_touch;
+ }
+ std::string GetSelectionText() const
+ {
+ return m_selection.GetSelectionText();
+ }
+ /**
+ * Get the wl_seat underlying this seat
+ *
+ * The wl_seat should only be used when strictly necessary, e.g. when
+ * starting a move operation with shell interfaces.
+ * It may not be used to derive further wl_pointer etc. instances.
+ */
+ wayland::seat_t const& GetWlSeat()
+ {
+ return m_seat;
+ }
+
+ /**
+ * Set the cursor of the pointer of this seat
+ *
+ * Parameters are identical wo wl_pointer.set_cursor().
+ * If the seat does not currently have the pointer capability, this is a no-op.
+ */
+ void SetCursor(std::uint32_t serial, wayland::surface_t const& surface, std::int32_t hotspotX, std::int32_t hotspotY);
+
+private:
+ CSeat(CSeat const& other) = delete;
+ CSeat& operator=(CSeat const& other) = delete;
+
+ void HandleOnCapabilities(const wayland::seat_capability& caps);
+ void HandlePointerCapability();
+ void HandleKeyboardCapability();
+ void HandleTouchCapability();
+
+ std::uint32_t m_globalName;
+ std::string m_name{"<unknown>"};
+
+ wayland::seat_t m_seat;
+ wayland::pointer_t m_pointer;
+ wayland::keyboard_t m_keyboard;
+ wayland::touch_t m_touch;
+
+ std::set<IRawInputHandlerKeyboard*> m_rawKeyboardHandlers;
+ std::set<IRawInputHandlerPointer*> m_rawPointerHandlers;
+ std::set<IRawInputHandlerTouch*> m_rawTouchHandlers;
+
+ CSeatSelection m_selection;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/SeatInputProcessing.cpp b/xbmc/windowing/wayland/SeatInputProcessing.cpp
new file mode 100644
index 0000000..6430aad
--- /dev/null
+++ b/xbmc/windowing/wayland/SeatInputProcessing.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "SeatInputProcessing.h"
+
+#include <cassert>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+CSeatInputProcessing::CSeatInputProcessing(wayland::surface_t const& inputSurface, IInputHandler& handler)
+: m_inputSurface{inputSurface}, m_handler{handler}
+{
+}
+
+void CSeatInputProcessing::AddSeat(CSeat* seat)
+{
+ assert(m_seats.find(seat->GetGlobalName()) == m_seats.end());
+ auto& seatState = m_seats.emplace(seat->GetGlobalName(), seat).first->second;
+
+ seatState.keyboardProcessor.reset(new CInputProcessorKeyboard(*this));
+ seat->AddRawInputHandlerKeyboard(seatState.keyboardProcessor.get());
+ seatState.pointerProcessor.reset(new CInputProcessorPointer(m_inputSurface, *this));
+ seat->AddRawInputHandlerPointer(seatState.pointerProcessor.get());
+ seatState.touchProcessor.reset(new CInputProcessorTouch(m_inputSurface));
+ seat->AddRawInputHandlerTouch(seatState.touchProcessor.get());
+}
+
+void CSeatInputProcessing::RemoveSeat(CSeat* seat)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI != m_seats.end())
+ {
+ seat->RemoveRawInputHandlerKeyboard(seatStateI->second.keyboardProcessor.get());
+ seat->RemoveRawInputHandlerPointer(seatStateI->second.pointerProcessor.get());
+ seat->RemoveRawInputHandlerTouch(seatStateI->second.touchProcessor.get());
+ m_seats.erase(seatStateI);
+ }
+}
+
+void CSeatInputProcessing::OnPointerEnter(std::uint32_t seatGlobalName, std::uint32_t serial)
+{
+ m_handler.OnSetCursor(seatGlobalName, serial);
+ m_handler.OnEnter(InputType::POINTER);
+}
+
+void CSeatInputProcessing::OnPointerLeave()
+{
+ m_handler.OnLeave(InputType::POINTER);
+}
+
+void CSeatInputProcessing::OnPointerEvent(XBMC_Event& event)
+{
+ m_handler.OnEvent(InputType::POINTER, event);
+}
+
+void CSeatInputProcessing::OnKeyboardEnter()
+{
+ m_handler.OnEnter(InputType::KEYBOARD);
+}
+
+void CSeatInputProcessing::OnKeyboardLeave()
+{
+ m_handler.OnLeave(InputType::KEYBOARD);
+}
+
+void CSeatInputProcessing::OnKeyboardEvent(XBMC_Event& event)
+{
+ m_handler.OnEvent(InputType::KEYBOARD, event);
+}
+
+void CSeatInputProcessing::SetCoordinateScale(std::int32_t scale)
+{
+ for (auto& seatPair : m_seats)
+ {
+ seatPair.second.touchProcessor->SetCoordinateScale(scale);
+ seatPair.second.pointerProcessor->SetCoordinateScale(scale);
+ }
+}
diff --git a/xbmc/windowing/wayland/SeatInputProcessing.h b/xbmc/windowing/wayland/SeatInputProcessing.h
new file mode 100644
index 0000000..ce1f0ee
--- /dev/null
+++ b/xbmc/windowing/wayland/SeatInputProcessing.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "InputProcessorKeyboard.h"
+#include "InputProcessorPointer.h"
+#include "InputProcessorTouch.h"
+#include "Seat.h"
+
+#include <cstdint>
+#include <map>
+#include <memory>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+enum class InputType
+{
+ POINTER,
+ KEYBOARD,
+ TOUCH
+};
+
+/**
+ * Handler interface for input events from \ref CSeatInputProcessor
+ */
+class IInputHandler
+{
+public:
+ /**
+ * Handle input event
+ * \param type input device type that caused the event
+ * \param event XBMC event data
+ */
+ virtual void OnEvent(InputType type, XBMC_Event& event) {}
+ /**
+ * Handle focus enter
+ * \param type input device type for which the surface has gained the focus
+ */
+ virtual void OnEnter(InputType type) {}
+ /**
+ * Handle focus leave
+ * \param type input device type for which the surface has lost the focus
+ */
+ virtual void OnLeave(InputType type) {}
+ /**
+ * Handle request for setting the cursor
+ *
+ * When the client gains pointer focus for a surface, a cursor image must be
+ * attached to the pointer. Otherwise the previous pointer image would
+ * be used.
+ *
+ * This request is sent in addition to \ref OnEnter for \ref InputType::POINTER.
+ *
+ * \param seatGlobalName numeric Wayland global name of the seat the event occurred on
+ * \param pointer pointer instance that needs its cursor set
+ * \param serial Wayland protocol message serial that must be sent back in set_cursor
+ */
+ virtual void OnSetCursor(std::uint32_t seatGlobalName, std::uint32_t serial) {}
+
+ virtual ~IInputHandler() = default;
+};
+
+/**
+ * Receive events from all registered wl_seats and process them into Kodi events
+ *
+ * Multi-seat support is not currently implemented completely, but each seat has
+ * separate state.
+ */
+class CSeatInputProcessing final : IInputHandlerPointer, IInputHandlerKeyboard
+{
+public:
+ /**
+ * Construct a seat input processor
+ *
+ * \param inputSurface Surface that events should be processed on (all other surfaces are ignored)
+ * \param handler Mandatory handler for processed input events
+ */
+ CSeatInputProcessing(wayland::surface_t const& inputSurface, IInputHandler& handler);
+ void AddSeat(CSeat* seat);
+ void RemoveSeat(CSeat* seat);
+
+ /**
+ * Set the scale the coordinates should be interpreted at
+ *
+ * Wayland input events are always in surface coordinates, but Kodi only uses
+ * buffer coordinates internally. Use this function to set the scaling
+ * factor between the two and multiply the surface coordinates accordingly
+ * for Kodi events.
+ *
+ * \param scale new buffer-to-surface pixel ratio
+ */
+ void SetCoordinateScale(std::int32_t scale);
+
+private:
+ wayland::surface_t m_inputSurface;
+ IInputHandler& m_handler;
+
+ void OnPointerEnter(std::uint32_t seatGlobalName, std::uint32_t serial) override;
+ void OnPointerLeave() override;
+ void OnPointerEvent(XBMC_Event& event) override;
+
+ void OnKeyboardEnter() override;
+ void OnKeyboardLeave() override;
+ void OnKeyboardEvent(XBMC_Event& event) override;
+
+ struct SeatState
+ {
+ CSeat* seat;
+ std::unique_ptr<CInputProcessorKeyboard> keyboardProcessor;
+ std::unique_ptr<CInputProcessorPointer> pointerProcessor;
+ std::unique_ptr<CInputProcessorTouch> touchProcessor;
+
+ SeatState(CSeat* seat)
+ : seat{seat}
+ {}
+ };
+ std::map<std::uint32_t, SeatState> m_seats;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/SeatSelection.cpp b/xbmc/windowing/wayland/SeatSelection.cpp
new file mode 100644
index 0000000..f66555a
--- /dev/null
+++ b/xbmc/windowing/wayland/SeatSelection.cpp
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "SeatSelection.h"
+
+#include "Connection.h"
+#include "Registry.h"
+#include "WinEventsWayland.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include "platform/posix/utils/FileHandle.h"
+
+#include <cerrno>
+#include <chrono>
+#include <cstring>
+#include <mutex>
+#include <system_error>
+#include <utility>
+
+#include <poll.h>
+#include <unistd.h>
+
+using namespace KODI::UTILS::POSIX;
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+const std::vector<std::string> MIME_TYPES_PREFERENCE =
+{
+ "text/plain;charset=utf-8",
+ "text/plain;charset=iso-8859-1",
+ "text/plain;charset=us-ascii",
+ "text/plain"
+};
+
+}
+
+CSeatSelection::CSeatSelection(CConnection& connection, wayland::seat_t const& seat)
+{
+ wayland::data_device_manager_t manager;
+ {
+ CRegistry registry{connection};
+ registry.RequestSingleton(manager, 1, 3, false);
+ registry.Bind();
+ }
+
+ if (!manager)
+ {
+ CLog::Log(LOGWARNING, "No data device manager announced by compositor, clipboard will not be available");
+ return;
+ }
+
+ m_dataDevice = manager.get_data_device(seat);
+
+ // Class is created in response to seat add events - so no events can get lost
+ m_dataDevice.on_data_offer() = [this](wayland::data_offer_t offer)
+ {
+ // We don't know yet whether this is drag-and-drop or selection, so collect
+ // MIME types in either case
+ m_currentOffer = std::move(offer);
+ m_mimeTypeOffers.clear();
+ m_currentOffer.on_offer() = [this](std::string mime)
+ {
+ m_mimeTypeOffers.push_back(std::move(mime));
+ };
+ };
+ m_dataDevice.on_selection() = [this](const wayland::data_offer_t& offer)
+ {
+ std::unique_lock<CCriticalSection> lock(m_currentSelectionMutex);
+ m_matchedMimeType.clear();
+
+ if (offer != m_currentOffer)
+ {
+ // Selection was not previously introduced by offer (could be NULL for example)
+ m_currentSelection.proxy_release();
+ }
+ else
+ {
+ m_currentSelection = offer;
+ std::string offers = StringUtils::Join(m_mimeTypeOffers, ", ");
+
+ // Match MIME type by priority: Find first preferred MIME type that is in the
+ // set of offered types
+ // Charset is not case-sensitive in MIME type spec, so match case-insensitively
+ auto mimeIt = std::find_first_of(MIME_TYPES_PREFERENCE.cbegin(), MIME_TYPES_PREFERENCE.cend(),
+ m_mimeTypeOffers.cbegin(), m_mimeTypeOffers.cend(),
+ // static_cast needed for overload resolution
+ static_cast<bool (*)(std::string const&, std::string const&)> (&StringUtils::EqualsNoCase));
+ if (mimeIt != MIME_TYPES_PREFERENCE.cend())
+ {
+ m_matchedMimeType = *mimeIt;
+ CLog::Log(LOGDEBUG, "Chose selection MIME type {} out of offered {}", m_matchedMimeType,
+ offers);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Could not find compatible MIME type for selection data (offered: {})",
+ offers);
+ }
+ }
+ };
+}
+
+std::string CSeatSelection::GetSelectionText() const
+{
+ std::unique_lock<CCriticalSection> lock(m_currentSelectionMutex);
+ if (!m_currentSelection || m_matchedMimeType.empty())
+ {
+ return "";
+ }
+
+ std::array<int, 2> fds;
+ if (pipe(fds.data()) != 0)
+ {
+ CLog::LogF(LOGERROR, "Could not open pipe for selection data transfer: {}",
+ std::strerror(errno));
+ return "";
+ }
+
+ CFileHandle readFd{fds[0]};
+ CFileHandle writeFd{fds[1]};
+
+ m_currentSelection.receive(m_matchedMimeType, writeFd);
+ lock.unlock();
+ // Make sure the other party gets the request as soon as possible
+ CWinEventsWayland::Flush();
+ // Fd now gets sent to the other party -> make sure our write end is closed
+ // so we get POLLHUP when the other party closes its write fd
+ writeFd.reset();
+
+ pollfd fd =
+ {
+ .fd = readFd,
+ .events = POLLIN,
+ .revents = 0
+ };
+
+ // UI will block in this function when Ctrl+V is pressed, so timeout should be
+ // rather short!
+ const std::chrono::seconds TIMEOUT{1};
+ const std::size_t MAX_SIZE{4096};
+ std::array<char, MAX_SIZE> buffer;
+
+ auto start = std::chrono::steady_clock::now();
+ std::size_t totalBytesRead{0};
+
+ do
+ {
+ auto now = std::chrono::steady_clock::now();
+ // Do not permit negative timeouts (would cause infinitely long poll)
+ auto remainingTimeout = std::max(std::chrono::milliseconds(0), std::chrono::duration_cast<std::chrono::milliseconds> (TIMEOUT - (now - start))).count();
+ // poll() for changes until poll signals POLLHUP and the remaining data was read
+ int ret{poll(&fd, 1, remainingTimeout)};
+ if (ret == 0)
+ {
+ // Timeout
+ CLog::LogF(LOGERROR, "Reading from selection data pipe timed out");
+ return "";
+ }
+ else if (ret < 0 && errno == EINTR)
+ {
+ continue;
+ }
+ else if (ret < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "Error polling selection pipe");
+ }
+ else if (fd.revents & POLLNVAL || fd.revents & POLLERR)
+ {
+ CLog::LogF(LOGERROR, "poll() indicated error on selection pipe");
+ return "";
+ }
+ else if (fd.revents & POLLIN)
+ {
+ if (totalBytesRead >= buffer.size())
+ {
+ CLog::LogF(LOGERROR, "Selection data is too big, aborting read");
+ return "";
+ }
+ ssize_t readBytes{read(fd.fd, buffer.data() + totalBytesRead, buffer.size() - totalBytesRead)};
+ if (readBytes < 0)
+ {
+ CLog::LogF(LOGERROR, "read() from selection pipe failed: {}", std::strerror(errno));
+ return "";
+ }
+ totalBytesRead += readBytes;
+ }
+ }
+ while (!(fd.revents & POLLHUP));
+
+ return std::string(buffer.data(), totalBytesRead);
+}
diff --git a/xbmc/windowing/wayland/SeatSelection.h b/xbmc/windowing/wayland/SeatSelection.h
new file mode 100644
index 0000000..ec99a0d
--- /dev/null
+++ b/xbmc/windowing/wayland/SeatSelection.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <string>
+#include <vector>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CConnection;
+
+/**
+ * Retrieve and accept selection (clipboard) offers on the data device of a seat
+ */
+class CSeatSelection
+{
+public:
+ CSeatSelection(CConnection& connection, wayland::seat_t const& seat);
+ std::string GetSelectionText() const;
+
+private:
+ wayland::data_device_t m_dataDevice;
+ wayland::data_offer_t m_currentOffer;
+ mutable wayland::data_offer_t m_currentSelection;
+
+ std::vector<std::string> m_mimeTypeOffers;
+ std::string m_matchedMimeType;
+
+ mutable CCriticalSection m_currentSelectionMutex;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/ShellSurface.cpp b/xbmc/windowing/wayland/ShellSurface.cpp
new file mode 100644
index 0000000..5836689
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurface.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ShellSurface.h"
+
+#include "utils/StringUtils.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+std::string IShellSurface::StateToString(StateBitset state)
+{
+ std::vector<std::string> parts;
+ if (state.test(STATE_ACTIVATED))
+ {
+ parts.emplace_back("activated");
+ }
+ if (state.test(STATE_FULLSCREEN))
+ {
+ parts.emplace_back("fullscreen");
+ }
+ if (state.test(STATE_MAXIMIZED))
+ {
+ parts.emplace_back("maximized");
+ }
+ if (state.test(STATE_RESIZING))
+ {
+ parts.emplace_back("resizing");
+ }
+ return parts.empty() ? "none" : StringUtils::Join(parts, ",");
+} \ No newline at end of file
diff --git a/xbmc/windowing/wayland/ShellSurface.h b/xbmc/windowing/wayland/ShellSurface.h
new file mode 100644
index 0000000..08690cc
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurface.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "utils/Geometry.h"
+
+#include <bitset>
+#include <cstdint>
+
+#include <wayland-client.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class IShellSurfaceHandler;
+
+/**
+ * Abstraction for shell surfaces to support multiple protocols
+ * such as wl_shell (for compatibility) and xdg_shell (for features)
+ *
+ * The interface itself is modeled after xdg_shell, so see there for the meaning
+ * of e.g. the surface states
+ */
+class IShellSurface
+{
+public:
+ // Not enum class since it must be used like a bitfield
+ enum State
+ {
+ STATE_MAXIMIZED = 0,
+ STATE_FULLSCREEN,
+ STATE_RESIZING,
+ STATE_ACTIVATED,
+ STATE_COUNT
+ };
+ using StateBitset = std::bitset<STATE_COUNT>;
+ static std::string StateToString(StateBitset state);
+
+ /**
+ * Initialize shell surface
+ *
+ * The event loop thread MUST NOT be running when this function is called.
+ * The difference to the constructor is that in this function callbacks may
+ * already be called.
+ */
+ virtual void Initialize() = 0;
+
+ virtual void SetFullScreen(wayland::output_t const& output, float refreshRate) = 0;
+ virtual void SetWindowed() = 0;
+ virtual void SetMaximized() = 0;
+ virtual void UnsetMaximized() = 0;
+ virtual void SetMinimized() = 0;
+ virtual void SetWindowGeometry(CRectInt geometry) = 0;
+
+ virtual void AckConfigure(std::uint32_t serial) = 0;
+
+ virtual void StartMove(wayland::seat_t const& seat, std::uint32_t serial) = 0;
+ virtual void StartResize(wayland::seat_t const& seat, std::uint32_t serial, wayland::shell_surface_resize edge) = 0;
+ virtual void ShowShellContextMenu(wayland::seat_t const& seat, std::uint32_t serial, CPointInt position) = 0;
+
+ virtual ~IShellSurface() = default;
+
+protected:
+ IShellSurface() noexcept = default;
+
+private:
+ IShellSurface(IShellSurface const& other) = delete;
+ IShellSurface& operator=(IShellSurface const& other) = delete;
+};
+
+class IShellSurfaceHandler
+{
+public:
+ virtual void OnConfigure(std::uint32_t serial, CSizeInt size, IShellSurface::StateBitset state) = 0;
+ virtual void OnClose() = 0;
+
+ virtual ~IShellSurfaceHandler() = default;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp b/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp
new file mode 100644
index 0000000..2c91858
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ShellSurfaceWlShell.h"
+
+#include "Registry.h"
+
+#include <cmath>
+
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+
+CShellSurfaceWlShell::CShellSurfaceWlShell(IShellSurfaceHandler& handler,
+ CConnection& connection,
+ const wayland::surface_t& surface,
+ const std::string& title,
+ const std::string& class_)
+ : m_handler{handler}
+{
+ {
+ CRegistry registry{connection};
+ registry.RequestSingleton(m_shell, 1, 1);
+ registry.Bind();
+ }
+
+ m_shellSurface = m_shell.get_shell_surface(surface);
+
+ m_surfaceState.set(STATE_ACTIVATED);
+ m_shellSurface.set_class(class_);
+ m_shellSurface.set_title(title);
+ m_shellSurface.on_ping() = [this](std::uint32_t serial)
+ {
+ m_shellSurface.pong(serial);
+ };
+ m_shellSurface.on_configure() = [this](const wayland::shell_surface_resize&, std::int32_t width,
+ std::int32_t height) {
+ // wl_shell does not have serials
+ m_handler.OnConfigure(0, {width, height}, m_surfaceState);
+ };
+}
+
+void CShellSurfaceWlShell::AckConfigure(std::uint32_t)
+{
+}
+
+void CShellSurfaceWlShell::Initialize()
+{
+ // Nothing to do here - constructor already handles it
+ // This is not a problem since the constructor is guaranteed not to call
+ // handler functions since the event loop is not running.
+}
+
+void CShellSurfaceWlShell::SetFullScreen(const wayland::output_t& output, float refreshRate)
+{
+ m_shellSurface.set_fullscreen(wayland::shell_surface_fullscreen_method::driver, std::round(refreshRate * 1000.0f), output);
+ m_surfaceState.set(STATE_FULLSCREEN);
+}
+
+void CShellSurfaceWlShell::SetWindowed()
+{
+ m_shellSurface.set_toplevel();
+ m_surfaceState.reset(STATE_FULLSCREEN);
+}
+
+void CShellSurfaceWlShell::SetMaximized()
+{
+ m_shellSurface.set_maximized(wayland::output_t());
+ m_surfaceState.set(STATE_MAXIMIZED);
+}
+
+void CShellSurfaceWlShell::UnsetMaximized()
+{
+ m_surfaceState.reset(STATE_MAXIMIZED);
+}
+
+void CShellSurfaceWlShell::SetMinimized()
+{
+}
+
+void CShellSurfaceWlShell::SetWindowGeometry(CRectInt)
+{
+}
+
+void CShellSurfaceWlShell::StartMove(const wayland::seat_t& seat, std::uint32_t serial)
+{
+ m_shellSurface.move(seat, serial);
+}
+
+void CShellSurfaceWlShell::StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge)
+{
+ m_shellSurface.resize(seat, serial, edge);
+}
+
+void CShellSurfaceWlShell::ShowShellContextMenu(const wayland::seat_t&, std::uint32_t, CPointInt)
+{
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceWlShell.h b/xbmc/windowing/wayland/ShellSurfaceWlShell.h
new file mode 100644
index 0000000..8b60d60
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceWlShell.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Connection.h"
+#include "ShellSurface.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CShellSurfaceWlShell : public IShellSurface
+{
+public:
+ /**
+ * Construct wl_shell_surface for given surface
+ *
+ * \parma handler shell surface handler
+ * \param connection connection global
+ * \param surface surface to make shell surface for
+ * \param title title of the surfae
+ * \param class_ class of the surface, which should match the name of the
+ * .desktop file of the application
+ */
+ CShellSurfaceWlShell(IShellSurfaceHandler& handler,
+ CConnection& connection,
+ wayland::surface_t const& surface,
+ const std::string& title,
+ const std::string& class_);
+
+ void Initialize() override;
+
+ void SetFullScreen(wayland::output_t const& output, float refreshRate) override;
+ void SetWindowed() override;
+ void SetMaximized() override;
+ void UnsetMaximized() override;
+ void SetMinimized() override;
+ void SetWindowGeometry(CRectInt geometry) override;
+ void AckConfigure(std::uint32_t serial) override;
+
+ void StartMove(const wayland::seat_t& seat, std::uint32_t serial) override;
+ void StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge) override;
+ void ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position) override;
+
+private:
+ IShellSurfaceHandler& m_handler;
+ wayland::shell_t m_shell;
+ wayland::shell_surface_t m_shellSurface;
+ StateBitset m_surfaceState;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShell.cpp b/xbmc/windowing/wayland/ShellSurfaceXdgShell.cpp
new file mode 100644
index 0000000..e92d33c
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceXdgShell.cpp
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ShellSurfaceXdgShell.h"
+
+#include "Registry.h"
+#include "messaging/ApplicationMessenger.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+IShellSurface::State ConvertStateFlag(wayland::xdg_toplevel_state flag)
+{
+ switch(flag)
+ {
+ case wayland::xdg_toplevel_state::activated:
+ return IShellSurface::STATE_ACTIVATED;
+ case wayland::xdg_toplevel_state::fullscreen:
+ return IShellSurface::STATE_FULLSCREEN;
+ case wayland::xdg_toplevel_state::maximized:
+ return IShellSurface::STATE_MAXIMIZED;
+ case wayland::xdg_toplevel_state::resizing:
+ return IShellSurface::STATE_RESIZING;
+ default:
+ throw std::runtime_error(std::string("Unknown xdg_toplevel state flag ") + std::to_string(static_cast<std::underlying_type<decltype(flag)>::type> (flag)));
+ }
+}
+
+}
+
+CShellSurfaceXdgShell* CShellSurfaceXdgShell::TryCreate(IShellSurfaceHandler& handler, CConnection& connection, const wayland::surface_t& surface, std::string const& title, std::string const& class_)
+{
+ wayland::xdg_wm_base_t shell;
+ CRegistry registry{connection};
+ registry.RequestSingleton(shell, 1, 1, false);
+ registry.Bind();
+
+ if (shell)
+ {
+ return new CShellSurfaceXdgShell(handler, connection.GetDisplay(), shell, surface, title, class_);
+ }
+ else
+ {
+ return nullptr;
+ }
+}
+
+CShellSurfaceXdgShell::CShellSurfaceXdgShell(IShellSurfaceHandler& handler, wayland::display_t& display, const wayland::xdg_wm_base_t& shell, const wayland::surface_t& surface, std::string const& title, std::string const& app_id)
+: m_handler{handler}, m_display{display}, m_shell{shell}, m_surface{surface}, m_xdgSurface{m_shell.get_xdg_surface(m_surface)}, m_xdgToplevel{m_xdgSurface.get_toplevel()}
+{
+ m_shell.on_ping() = [this](std::uint32_t serial)
+ {
+ m_shell.pong(serial);
+ };
+ m_xdgSurface.on_configure() = [this](std::uint32_t serial)
+ {
+ m_handler.OnConfigure(serial, m_configuredSize, m_configuredState);
+ };
+ m_xdgToplevel.on_close() = [this]()
+ {
+ m_handler.OnClose();
+ };
+ m_xdgToplevel.on_configure() = [this](std::int32_t width, std::int32_t height,
+ const std::vector<wayland::xdg_toplevel_state>& states) {
+ m_configuredSize.Set(width, height);
+ m_configuredState.reset();
+ for (auto state : states)
+ {
+ m_configuredState.set(ConvertStateFlag(state));
+ }
+ };
+ m_xdgToplevel.set_app_id(app_id);
+ m_xdgToplevel.set_title(title);
+ // Set sensible minimum size
+ m_xdgToplevel.set_min_size(300, 200);
+}
+
+void CShellSurfaceXdgShell::Initialize()
+{
+ // Commit surface to confirm role
+ // Don't do it in constructor since SetFullScreen might be called before
+ m_surface.commit();
+ // Make sure we get the initial configure before continuing
+ m_display.roundtrip();
+}
+
+void CShellSurfaceXdgShell::AckConfigure(std::uint32_t serial)
+{
+ m_xdgSurface.ack_configure(serial);
+}
+
+CShellSurfaceXdgShell::~CShellSurfaceXdgShell() noexcept
+{
+ // xdg_shell is picky: must destroy toplevel role before surface
+ m_xdgToplevel.proxy_release();
+ m_xdgSurface.proxy_release();
+}
+
+void CShellSurfaceXdgShell::SetFullScreen(const wayland::output_t& output, float)
+{
+ // xdg_shell does not support refresh rate setting at the moment
+ m_xdgToplevel.set_fullscreen(output);
+}
+
+void CShellSurfaceXdgShell::SetWindowed()
+{
+ m_xdgToplevel.unset_fullscreen();
+}
+
+void CShellSurfaceXdgShell::SetMaximized()
+{
+ m_xdgToplevel.set_maximized();
+}
+
+void CShellSurfaceXdgShell::UnsetMaximized()
+{
+ m_xdgToplevel.unset_maximized();
+}
+
+void CShellSurfaceXdgShell::SetMinimized()
+{
+ m_xdgToplevel.set_minimized();
+}
+
+void CShellSurfaceXdgShell::SetWindowGeometry(CRectInt geometry)
+{
+ m_xdgSurface.set_window_geometry(geometry.x1, geometry.y1, geometry.Width(), geometry.Height());
+}
+
+void CShellSurfaceXdgShell::StartMove(const wayland::seat_t& seat, std::uint32_t serial)
+{
+ m_xdgToplevel.move(seat, serial);
+}
+
+void CShellSurfaceXdgShell::StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge)
+{
+ // wl_shell shell_surface_resize is identical to xdg_shell resize_edge
+ m_xdgToplevel.resize(seat, serial, static_cast<std::uint32_t> (edge));
+}
+
+void CShellSurfaceXdgShell::ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position)
+{
+ m_xdgToplevel.show_window_menu(seat, serial, position.x, position.y);
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShell.h b/xbmc/windowing/wayland/ShellSurfaceXdgShell.h
new file mode 100644
index 0000000..718b572
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceXdgShell.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Connection.h"
+#include "ShellSurface.h"
+
+#include <wayland-extra-protocols.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Shell surface implementation for stable xdg_shell
+ */
+class CShellSurfaceXdgShell : public IShellSurface
+{
+public:
+ /**
+ * Construct xdg_shell toplevel object for given surface
+ *
+ * \param handler the shell surface handler
+ * \param display the wl_display global (for initial roundtrip)
+ * \param shell the xdg_wm_base global
+ * \param surface surface to make shell surface for
+ * \param title title of the surfae
+ * \param class_ class of the surface, which should match the name of the
+ * .desktop file of the application
+ */
+ CShellSurfaceXdgShell(IShellSurfaceHandler& handler, wayland::display_t& display, wayland::xdg_wm_base_t const& shell, wayland::surface_t const& surface, std::string const& title, std::string const& class_);
+ ~CShellSurfaceXdgShell() noexcept override;
+
+ static CShellSurfaceXdgShell* TryCreate(IShellSurfaceHandler& handler, CConnection& connection, wayland::surface_t const& surface, std::string const& title, std::string const& class_);
+
+ void Initialize() override;
+
+ void SetFullScreen(wayland::output_t const& output, float refreshRate) override;
+ void SetWindowed() override;
+ void SetMaximized() override;
+ void UnsetMaximized() override;
+ void SetMinimized() override;
+ void SetWindowGeometry(CRectInt geometry) override;
+ void AckConfigure(std::uint32_t serial) override;
+
+ void StartMove(const wayland::seat_t& seat, std::uint32_t serial) override;
+ void StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge) override;
+ void ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position) override;
+
+private:
+ IShellSurfaceHandler& m_handler;
+ wayland::display_t& m_display;
+ wayland::xdg_wm_base_t m_shell;
+ wayland::surface_t m_surface;
+ wayland::xdg_surface_t m_xdgSurface;
+ wayland::xdg_toplevel_t m_xdgToplevel;
+
+ CSizeInt m_configuredSize;
+ StateBitset m_configuredState;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp
new file mode 100644
index 0000000..ca68b98
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ShellSurfaceXdgShellUnstableV6.h"
+
+#include "Registry.h"
+#include "messaging/ApplicationMessenger.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+IShellSurface::State ConvertStateFlag(wayland::zxdg_toplevel_v6_state flag)
+{
+ switch(flag)
+ {
+ case wayland::zxdg_toplevel_v6_state::activated:
+ return IShellSurface::STATE_ACTIVATED;
+ case wayland::zxdg_toplevel_v6_state::fullscreen:
+ return IShellSurface::STATE_FULLSCREEN;
+ case wayland::zxdg_toplevel_v6_state::maximized:
+ return IShellSurface::STATE_MAXIMIZED;
+ case wayland::zxdg_toplevel_v6_state::resizing:
+ return IShellSurface::STATE_RESIZING;
+ default:
+ throw std::runtime_error(std::string("Unknown xdg_toplevel state flag ") + std::to_string(static_cast<std::underlying_type<decltype(flag)>::type> (flag)));
+ }
+}
+
+}
+
+CShellSurfaceXdgShellUnstableV6* CShellSurfaceXdgShellUnstableV6::TryCreate(IShellSurfaceHandler& handler, CConnection& connection, const wayland::surface_t& surface, std::string const& title, std::string const& class_)
+{
+ wayland::zxdg_shell_v6_t shell;
+ CRegistry registry{connection};
+ registry.RequestSingleton(shell, 1, 1, false);
+ registry.Bind();
+
+ if (shell)
+ {
+ return new CShellSurfaceXdgShellUnstableV6(handler, connection.GetDisplay(), shell, surface, title, class_);
+ }
+ else
+ {
+ return nullptr;
+ }
+}
+
+CShellSurfaceXdgShellUnstableV6::CShellSurfaceXdgShellUnstableV6(IShellSurfaceHandler& handler, wayland::display_t& display, const wayland::zxdg_shell_v6_t& shell, const wayland::surface_t& surface, std::string const& title, std::string const& app_id)
+: m_handler{handler}, m_display{display}, m_shell{shell}, m_surface{surface}, m_xdgSurface{m_shell.get_xdg_surface(m_surface)}, m_xdgToplevel{m_xdgSurface.get_toplevel()}
+{
+ m_shell.on_ping() = [this](std::uint32_t serial)
+ {
+ m_shell.pong(serial);
+ };
+ m_xdgSurface.on_configure() = [this](std::uint32_t serial)
+ {
+ m_handler.OnConfigure(serial, m_configuredSize, m_configuredState);
+ };
+ m_xdgToplevel.on_close() = [this]()
+ {
+ m_handler.OnClose();
+ };
+ m_xdgToplevel.on_configure() =
+ [this](std::int32_t width, std::int32_t height,
+ const std::vector<wayland::zxdg_toplevel_v6_state>& states) {
+ m_configuredSize.Set(width, height);
+ m_configuredState.reset();
+ for (auto state : states)
+ {
+ m_configuredState.set(ConvertStateFlag(state));
+ }
+ };
+ m_xdgToplevel.set_app_id(app_id);
+ m_xdgToplevel.set_title(title);
+ // Set sensible minimum size
+ m_xdgToplevel.set_min_size(300, 200);
+}
+
+void CShellSurfaceXdgShellUnstableV6::Initialize()
+{
+ // Commit surface to confirm role
+ // Don't do it in constructor since SetFullScreen might be called before
+ m_surface.commit();
+ // Make sure we get the initial configure before continuing
+ m_display.roundtrip();
+}
+
+void CShellSurfaceXdgShellUnstableV6::AckConfigure(std::uint32_t serial)
+{
+ m_xdgSurface.ack_configure(serial);
+}
+
+CShellSurfaceXdgShellUnstableV6::~CShellSurfaceXdgShellUnstableV6() noexcept
+{
+ // xdg_shell is picky: must destroy toplevel role before surface
+ m_xdgToplevel.proxy_release();
+ m_xdgSurface.proxy_release();
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetFullScreen(const wayland::output_t& output, float)
+{
+ // xdg_shell does not support refresh rate setting at the moment
+ m_xdgToplevel.set_fullscreen(output);
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetWindowed()
+{
+ m_xdgToplevel.unset_fullscreen();
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetMaximized()
+{
+ m_xdgToplevel.set_maximized();
+}
+
+void CShellSurfaceXdgShellUnstableV6::UnsetMaximized()
+{
+ m_xdgToplevel.unset_maximized();
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetMinimized()
+{
+ m_xdgToplevel.set_minimized();
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetWindowGeometry(CRectInt geometry)
+{
+ m_xdgSurface.set_window_geometry(geometry.x1, geometry.y1, geometry.Width(), geometry.Height());
+}
+
+void CShellSurfaceXdgShellUnstableV6::StartMove(const wayland::seat_t& seat, std::uint32_t serial)
+{
+ m_xdgToplevel.move(seat, serial);
+}
+
+void CShellSurfaceXdgShellUnstableV6::StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge)
+{
+ // wl_shell shell_surface_resize is identical to xdg_shell resize_edge
+ m_xdgToplevel.resize(seat, serial, static_cast<std::uint32_t> (edge));
+}
+
+void CShellSurfaceXdgShellUnstableV6::ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position)
+{
+ m_xdgToplevel.show_window_menu(seat, serial, position.x, position.y);
+} \ No newline at end of file
diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h
new file mode 100644
index 0000000..d84f4a5
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Connection.h"
+#include "ShellSurface.h"
+
+#include <wayland-extra-protocols.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Shell surface implementation for unstable xdg_shell in version 6
+ *
+ * xdg_shell was accepted as a stable protocol in wayland-protocols, which
+ * means this class is deprecated and can be safely removed once the relevant
+ * compositors have made the switch.
+ */
+class CShellSurfaceXdgShellUnstableV6 : public IShellSurface
+{
+public:
+ /**
+ * Construct xdg_shell toplevel object for given surface
+ *
+ * \param handler the shell surface handler
+ * \param display the wl_display global (for initial roundtrip)
+ * \param shell the zxdg_shell_v6 global
+ * \param surface surface to make shell surface for
+ * \param title title of the surfae
+ * \param class_ class of the surface, which should match the name of the
+ * .desktop file of the application
+ */
+ CShellSurfaceXdgShellUnstableV6(IShellSurfaceHandler& handler, wayland::display_t& display, wayland::zxdg_shell_v6_t const& shell, wayland::surface_t const& surface, std::string const& title, std::string const& class_);
+ ~CShellSurfaceXdgShellUnstableV6() noexcept override;
+
+ static CShellSurfaceXdgShellUnstableV6* TryCreate(IShellSurfaceHandler& handler, CConnection& connection, wayland::surface_t const& surface, std::string const& title, std::string const& class_);
+
+ void Initialize() override;
+
+ void SetFullScreen(wayland::output_t const& output, float refreshRate) override;
+ void SetWindowed() override;
+ void SetMaximized() override;
+ void UnsetMaximized() override;
+ void SetMinimized() override;
+ void SetWindowGeometry(CRectInt geometry) override;
+ void AckConfigure(std::uint32_t serial) override;
+
+ void StartMove(const wayland::seat_t& seat, std::uint32_t serial) override;
+ void StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge) override;
+ void ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position) override;
+
+private:
+ IShellSurfaceHandler& m_handler;
+ wayland::display_t& m_display;
+ wayland::zxdg_shell_v6_t m_shell;
+ wayland::surface_t m_surface;
+ wayland::zxdg_surface_v6_t m_xdgSurface;
+ wayland::zxdg_toplevel_v6_t m_xdgToplevel;
+
+ CSizeInt m_configuredSize;
+ StateBitset m_configuredState;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/Signals.h b/xbmc/windowing/wayland/Signals.h
new file mode 100644
index 0000000..59a1fdf
--- /dev/null
+++ b/xbmc/windowing/wayland/Signals.h
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <mutex>
+
+namespace KODI
+{
+
+using RegistrationIdentifierType = int;
+
+class ISignalHandlerData
+{
+protected:
+ ~ISignalHandlerData() = default;
+
+public:
+ virtual void Unregister(RegistrationIdentifierType id) = 0;
+};
+
+class CSignalRegistration
+{
+ std::weak_ptr<ISignalHandlerData> m_list;
+ RegistrationIdentifierType m_registration;
+
+ template<typename ManagedT>
+ friend class CSignalHandlerList;
+
+ CSignalRegistration(std::shared_ptr<ISignalHandlerData> const& list, RegistrationIdentifierType registration)
+ : m_list{list}, m_registration{registration}
+ {
+ }
+
+ CSignalRegistration(CSignalRegistration const& other) = delete;
+ CSignalRegistration& operator=(CSignalRegistration const& other) = delete;
+
+public:
+ CSignalRegistration() noexcept = default;
+
+ CSignalRegistration(CSignalRegistration&& other) noexcept
+ {
+ *this = std::move(other);
+ }
+
+ inline CSignalRegistration& operator=(CSignalRegistration&& other) noexcept
+ {
+ Unregister();
+ std::swap(m_list, other.m_list);
+ m_registration = other.m_registration;
+ return *this;
+ }
+
+ ~CSignalRegistration() noexcept
+ {
+ Unregister();
+ }
+
+ inline void Unregister()
+ {
+ if (auto list = m_list.lock())
+ {
+ list->Unregister(m_registration);
+ list.reset();
+ }
+ }
+};
+
+template<typename ManagedT>
+class CSignalHandlerList
+{
+ /**
+ * Internal storage for handler list
+ *
+ * Extra struct so memory handling with shared_ptr and weak_ptr can be done
+ * on this level
+ */
+ struct Data final : public ISignalHandlerData
+ {
+ CCriticalSection m_handlerCriticalSection;
+ std::map<RegistrationIdentifierType, ManagedT> m_handlers;
+
+ void Unregister(RegistrationIdentifierType id) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_handlerCriticalSection);
+ m_handlers.erase(id);
+ }
+ };
+
+ std::shared_ptr<Data> m_data;
+ RegistrationIdentifierType m_lastRegistrationId{};
+
+ CSignalHandlerList(CSignalHandlerList const& other) = delete;
+ CSignalHandlerList& operator=(CSignalHandlerList const& other) = delete;
+
+public:
+ CSignalHandlerList()
+ : m_data{new Data}
+ {}
+
+ CSignalRegistration Register(ManagedT const& handler)
+ {
+ std::unique_lock<CCriticalSection> lock(m_data->m_handlerCriticalSection);
+ bool inserted{false};
+ while(!inserted)
+ {
+ inserted = m_data->m_handlers.emplace(++m_lastRegistrationId, handler).second;
+ }
+ return {m_data, m_lastRegistrationId};
+ }
+
+ /**
+ * Invoke all registered signal handlers with the provided arguments
+ * when the signal type is a std::function or otherwise implements
+ * operator()
+ */
+ template<typename... ArgsT>
+ void Invoke(ArgsT&&... args)
+ {
+ std::unique_lock<CCriticalSection> lock(m_data->m_handlerCriticalSection);
+ for (auto const& handler : *this)
+ {
+ handler.second(std::forward<ArgsT>(args)...);
+ }
+ }
+
+ auto begin() const { return m_data->m_handlers.cbegin(); }
+
+ auto end() const { return m_data->m_handlers.cend(); }
+
+ /**
+ * Get critical section for accessing the handler list
+ * \note You must lock this yourself if you iterate through the handler
+ * list manually without using \ref Invoke or similar.
+ */
+ CCriticalSection const& CriticalSection() const
+ {
+ return m_data->m_handlerCriticalSection;
+ }
+};
+
+}
diff --git a/xbmc/windowing/wayland/Util.cpp b/xbmc/windowing/wayland/Util.cpp
new file mode 100644
index 0000000..707da11
--- /dev/null
+++ b/xbmc/windowing/wayland/Util.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "Util.h"
+
+#include <map>
+#include <string>
+
+#include <wayland-cursor.hpp>
+
+namespace
+{
+/*
+ * List from gdkcursor-wayland.c
+ *
+ * GDK - The GIMP Drawing Kit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+static std::map<std::string, std::string> CursorFallbackNameMap =
+{
+ { "default", "left_ptr" },
+ { "help", "question_arrow" },
+ { "context-menu", "left_ptr" },
+ { "pointer", "hand" },
+ { "progress", "left_ptr_watch" },
+ { "wait", "watch" },
+ { "cell", "crosshair" },
+ { "crosshair", "cross" },
+ { "text", "xterm" },
+ { "vertical-text","xterm" },
+ { "alias", "dnd-link" },
+ { "copy", "dnd-copy" },
+ { "move", "dnd-move" },
+ { "no-drop", "dnd-none" },
+ { "dnd-ask", "dnd-copy" }, // not CSS, but we want to guarantee it anyway
+ { "not-allowed", "crossed_circle" },
+ { "grab", "hand2" },
+ { "grabbing", "hand2" },
+ { "all-scroll", "left_ptr" },
+ { "col-resize", "h_double_arrow" },
+ { "row-resize", "v_double_arrow" },
+ { "n-resize", "top_side" },
+ { "e-resize", "right_side" },
+ { "s-resize", "bottom_side" },
+ { "w-resize", "left_side" },
+ { "ne-resize", "top_right_corner" },
+ { "nw-resize", "top_left_corner" },
+ { "se-resize", "bottom_right_corner" },
+ { "sw-resize", "bottom_left_corner" },
+ { "ew-resize", "h_double_arrow" },
+ { "ns-resize", "v_double_arrow" },
+ { "nesw-resize", "fd_double_arrow" },
+ { "nwse-resize", "bd_double_arrow" },
+ { "zoom-in", "left_ptr" },
+ { "zoom-out", "left_ptr" }
+};
+
+}
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+wayland::cursor_t CCursorUtil::LoadFromTheme(wayland::cursor_theme_t const& theme, std::string const& name)
+{
+ try
+ {
+ return theme.get_cursor(name);
+ }
+ catch (std::exception const&)
+ {
+ auto i = CursorFallbackNameMap.find(name);
+ if (i == CursorFallbackNameMap.end())
+ {
+ throw;
+ }
+ else
+ {
+ return theme.get_cursor(i->second);
+ }
+ }
+}
diff --git a/xbmc/windowing/wayland/Util.h b/xbmc/windowing/wayland/Util.h
new file mode 100644
index 0000000..ab13e29
--- /dev/null
+++ b/xbmc/windowing/wayland/Util.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <string>
+
+#include <wayland-client.hpp>
+#include <wayland-cursor.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+struct WaylandCPtrCompare
+{
+ bool operator()(wayland::proxy_t const& p1, wayland::proxy_t const& p2) const
+ {
+ return reinterpret_cast<std::uintptr_t>(p1.c_ptr()) < reinterpret_cast<std::uintptr_t>(p2.c_ptr());
+ }
+};
+
+class CCursorUtil
+{
+public:
+ /**
+ * Load a cursor from a theme with automatic fallback
+ *
+ * Modern cursor themes use CSS names for the cursors as defined in
+ * the XDG cursor-spec (draft at the moment), but older themes
+ * might still use the cursor names that were popular with X11.
+ * This function tries to load a cursor by the given CSS name and
+ * automatically falls back to the corresponding X11 name if the
+ * load fails.
+ *
+ * \param theme cursor theme to load from
+ * \param name CSS cursor name to load
+ * \return requested cursor
+ */
+ static wayland::cursor_t LoadFromTheme(wayland::cursor_theme_t const& theme, std::string const& name);
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/VideoSyncWpPresentation.cpp b/xbmc/windowing/wayland/VideoSyncWpPresentation.cpp
new file mode 100644
index 0000000..e050a2d
--- /dev/null
+++ b/xbmc/windowing/wayland/VideoSyncWpPresentation.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "VideoSyncWpPresentation.h"
+
+#include "settings/AdvancedSettings.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/wayland/WinSystemWayland.h"
+
+#include <cinttypes>
+#include <functional>
+
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+
+CVideoSyncWpPresentation::CVideoSyncWpPresentation(void* clock, CWinSystemWayland& winSystem)
+: CVideoSync(clock), m_winSystem(winSystem)
+{
+}
+
+bool CVideoSyncWpPresentation::Setup(PUPDATECLOCK func)
+{
+ UpdateClock = func;
+ m_stopEvent.Reset();
+ m_fps = m_winSystem.GetSyncOutputRefreshRate();
+
+ return true;
+}
+
+void CVideoSyncWpPresentation::Run(CEvent& stopEvent)
+{
+ m_presentationHandler = m_winSystem.RegisterOnPresentationFeedback(std::bind(&CVideoSyncWpPresentation::HandlePresentation, this, _1, _2, _3, _4, _5));
+
+ XbmcThreads::CEventGroup waitGroup{&stopEvent, &m_stopEvent};
+ waitGroup.wait();
+
+ m_presentationHandler.Unregister();
+}
+
+void CVideoSyncWpPresentation::Cleanup()
+{
+}
+
+float CVideoSyncWpPresentation::GetFps()
+{
+ return m_fps;
+}
+
+void CVideoSyncWpPresentation::HandlePresentation(timespec tv, std::uint32_t refresh, std::uint32_t syncOutputID, float syncOutputRefreshRate, std::uint64_t msc)
+{
+ auto mscDiff = msc - m_lastMsc;
+
+ CLog::Log(LOGDEBUG, LOGAVTIMING,
+ "VideoSyncWpPresentation: tv {}.{:09} s next refresh in +{} ns (fps {:f}) sync output "
+ "id {} fps {:f} msc {} mscdiff {}",
+ static_cast<std::uint64_t>(tv.tv_sec), static_cast<std::uint64_t>(tv.tv_nsec), refresh,
+ 1.0e9 / refresh, syncOutputID, syncOutputRefreshRate, msc, mscDiff);
+
+ if (m_fps != syncOutputRefreshRate || (m_syncOutputID != 0 && m_syncOutputID != syncOutputID))
+ {
+ // Restart if fps changes or sync output changes (which means that the msc jumps)
+ CLog::Log(LOGDEBUG, "fps or sync output changed, restarting Wayland video sync");
+ m_stopEvent.Set();
+ }
+ m_syncOutputID = syncOutputID;
+
+ if (m_lastMsc == 0)
+ {
+ // If this is the first time or MSC is not supported, assume we moved one frame
+ mscDiff = 1;
+ }
+ m_lastMsc = msc;
+
+ // FIXME use timespec instead of currenthostcounter()? Possibly difficult
+ // due to different clock base
+ UpdateClock(mscDiff, CurrentHostCounter(), m_refClock);
+}
diff --git a/xbmc/windowing/wayland/VideoSyncWpPresentation.h b/xbmc/windowing/wayland/VideoSyncWpPresentation.h
new file mode 100644
index 0000000..9d05550
--- /dev/null
+++ b/xbmc/windowing/wayland/VideoSyncWpPresentation.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Signals.h"
+#include "windowing/VideoSync.h"
+
+#include <cstdint>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CWinSystemWayland;
+
+class CVideoSyncWpPresentation : public CVideoSync
+{
+public:
+ explicit CVideoSyncWpPresentation(void* clock, CWinSystemWayland& winSystem);
+
+ float GetFps() override;
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stop) override;
+ void Cleanup() override;
+
+private:
+ void HandlePresentation(timespec tv, std::uint32_t refresh, std::uint32_t syncOutputID, float syncOutputRefreshRate, std::uint64_t msc);
+
+ CEvent m_stopEvent;
+ CSignalRegistration m_presentationHandler;
+ std::uint64_t m_lastMsc{};
+ std::uint32_t m_syncOutputID{};
+ CWinSystemWayland &m_winSystem;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinEventsWayland.cpp b/xbmc/windowing/wayland/WinEventsWayland.cpp
new file mode 100644
index 0000000..4345cf0
--- /dev/null
+++ b/xbmc/windowing/wayland/WinEventsWayland.cpp
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WinEventsWayland.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/log.h"
+
+#include "platform/posix/utils/FileHandle.h"
+
+#include <exception>
+#include <memory>
+#include <mutex>
+#include <system_error>
+
+#include <sys/poll.h>
+#include <unistd.h>
+#include <wayland-client.hpp>
+
+using namespace KODI::UTILS::POSIX;
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+/**
+ * Thread for processing Wayland events
+ *
+ * While not strictly needed, reading from the Wayland display file descriptor
+ * and dispatching the resulting events is done in an extra thread here.
+ * Sometime in the future, MessagePump() might be gone and then the
+ * transition will be easier since this extra thread is already here.
+ */
+class CWinEventsWaylandThread : CThread
+{
+ wayland::display_t& m_display;
+ // Pipe used for cancelling poll() on shutdown
+ CFileHandle m_pipeRead;
+ CFileHandle m_pipeWrite;
+
+ CCriticalSection m_roundtripQueueMutex;
+ std::atomic<wayland::event_queue_t*> m_roundtripQueue{nullptr};
+ CEvent m_roundtripQueueEvent;
+
+public:
+ CWinEventsWaylandThread(wayland::display_t& display)
+ : CThread("Wayland message pump"), m_display{display}
+ {
+ std::array<int, 2> fds;
+ if (pipe(fds.data()) < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "Error creating pipe for Wayland message pump cancellation");
+ }
+ m_pipeRead.attach(fds[0]);
+ m_pipeWrite.attach(fds[1]);
+ Create();
+ }
+
+ ~CWinEventsWaylandThread() override
+ {
+ Stop();
+ // Wait for roundtrip invocation to finish
+ std::unique_lock<CCriticalSection> lock(m_roundtripQueueMutex);
+ }
+
+ void Stop()
+ {
+ CLog::Log(LOGDEBUG, "Stopping Wayland message pump");
+ // Set m_bStop
+ StopThread(false);
+ InterruptPoll();
+ // Now wait for actual exit
+ StopThread(true);
+ }
+
+ void RoundtripQueue(wayland::event_queue_t const& queue)
+ {
+ wayland::event_queue_t queueCopy{queue};
+
+ // Serialize invocations of this function - it's used very rarely and usually
+ // not in parallel anyway, and doing it avoids lots of complications
+ std::unique_lock<CCriticalSection> lock(m_roundtripQueueMutex);
+
+ m_roundtripQueueEvent.Reset();
+ // We can just set the value here since there is no other writer in parallel
+ m_roundtripQueue.store(&queueCopy);
+ // Dispatching can happen now
+
+ // Make sure we don't wait for an event to happen on the socket
+ InterruptPoll();
+
+ if (m_bStop)
+ return;
+
+ m_roundtripQueueEvent.Wait();
+ }
+
+ wayland::display_t& GetDisplay()
+ {
+ return m_display;
+ }
+
+private:
+ void InterruptPoll()
+ {
+ char c = 0;
+ if (write(m_pipeWrite, &c, 1) != 1)
+ throw std::runtime_error("Failed to write to wayland message pipe");
+ }
+
+ void Process() override
+ {
+ try
+ {
+ std::array<pollfd, 2> pollFds;
+ pollfd& waylandPoll = pollFds[0];
+ pollfd& cancelPoll = pollFds[1];
+ // Wayland filedescriptor
+ waylandPoll.fd = m_display.get_fd();
+ waylandPoll.events = POLLIN;
+ waylandPoll.revents = 0;
+ // Read end of the cancellation pipe
+ cancelPoll.fd = m_pipeRead;
+ cancelPoll.events = POLLIN;
+ cancelPoll.revents = 0;
+
+ CLog::Log(LOGDEBUG, "Starting Wayland message pump");
+
+ // Run until cancelled or error
+ while (!m_bStop)
+ {
+ // dispatch() provides no way to cancel a blocked read from the socket
+ // wl_display_disconnect would just close the socket, leading to problems
+ // with the poll() that dispatch() uses internally - so we have to implement
+ // cancellation ourselves here
+
+ // Acquire global read intent
+ wayland::read_intent readIntent = m_display.obtain_read_intent();
+ m_display.flush();
+
+ if (poll(pollFds.data(), pollFds.size(), -1) < 0)
+ {
+ if (errno == EINTR)
+ {
+ continue;
+ }
+ else
+ {
+ throw std::system_error(errno, std::generic_category(), "Error polling on Wayland socket");
+ }
+ }
+
+ if (cancelPoll.revents & POLLERR || cancelPoll.revents & POLLHUP || cancelPoll.revents & POLLNVAL)
+ {
+ throw std::runtime_error("poll() signalled error condition on poll interruption socket");
+ }
+
+ if (waylandPoll.revents & POLLERR || waylandPoll.revents & POLLHUP || waylandPoll.revents & POLLNVAL)
+ {
+ throw std::runtime_error("poll() signalled error condition on Wayland socket");
+ }
+
+ // Read events and release intent; this does not block
+ readIntent.read();
+ // Dispatch default event queue
+ m_display.dispatch_pending();
+
+ if (auto* roundtripQueue = m_roundtripQueue.exchange(nullptr))
+ {
+ m_display.roundtrip_queue(*roundtripQueue);
+ m_roundtripQueueEvent.Set();
+ }
+ if (cancelPoll.revents & POLLIN)
+ {
+ // Read away the char so we don't get another notification
+ // Indepentent from m_roundtripQueue so there are no races
+ char c;
+ if (read(m_pipeRead, &c, 1) != 1)
+ throw std::runtime_error("Error reading from wayland message pipe");
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "Wayland message pump stopped");
+ }
+ catch (std::exception const& e)
+ {
+ // FIXME CThread::OnException is very badly named and should probably go away
+ // FIXME Thread exception handling is seriously broken:
+ // Exceptions will be swallowed and do not terminate the program.
+ // Even XbmcCommons::UncheckedException which claims to be there for just this
+ // purpose does not cause termination, the log message will just be slightly different.
+
+ // But here, going on would be meaningless, so do a hard exit
+ CLog::Log(LOGFATAL, "Exception in Wayland message pump, exiting: {}", e.what());
+ std::terminate();
+ }
+
+ // Wake up if someone is still waiting for roundtrip, won't happen anytime soon...
+ m_roundtripQueueEvent.Set();
+ }
+};
+
+std::unique_ptr<CWinEventsWaylandThread> g_WlMessagePump{nullptr};
+
+}
+
+void CWinEventsWayland::SetDisplay(wayland::display_t* display)
+{
+ if (display && !g_WlMessagePump)
+ {
+ // Start message processing as soon as we have a display
+ g_WlMessagePump.reset(new CWinEventsWaylandThread(*display));
+ }
+ else if (g_WlMessagePump)
+ {
+ // Stop if display is set to nullptr
+ g_WlMessagePump.reset();
+ }
+}
+
+void CWinEventsWayland::Flush()
+{
+ if (g_WlMessagePump)
+ {
+ g_WlMessagePump->GetDisplay().flush();
+ }
+}
+
+void CWinEventsWayland::RoundtripQueue(const wayland::event_queue_t& queue)
+{
+ if (g_WlMessagePump)
+ {
+ g_WlMessagePump->RoundtripQueue(queue);
+ }
+}
+
+bool CWinEventsWayland::MessagePump()
+{
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ // Forward any events that may have been pushed to our queue
+ while (true)
+ {
+ XBMC_Event event;
+ {
+ // Scoped lock for reentrancy
+ std::unique_lock<CCriticalSection> lock(m_queueMutex);
+
+ if (m_queue.empty())
+ {
+ break;
+ }
+
+ // First get event and remove it from the queue, then pass it on - be aware that this
+ // function must be reentrant
+ event = m_queue.front();
+ m_queue.pop();
+ }
+
+ if (appPort)
+ appPort->OnEvent(event);
+ }
+
+ return true;
+}
+
+void CWinEventsWayland::MessagePush(XBMC_Event* ev)
+{
+ std::unique_lock<CCriticalSection> lock(m_queueMutex);
+ m_queue.emplace(*ev);
+}
diff --git a/xbmc/windowing/wayland/WinEventsWayland.h b/xbmc/windowing/wayland/WinEventsWayland.h
new file mode 100644
index 0000000..9390956
--- /dev/null
+++ b/xbmc/windowing/wayland/WinEventsWayland.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "../WinEvents.h"
+#include "threads/CriticalSection.h"
+
+#include <queue>
+
+namespace wayland
+{
+class event_queue_t;
+class display_t;
+}
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CWinEventsWayland : public IWinEvents
+{
+public:
+ bool MessagePump() override;
+ void MessagePush(XBMC_Event* ev);
+ /// Write buffered messages to the compositor
+ static void Flush();
+ /// Do a roundtrip on the specified queue from the event processing thread
+ static void RoundtripQueue(wayland::event_queue_t const& queue);
+
+private:
+ friend class CWinSystemWayland;
+ static void SetDisplay(wayland::display_t* display);
+
+ CCriticalSection m_queueMutex;
+ std::queue<XBMC_Event> m_queue;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinSystemWayland.cpp b/xbmc/windowing/wayland/WinSystemWayland.cpp
new file mode 100644
index 0000000..9733339
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWayland.cpp
@@ -0,0 +1,1568 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WinSystemWayland.h"
+
+#include "CompileInfo.h"
+#include "Connection.h"
+#include "OSScreenSaverIdleInhibitUnstableV1.h"
+#include "OptionalsReg.h"
+#include "Registry.h"
+#include "ServiceBroker.h"
+#include "ShellSurfaceWlShell.h"
+#include "ShellSurfaceXdgShell.h"
+#include "ShellSurfaceXdgShellUnstableV6.h"
+#include "Util.h"
+#include "VideoSyncWpPresentation.h"
+#include "WinEventsWayland.h"
+#include "WindowDecorator.h"
+#include "application/Application.h"
+#include "cores/RetroPlayer/process/wayland/RPProcessInfoWayland.h"
+#include "cores/VideoPlayer/Process/wayland/ProcessInfoWayland.h"
+#include "guilib/DispResource.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/InputManager.h"
+#include "input/touch/generic/GenericTouchActionHandler.h"
+#include "input/touch/generic/GenericTouchInputHandler.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/ActorProtocol.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/linux/OSScreenSaverFreedesktop.h"
+
+#include "platform/linux/TimeUtils.h"
+
+#include <algorithm>
+#include <limits>
+#include <mutex>
+#include <numeric>
+
+#if defined(HAS_DBUS)
+# include "windowing/linux/OSScreenSaverFreedesktop.h"
+#endif
+
+using namespace KODI::WINDOWING;
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+using namespace std::chrono_literals;
+
+namespace
+{
+
+RESOLUTION FindMatchingCustomResolution(CSizeInt size, float refreshRate)
+{
+ for (size_t res{RES_DESKTOP}; res < CDisplaySettings::GetInstance().ResolutionInfoSize(); ++res)
+ {
+ auto const& resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+ if (resInfo.iWidth == size.Width() && resInfo.iHeight == size.Height() && MathUtils::FloatEquals(resInfo.fRefreshRate, refreshRate, 0.0005f))
+ {
+ return static_cast<RESOLUTION> (res);
+ }
+ }
+ return RES_INVALID;
+}
+
+struct OutputScaleComparer
+{
+ bool operator()(std::shared_ptr<COutput> const& output1, std::shared_ptr<COutput> const& output2)
+ {
+ return output1->GetScale() < output2->GetScale();
+ }
+};
+
+struct OutputCurrentRefreshRateComparer
+{
+ bool operator()(std::shared_ptr<COutput> const& output1, std::shared_ptr<COutput> const& output2)
+ {
+ return output1->GetCurrentMode().refreshMilliHz < output2->GetCurrentMode().refreshMilliHz;
+ }
+};
+
+/// Scope guard for Actor::Message
+class MessageHandle : public KODI::UTILS::CScopeGuard<Actor::Message*, nullptr, void(Actor::Message*)>
+{
+public:
+ MessageHandle() : CScopeGuard{std::bind(&Actor::Message::Release, std::placeholders::_1), nullptr} {}
+ explicit MessageHandle(Actor::Message* message) : CScopeGuard{std::bind(&Actor::Message::Release, std::placeholders::_1), message} {}
+ Actor::Message* Get() { return static_cast<Actor::Message*> (*this); }
+};
+
+/**
+ * Protocol for communication between Wayland event thread and main thread
+ *
+ * Many messages received from the Wayland compositor must be processed at a
+ * defined time between frame rendering, such as resolution switches. Thus
+ * they are pushed to the main thread for processing.
+ *
+ * The protocol is strictly uni-directional from event to main thread at the moment,
+ * so \ref Actor::Protocol is mainly used as an event queue.
+ */
+namespace WinSystemWaylandProtocol
+{
+
+enum OutMessage
+{
+ CONFIGURE,
+ OUTPUT_HOTPLUG,
+ BUFFER_SCALE
+};
+
+struct MsgConfigure
+{
+ std::uint32_t serial;
+ CSizeInt surfaceSize;
+ IShellSurface::StateBitset state;
+};
+
+struct MsgBufferScale
+{
+ int scale;
+};
+
+};
+
+}
+
+CWinSystemWayland::CWinSystemWayland()
+: CWinSystemBase{}, m_protocol{"WinSystemWaylandInternal"}
+{
+ m_winEvents.reset(new CWinEventsWayland());
+}
+
+CWinSystemWayland::~CWinSystemWayland() noexcept
+{
+ DestroyWindowSystem();
+}
+
+bool CWinSystemWayland::InitWindowSystem()
+{
+ const char* env = getenv("WAYLAND_DISPLAY");
+ if (!env)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemWayland::{} - WAYLAND_DISPLAY env not set", __FUNCTION__);
+ return false;
+ }
+
+ wayland::set_log_handler([](const std::string& message)
+ { CLog::Log(LOGWARNING, "wayland-client log message: {}", message); });
+
+ CLog::LogF(LOGINFO, "Connecting to Wayland server");
+ m_connection = std::make_unique<CConnection>();
+ if (!m_connection->HasDisplay())
+ return false;
+
+ VIDEOPLAYER::CProcessInfoWayland::Register();
+ RETRO::CRPProcessInfoWayland::Register();
+
+ m_registry.reset(new CRegistry{*m_connection});
+
+ m_registry->RequestSingleton(m_compositor, 1, 4);
+ m_registry->RequestSingleton(m_shm, 1, 1);
+ m_registry->RequestSingleton(m_presentation, 1, 1, false);
+ // version 2 adds done() -> required
+ // version 3 adds destructor -> optional
+ m_registry->Request<wayland::output_t>(2, 3, std::bind(&CWinSystemWayland::OnOutputAdded, this, _1, _2), std::bind(&CWinSystemWayland::OnOutputRemoved, this, _1));
+
+ m_registry->Bind();
+
+ if (m_presentation)
+ {
+ m_presentation.on_clock_id() = [this](std::uint32_t clockId)
+ {
+ CLog::Log(LOGINFO, "Wayland presentation clock: {}", clockId);
+ m_presentationClock = static_cast<clockid_t> (clockId);
+ };
+ }
+
+ // Do another roundtrip to get initial wl_output information
+ m_connection->GetDisplay().roundtrip();
+ if (m_outputs.empty())
+ {
+ throw std::runtime_error("No outputs received from compositor");
+ }
+
+ // Event loop is started in CreateWindow
+
+ // pointer is by default not on this window, will be immediately rectified
+ // by the enter() events if it is
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ // Always use the generic touch action handler
+ CGenericTouchInputHandler::GetInstance().RegisterHandler(&CGenericTouchActionHandler::GetInstance());
+
+ CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetSetting(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE)
+ ->SetVisible(true);
+
+ return CWinSystemBase::InitWindowSystem();
+}
+
+bool CWinSystemWayland::DestroyWindowSystem()
+{
+ DestroyWindow();
+ // wl_display_disconnect frees all proxy objects, so we have to make sure
+ // all stuff is gone on the C++ side before that
+ m_cursorSurface = wayland::surface_t{};
+ m_cursorBuffer = wayland::buffer_t{};
+ m_cursorImage = wayland::cursor_image_t{};
+ m_cursorTheme = wayland::cursor_theme_t{};
+ m_outputsInPreparation.clear();
+ m_outputs.clear();
+ m_frameCallback = wayland::callback_t{};
+ m_screenSaverManager.reset();
+
+ m_seatInputProcessing.reset();
+
+ if (m_registry)
+ {
+ m_registry->UnbindSingletons();
+ }
+ m_registry.reset();
+ m_connection.reset();
+
+ CGenericTouchInputHandler::GetInstance().UnregisterHandler();
+
+ return CWinSystemBase::DestroyWindowSystem();
+}
+
+bool CWinSystemWayland::CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res)
+{
+ CLog::LogF(LOGINFO, "Starting {} size {}x{}", fullScreen ? "full screen" : "windowed", res.iWidth,
+ res.iHeight);
+
+ m_surface = m_compositor.create_surface();
+ m_surface.on_enter() = [this](const wayland::output_t& wloutput) {
+ if (auto output = FindOutputByWaylandOutput(wloutput))
+ {
+ CLog::Log(LOGDEBUG, "Entering output \"{}\" with scale {} and {:.3f} dpi",
+ UserFriendlyOutputName(output), output->GetScale(), output->GetCurrentDpi());
+ std::unique_lock<CCriticalSection> lock(m_surfaceOutputsMutex);
+ m_surfaceOutputs.emplace(output);
+ lock.unlock();
+ UpdateBufferScale();
+ UpdateTouchDpi();
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Entering output that was not configured yet, ignoring");
+ }
+ };
+ m_surface.on_leave() = [this](const wayland::output_t& wloutput) {
+ if (auto output = FindOutputByWaylandOutput(wloutput))
+ {
+ CLog::Log(LOGDEBUG, "Leaving output \"{}\" with scale {}", UserFriendlyOutputName(output),
+ output->GetScale());
+ std::unique_lock<CCriticalSection> lock(m_surfaceOutputsMutex);
+ m_surfaceOutputs.erase(output);
+ lock.unlock();
+ UpdateBufferScale();
+ UpdateTouchDpi();
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Leaving output that was not configured yet, ignoring");
+ }
+ };
+
+ m_windowDecorator.reset(new CWindowDecorator(*this, *m_connection, m_surface));
+
+ m_seatInputProcessing.reset(new CSeatInputProcessing(m_surface, *this));
+ m_seatRegistry.reset(new CRegistry{*m_connection});
+ // version 2 adds name event -> optional
+ // version 4 adds wl_keyboard repeat_info -> optional
+ // version 5 adds discrete axis events in wl_pointer -> unused
+ m_seatRegistry->Request<wayland::seat_t>(1, 5, std::bind(&CWinSystemWayland::OnSeatAdded, this, _1, _2), std::bind(&CWinSystemWayland::OnSeatRemoved, this, _1));
+ m_seatRegistry->Bind();
+
+ if (m_seats.empty())
+ {
+ CLog::Log(LOGWARNING, "Wayland compositor did not announce a wl_seat - you will not have any input devices for the time being");
+ }
+
+ if (fullScreen)
+ {
+ m_shellSurfaceState.set(IShellSurface::STATE_FULLSCREEN);
+ }
+ // Assume we're active on startup until someone tells us otherwise
+ m_shellSurfaceState.set(IShellSurface::STATE_ACTIVATED);
+ // Try with this resolution if compositor does not say otherwise
+ UpdateSizeVariables({res.iWidth, res.iHeight}, m_scale, m_shellSurfaceState, false);
+
+ // Use AppName as the desktop file name. This is required to lookup the app icon of the same name.
+ m_shellSurface.reset(CShellSurfaceXdgShell::TryCreate(*this, *m_connection, m_surface, name,
+ std::string(CCompileInfo::GetAppName())));
+ if (!m_shellSurface)
+ {
+ m_shellSurface.reset(CShellSurfaceXdgShellUnstableV6::TryCreate(
+ *this, *m_connection, m_surface, name, std::string(CCompileInfo::GetAppName())));
+ }
+ if (!m_shellSurface)
+ {
+ CLog::LogF(LOGWARNING, "Compositor does not support xdg_shell protocol (stable or unstable v6) - falling back to wl_shell, not all features might work");
+ m_shellSurface.reset(new CShellSurfaceWlShell(*this, *m_connection, m_surface, name,
+ std::string(CCompileInfo::GetAppName())));
+ }
+
+ if (fullScreen)
+ {
+ // Try to start on correct monitor and with correct buffer scale
+ auto output = FindOutputByUserFriendlyName(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+ auto wlOutput = output ? output->GetWaylandOutput() : wayland::output_t{};
+ m_lastSetOutput = wlOutput;
+ m_shellSurface->SetFullScreen(wlOutput, res.fRefreshRate);
+ if (output && m_surface.can_set_buffer_scale())
+ {
+ m_scale = output->GetScale();
+ ApplyBufferScale();
+ }
+ }
+
+ // Just remember initial width/height for context creation in OnConfigure
+ // This is used for sizing the EGLSurface
+ m_shellSurfaceInitializing = true;
+ m_shellSurface->Initialize();
+ m_shellSurfaceInitializing = false;
+
+ // Apply window decorations if necessary
+ m_windowDecorator->SetState(m_configuredSize, m_scale, m_shellSurfaceState);
+
+ // Set initial opaque region and window geometry
+ ApplyOpaqueRegion();
+ ApplyWindowGeometry();
+
+ // Update resolution with real size as it could have changed due to configure()
+ UpdateDesktopResolution(res, res.strOutput, m_bufferSize.Width(), m_bufferSize.Height(), res.fRefreshRate, 0);
+ res.bFullScreen = fullScreen;
+
+ // Now start processing events
+ //
+ // There are two stages to the event handling:
+ // * Initialization (which ends here): Everything runs synchronously and init
+ // code that needs events processed must call roundtrip().
+ // This is done for simplicity because it is a lot easier than to make
+ // everything event-based and thread-safe everywhere in the startup code,
+ // which is also not really necessary.
+ // * Runtime (which starts here): Every object creation from now on
+ // needs to take great care to be thread-safe:
+ // Since the event pump is always running now, there is a tiny window between
+ // creating an object and attaching the C++ event handlers during which
+ // events can get queued and dispatched for the object but the handlers have
+ // not been set yet. Consequently, the events would get lost.
+ // However, this does not apply to objects that are created in response to
+ // compositor events. Since the callbacks are called from the event processing
+ // thread and ran strictly sequentially, no other events are dispatched during
+ // the runtime of a callback. Luckily this applies to global binding like
+ // wl_output and wl_seat and thus to most if not all runtime object creation
+ // cases we have to support.
+ // There is another problem when Wayland objects are destructed from the main
+ // thread: An event handler could be running in parallel, resulting in certain
+ // doom. So objects should only be deleted in response to compositor events, too.
+ // They might be hiding behind class member variables, so be wary.
+ // Note that this does not apply to global teardown since the event pump is
+ // stopped then.
+ CWinEventsWayland::SetDisplay(&m_connection->GetDisplay());
+
+ return true;
+}
+
+bool CWinSystemWayland::DestroyWindow()
+{
+ // Make sure no more events get processed when we kill the instances
+ CWinEventsWayland::SetDisplay(nullptr);
+
+ m_shellSurface.reset();
+ // waylandpp automatically calls wl_surface_destroy when the last reference is removed
+ m_surface = wayland::surface_t();
+ m_windowDecorator.reset();
+ m_seats.clear();
+ m_lastSetOutput.proxy_release();
+ m_surfaceOutputs.clear();
+ m_surfaceSubmissions.clear();
+ m_seatRegistry.reset();
+
+ return true;
+}
+
+bool CWinSystemWayland::CanDoWindowed()
+{
+ return true;
+}
+
+std::vector<std::string> CWinSystemWayland::GetConnectedOutputs()
+{
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ std::vector<std::string> outputs;
+ std::transform(m_outputs.cbegin(), m_outputs.cend(), std::back_inserter(outputs),
+ [this](decltype(m_outputs)::value_type const& pair)
+ { return UserFriendlyOutputName(pair.second); });
+
+ return outputs;
+}
+
+bool CWinSystemWayland::UseLimitedColor()
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE);
+}
+
+void CWinSystemWayland::UpdateResolutions()
+{
+ CWinSystemBase::UpdateResolutions();
+
+ CDisplaySettings::GetInstance().ClearCustomResolutions();
+
+ // Mimic X11:
+ // Only show resolutions for the currently selected output
+ std::string userOutput = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+
+ if (m_outputs.empty())
+ {
+ // *Usually* this should not happen - just give up
+ return;
+ }
+
+ auto output = FindOutputByUserFriendlyName(userOutput);
+ if (!output && m_lastSetOutput)
+ {
+ // Fallback to current output
+ output = FindOutputByWaylandOutput(m_lastSetOutput);
+ }
+ if (!output)
+ {
+ // Well just use the first one
+ output = m_outputs.begin()->second;
+ }
+
+ std::string outputName = UserFriendlyOutputName(output);
+
+ auto const& modes = output->GetModes();
+ auto const& currentMode = output->GetCurrentMode();
+ auto physicalSize = output->GetPhysicalSize();
+ CLog::LogF(LOGINFO,
+ "User wanted output \"{}\", we now have \"{}\" size {}x{} mm with {} mode(s):",
+ userOutput, outputName, physicalSize.Width(), physicalSize.Height(), modes.size());
+
+ for (auto const& mode : modes)
+ {
+ bool isCurrent = (mode == currentMode);
+ float pixelRatio = output->GetPixelRatioForMode(mode);
+ CLog::LogF(LOGINFO, "- {}x{} @{:.3f} Hz pixel ratio {:.3f}{}", mode.size.Width(),
+ mode.size.Height(), mode.refreshMilliHz / 1000.0f, pixelRatio,
+ isCurrent ? " current" : "");
+
+ RESOLUTION_INFO res;
+ UpdateDesktopResolution(res, outputName, mode.size.Width(), mode.size.Height(), mode.GetRefreshInHz(), 0);
+ res.fPixelRatio = pixelRatio;
+
+ if (isCurrent)
+ {
+ CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP) = res;
+ }
+ else
+ {
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ }
+ }
+
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+std::shared_ptr<COutput> CWinSystemWayland::FindOutputByUserFriendlyName(const std::string& name)
+{
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ auto outputIt = std::find_if(m_outputs.begin(), m_outputs.end(),
+ [this, &name](decltype(m_outputs)::value_type const& entry)
+ {
+ return (name == UserFriendlyOutputName(entry.second));
+ });
+
+ return (outputIt == m_outputs.end() ? nullptr : outputIt->second);
+}
+
+std::shared_ptr<COutput> CWinSystemWayland::FindOutputByWaylandOutput(wayland::output_t const& output)
+{
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ auto outputIt = std::find_if(m_outputs.begin(), m_outputs.end(),
+ [&output](decltype(m_outputs)::value_type const& entry)
+ {
+ return (output == entry.second->GetWaylandOutput());
+ });
+
+ return (outputIt == m_outputs.end() ? nullptr : outputIt->second);
+}
+
+/**
+ * Change resolution and window state on Kodi request
+ *
+ * This function is used for updating resolution when Kodi initiates a resolution
+ * change, such as when changing between full screen and windowed mode or when
+ * selecting a different monitor or resolution in the settings.
+ *
+ * Size updates originating from compositor events (such as configure or buffer
+ * scale changes) should not use this function, but \ref SetResolutionInternal
+ * instead.
+ *
+ * \param fullScreen whether to go full screen or windowed
+ * \param res resolution to set
+ * \return whether the requested resolution was actually set - is false e.g.
+ * when already in full screen mode since the application cannot
+ * set the size then
+ */
+bool CWinSystemWayland::SetResolutionExternal(bool fullScreen, RESOLUTION_INFO const& res)
+{
+ // In fullscreen modes, we never change the surface size on Kodi's request,
+ // but only when the compositor tells us to. At least xdg_shell specifies
+ // that with state fullscreen the dimensions given in configure() must
+ // always be observed.
+ // This does mean that the compositor has no way of knowing which resolution
+ // we would (in theory) want. Since no compositor implements dynamic resolution
+ // switching at the moment, this is not a problem. If it is some day implemented
+ // in compositors, this code must be changed to match the behavior that is
+ // expected then anyway.
+
+ // We can honor the Kodi-requested size only if we are not bound by configure rules,
+ // which applies for maximized and fullscreen states.
+ // Also, setting an unconfigured size when just going fullscreen makes no sense.
+ // Give precedence to the size we have still pending, if any.
+ bool mustHonorSize{m_waitingForApply || m_shellSurfaceState.test(IShellSurface::STATE_MAXIMIZED) || m_shellSurfaceState.test(IShellSurface::STATE_FULLSCREEN) || fullScreen};
+
+ CLog::LogF(LOGINFO, "Kodi asked to switch mode to {}x{} @{:.3f} Hz on output \"{}\" {}",
+ res.iWidth, res.iHeight, res.fRefreshRate, res.strOutput,
+ fullScreen ? "full screen" : "windowed");
+
+ if (fullScreen)
+ {
+ // Try to match output
+ auto output = FindOutputByUserFriendlyName(res.strOutput);
+ auto wlOutput = output ? output->GetWaylandOutput() : wayland::output_t{};
+ if (!m_shellSurfaceState.test(IShellSurface::STATE_FULLSCREEN) || (m_lastSetOutput != wlOutput))
+ {
+ // Remember the output we set last so we don't set it again until we
+ // either go windowed or were on a different output
+ m_lastSetOutput = wlOutput;
+
+ if (output)
+ {
+ CLog::LogF(LOGDEBUG, "Resolved output \"{}\" to bound Wayland global {}", res.strOutput,
+ output->GetGlobalName());
+ }
+ else
+ {
+ CLog::LogF(LOGINFO,
+ "Could not match output \"{}\" to a currently available Wayland output, falling "
+ "back to default output",
+ res.strOutput);
+ }
+
+ CLog::LogF(LOGDEBUG, "Setting full-screen with refresh rate {:.3f}", res.fRefreshRate);
+ m_shellSurface->SetFullScreen(wlOutput, res.fRefreshRate);
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "Not setting full screen: already full screen on requested output");
+ }
+ }
+ else
+ {
+ if (m_shellSurfaceState.test(IShellSurface::STATE_FULLSCREEN))
+ {
+ CLog::LogF(LOGDEBUG, "Setting windowed");
+ m_shellSurface->SetWindowed();
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "Not setting windowed: already windowed");
+ }
+ }
+
+ // Set Kodi-provided size only if we are free to choose any size, otherwise
+ // wait for the compositor configure
+ if (!mustHonorSize)
+ {
+ CLog::LogF(LOGDEBUG, "Directly setting windowed size {}x{} on Kodi request", res.iWidth,
+ res.iHeight);
+ // Kodi is directly setting window size, apply
+ auto updateResult = UpdateSizeVariables({res.iWidth, res.iHeight}, m_scale, m_shellSurfaceState, false);
+ ApplySizeUpdate(updateResult);
+ }
+
+ bool wasInitialSetFullScreen{m_isInitialSetFullScreen};
+ m_isInitialSetFullScreen = false;
+
+ // Need to return true
+ // * when this SetFullScreen() call was free to change the context size (and possibly did so)
+ // * on first SetFullScreen so GraphicsContext gets resolution
+ // Otherwise, Kodi must keep the old resolution.
+ return !mustHonorSize || wasInitialSetFullScreen;
+}
+
+bool CWinSystemWayland::ResizeWindow(int, int, int, int)
+{
+ // CGraphicContext is "smart" and calls ResizeWindow or SetFullScreen depending
+ // on some state like whether we were already fullscreen. But actually the processing
+ // here is always identical, so we are using a common function to handle both.
+ const auto& res = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ // The newWidth/newHeight parameters are taken from RES_WINDOW anyway, so we can just
+ // ignore them
+ return SetResolutionExternal(false, res);
+}
+
+bool CWinSystemWayland::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool)
+{
+ return SetResolutionExternal(fullScreen, res);
+}
+
+void CWinSystemWayland::ApplySizeUpdate(SizeUpdateInformation update)
+{
+ if (update.bufferScaleChanged)
+ {
+ // Buffer scale must also match egl size configuration
+ ApplyBufferScale();
+ }
+ if (update.surfaceSizeChanged)
+ {
+ // Update opaque region here so size always matches the configured egl surface
+ ApplyOpaqueRegion();
+ }
+ if (update.configuredSizeChanged)
+ {
+ // Update window decoration state
+ m_windowDecorator->SetState(m_configuredSize, m_scale, m_shellSurfaceState);
+ ApplyWindowGeometry();
+ }
+ // Set always, because of initialization order GL context has to keep track of
+ // whether the size changed. If we skip based on update.bufferSizeChanged here,
+ // GL context will never get its initial size set.
+ SetContextSize(m_bufferSize);
+}
+
+void CWinSystemWayland::ApplyOpaqueRegion()
+{
+ // Mark everything opaque so the compositor can render it faster
+ CLog::LogF(LOGDEBUG, "Setting opaque region size {}x{}", m_surfaceSize.Width(),
+ m_surfaceSize.Height());
+ wayland::region_t opaqueRegion{m_compositor.create_region()};
+ opaqueRegion.add(0, 0, m_surfaceSize.Width(), m_surfaceSize.Height());
+ m_surface.set_opaque_region(opaqueRegion);
+}
+
+void CWinSystemWayland::ApplyWindowGeometry()
+{
+ m_shellSurface->SetWindowGeometry(m_windowDecorator->GetWindowGeometry());
+}
+
+void CWinSystemWayland::ProcessMessages()
+{
+ if (m_waitingForApply)
+ {
+ // Do not put multiple size updates into the pipeline, this would only make
+ // it more complicated without any real benefit. Wait until the size was reconfigured,
+ // then process events again.
+ return;
+ }
+
+ Actor::Message* message{};
+ MessageHandle lastConfigureMessage;
+ int skippedConfigures{-1};
+ int newScale{m_scale};
+
+ while (m_protocol.ReceiveOutMessage(&message))
+ {
+ MessageHandle guard{message};
+ switch (message->signal)
+ {
+ case WinSystemWaylandProtocol::CONFIGURE:
+ // Do not directly process configures, get the last one queued:
+ // While resizing, the compositor will usually send a configure event
+ // each time the mouse moves without any throttling (i.e. multiple times
+ // per rendered frame).
+ // Going through all those and applying them would waste a lot of time when
+ // we already know that the size is not final and will change again anyway.
+ skippedConfigures++;
+ lastConfigureMessage = std::move(guard);
+ break;
+ case WinSystemWaylandProtocol::OUTPUT_HOTPLUG:
+ {
+ CLog::LogF(LOGDEBUG, "Output hotplug, re-reading resolutions");
+ UpdateResolutions();
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ auto const& desktopRes = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP);
+ auto output = FindOutputByUserFriendlyName(desktopRes.strOutput);
+ auto const& wlOutput = output->GetWaylandOutput();
+ // Maybe the output that was added was the one we should be on?
+ if (m_bFullScreen && m_lastSetOutput != wlOutput)
+ {
+ CLog::LogF(LOGDEBUG, "Output hotplug resulted in monitor set in settings appearing, switching");
+ // Switch to this output
+ m_lastSetOutput = wlOutput;
+ m_shellSurface->SetFullScreen(wlOutput, desktopRes.fRefreshRate);
+ // SetOutput will result in a configure that updates the actual context size
+ }
+ }
+ break;
+ case WinSystemWaylandProtocol::BUFFER_SCALE:
+ // Never update buffer scale if not possible to set it
+ if (m_surface.can_set_buffer_scale())
+ {
+ newScale = (reinterpret_cast<WinSystemWaylandProtocol::MsgBufferScale*> (message->data))->scale;
+ }
+ break;
+ }
+ }
+
+ if (lastConfigureMessage)
+ {
+ if (skippedConfigures > 0)
+ {
+ CLog::LogF(LOGDEBUG, "Skipped {} configures", skippedConfigures);
+ }
+ // Wayland will tell us here the size of the surface that was actually created,
+ // which might be different from what we expected e.g. when fullscreening
+ // on an output we chose - the compositor might have decided to use a different
+ // output for example
+ // It is very important that the EGL native module and the rendering system use the
+ // Wayland-announced size for rendering or corrupted graphics output will result.
+ auto configure = reinterpret_cast<WinSystemWaylandProtocol::MsgConfigure*> (lastConfigureMessage.Get()->data);
+ CLog::LogF(LOGDEBUG, "Configure serial {}: size {}x{} state {}", configure->serial,
+ configure->surfaceSize.Width(), configure->surfaceSize.Height(),
+ IShellSurface::StateToString(configure->state));
+
+
+ CSizeInt size = configure->surfaceSize;
+ bool sizeIncludesDecoration = true;
+
+ if (size.IsZero())
+ {
+ if (configure->state.test(IShellSurface::STATE_FULLSCREEN))
+ {
+ // Do not change current size - UpdateWithConfiguredSize must be called regardless in case
+ // scale or something else changed
+ size = m_configuredSize;
+ }
+ else
+ {
+ // Compositor has no preference and we're windowed
+ // -> adopt windowed size that Kodi wants
+ auto const& windowed = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ // Kodi resolution is buffer size, but SetResolutionInternal expects
+ // surface size, so divide by m_scale
+ size = CSizeInt{windowed.iWidth, windowed.iHeight} / newScale;
+ CLog::LogF(LOGDEBUG, "Adapting Kodi windowed size {}x{}", size.Width(), size.Height());
+ sizeIncludesDecoration = false;
+ }
+ }
+
+ SetResolutionInternal(size, newScale, configure->state, sizeIncludesDecoration, true, configure->serial);
+ }
+ // If we were also configured, scale is already taken care of. But it could
+ // also be a scale change without configure, so apply that.
+ else if (m_scale != newScale)
+ {
+ SetResolutionInternal(m_configuredSize, newScale, m_shellSurfaceState, true, false);
+ }
+}
+
+void CWinSystemWayland::ApplyShellSurfaceState(IShellSurface::StateBitset state)
+{
+ m_windowDecorator->SetState(m_configuredSize, m_scale, state);
+ m_shellSurfaceState = state;
+}
+
+void CWinSystemWayland::OnConfigure(std::uint32_t serial, CSizeInt size, IShellSurface::StateBitset state)
+{
+ if (m_shellSurfaceInitializing)
+ {
+ CLog::LogF(LOGDEBUG, "Initial configure serial {}: size {}x{} state {}", serial, size.Width(),
+ size.Height(), IShellSurface::StateToString(state));
+ m_shellSurfaceState = state;
+ if (!size.IsZero())
+ {
+ UpdateSizeVariables(size, m_scale, m_shellSurfaceState, true);
+ }
+ AckConfigure(serial);
+ }
+ else
+ {
+ WinSystemWaylandProtocol::MsgConfigure msg{serial, size, state};
+ m_protocol.SendOutMessage(WinSystemWaylandProtocol::CONFIGURE, &msg, sizeof(msg));
+ }
+}
+
+void CWinSystemWayland::AckConfigure(std::uint32_t serial)
+{
+ // Send ack if we have a new serial number or this is the first time
+ // this function is called
+ if (serial != m_lastAckedSerial || !m_firstSerialAcked)
+ {
+ CLog::LogF(LOGDEBUG, "Acking serial {}", serial);
+ m_shellSurface->AckConfigure(serial);
+ m_lastAckedSerial = serial;
+ m_firstSerialAcked = true;
+ }
+}
+
+/**
+ * Recalculate sizes from given parameters, apply them and update Kodi CDisplaySettings
+ * resolution if necessary
+ *
+ * This function should be called when events internal to the windowing system
+ * such as a compositor configure lead to a size change.
+ *
+ * Call only from main thread.
+ *
+ * \param size configured size, can be zero if compositor does not have a preference
+ * \param scale new buffer scale
+ * \param sizeIncludesDecoration whether size includes the size of the window decorations if present
+ */
+void CWinSystemWayland::SetResolutionInternal(CSizeInt size, std::int32_t scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration, bool mustAck, std::uint32_t configureSerial)
+{
+ // This should never be called while a size set is pending
+ assert(!m_waitingForApply);
+
+ bool fullScreen{state.test(IShellSurface::STATE_FULLSCREEN)};
+ auto sizes = CalculateSizes(size, scale, state, sizeIncludesDecoration);
+
+ CLog::LogF(LOGDEBUG, "Set size for serial {}: {}x{} {} decoration at scale {} state {}",
+ configureSerial, size.Width(), size.Height(),
+ sizeIncludesDecoration ? "including" : "excluding", scale,
+ IShellSurface::StateToString(state));
+
+ // Get actual frame rate from monitor, take highest frame rate if multiple
+ float refreshRate{m_fRefreshRate};
+ {
+ std::unique_lock<CCriticalSection> lock(m_surfaceOutputsMutex);
+ auto maxRefreshIt = std::max_element(m_surfaceOutputs.cbegin(), m_surfaceOutputs.cend(), OutputCurrentRefreshRateComparer());
+ if (maxRefreshIt != m_surfaceOutputs.cend())
+ {
+ refreshRate = (*maxRefreshIt)->GetCurrentMode().GetRefreshInHz();
+ CLog::LogF(LOGDEBUG, "Resolved actual (maximum) refresh rate to {:.3f} Hz on output \"{}\"",
+ refreshRate, UserFriendlyOutputName(*maxRefreshIt));
+ }
+ }
+
+ m_next.mustBeAcked = mustAck;
+ m_next.configureSerial = configureSerial;
+ m_next.configuredSize = sizes.configuredSize;
+ m_next.scale = scale;
+ m_next.shellSurfaceState = state;
+
+ // Check if any parameters of the Kodi resolution configuration changed
+ if (refreshRate != m_fRefreshRate || sizes.bufferSize != m_bufferSize || m_bFullScreen != fullScreen)
+ {
+ if (!fullScreen)
+ {
+ if (m_bFullScreen)
+ {
+ XBMC_Event msg{};
+ msg.type = XBMC_MODECHANGE;
+ msg.mode.res = RES_WINDOW;
+ SetWindowResolution(sizes.bufferSize.Width(), sizes.bufferSize.Height());
+ // FIXME
+ dynamic_cast<CWinEventsWayland&>(*m_winEvents).MessagePush(&msg);
+ m_waitingForApply = true;
+ CLog::LogF(LOGDEBUG, "Queued change to windowed mode size {}x{}", sizes.bufferSize.Width(),
+ sizes.bufferSize.Height());
+ }
+ else
+ {
+ XBMC_Event msg{};
+ msg.type = XBMC_VIDEORESIZE;
+ msg.resize = {sizes.bufferSize.Width(), sizes.bufferSize.Height()};
+ // FIXME
+ dynamic_cast<CWinEventsWayland&>(*m_winEvents).MessagePush(&msg);
+ m_waitingForApply = true;
+ CLog::LogF(LOGDEBUG, "Queued change to windowed buffer size {}x{}",
+ sizes.bufferSize.Width(), sizes.bufferSize.Height());
+ }
+ }
+ else
+ {
+ // Find matching Kodi resolution member
+ RESOLUTION res{FindMatchingCustomResolution(sizes.bufferSize, refreshRate)};
+ if (res == RES_INVALID)
+ {
+ // Add new resolution if none found
+ RESOLUTION_INFO newResInfo;
+ // we just assume the compositor put us on the right output
+ UpdateDesktopResolution(newResInfo, CDisplaySettings::GetInstance().GetCurrentResolutionInfo().strOutput, sizes.bufferSize.Width(), sizes.bufferSize.Height(), refreshRate, 0);
+ CDisplaySettings::GetInstance().AddResolutionInfo(newResInfo);
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+ res = static_cast<RESOLUTION> (CDisplaySettings::GetInstance().ResolutionInfoSize() - 1);
+ }
+
+ XBMC_Event msg{};
+ msg.type = XBMC_MODECHANGE;
+ msg.mode.res = res;
+ // FIXME
+ dynamic_cast<CWinEventsWayland&>(*m_winEvents).MessagePush(&msg);
+ m_waitingForApply = true;
+ CLog::LogF(LOGDEBUG, "Queued change to resolution {} surface size {}x{} scale {} state {}",
+ res, sizes.surfaceSize.Width(), sizes.surfaceSize.Height(), scale,
+ IShellSurface::StateToString(state));
+ }
+ }
+ else
+ {
+ // Apply directly, Kodi resolution does not change
+ ApplyNextState();
+ }
+}
+
+void CWinSystemWayland::FinishModeChange(RESOLUTION res)
+{
+ const auto& resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+
+ ApplyNextState();
+
+ m_fRefreshRate = resInfo.fRefreshRate;
+ m_bFullScreen = resInfo.bFullScreen;
+ m_waitingForApply = false;
+}
+
+void CWinSystemWayland::FinishWindowResize(int, int)
+{
+ ApplyNextState();
+ m_waitingForApply = false;
+}
+
+void CWinSystemWayland::ApplyNextState()
+{
+ CLog::LogF(LOGDEBUG, "Applying next state: serial {} configured size {}x{} scale {} state {}",
+ m_next.configureSerial, m_next.configuredSize.Width(), m_next.configuredSize.Height(),
+ m_next.scale, IShellSurface::StateToString(m_next.shellSurfaceState));
+
+ ApplyShellSurfaceState(m_next.shellSurfaceState);
+ auto updateResult = UpdateSizeVariables(m_next.configuredSize, m_next.scale, m_next.shellSurfaceState, true);
+ ApplySizeUpdate(updateResult);
+
+ if (m_next.mustBeAcked)
+ {
+ AckConfigure(m_next.configureSerial);
+ }
+}
+
+CWinSystemWayland::Sizes CWinSystemWayland::CalculateSizes(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration)
+{
+ Sizes result;
+
+ // Clamp to a sensible range
+ constexpr int MIN_WIDTH{300};
+ constexpr int MIN_HEIGHT{200};
+ if (size.Width() < MIN_WIDTH)
+ {
+ CLog::LogF(LOGWARNING, "Width {} is very small, clamping to {}", size.Width(), MIN_WIDTH);
+ size.SetWidth(MIN_WIDTH);
+ }
+ if (size.Height() < MIN_HEIGHT)
+ {
+ CLog::LogF(LOGWARNING, "Height {} is very small, clamping to {}", size.Height(), MIN_HEIGHT);
+ size.SetHeight(MIN_HEIGHT);
+ }
+
+ // Depending on whether the size has decorations included (i.e. comes from the
+ // compositor or from Kodi), we need to calculate differently
+ if (sizeIncludesDecoration)
+ {
+ result.configuredSize = size;
+ result.surfaceSize = m_windowDecorator->CalculateMainSurfaceSize(size, state);
+ }
+ else
+ {
+ result.surfaceSize = size;
+ result.configuredSize = m_windowDecorator->CalculateFullSurfaceSize(size, state);
+ }
+
+ result.bufferSize = result.surfaceSize * scale;
+
+ return result;
+}
+
+
+/**
+ * Calculate internal resolution from surface size and set variables
+ *
+ * \param next surface size
+ * \param scale new buffer scale
+ * \param state window state to determine whether decorations are enabled at all
+ * \param sizeIncludesDecoration if true, given size includes potential window decorations
+ * \return whether main buffer (not surface) size changed
+ */
+CWinSystemWayland::SizeUpdateInformation CWinSystemWayland::UpdateSizeVariables(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration)
+{
+ CLog::LogF(LOGDEBUG, "Set size {}x{} scale {} {} decorations with state {}", size.Width(),
+ size.Height(), scale, sizeIncludesDecoration ? "including" : "excluding",
+ IShellSurface::StateToString(state));
+
+ auto oldSurfaceSize = m_surfaceSize;
+ auto oldBufferSize = m_bufferSize;
+ auto oldConfiguredSize = m_configuredSize;
+ auto oldBufferScale = m_scale;
+
+ m_scale = scale;
+ auto sizes = CalculateSizes(size, scale, state, sizeIncludesDecoration);
+ m_surfaceSize = sizes.surfaceSize;
+ m_bufferSize = sizes.bufferSize;
+ m_configuredSize = sizes.configuredSize;
+
+ SizeUpdateInformation changes{m_surfaceSize != oldSurfaceSize, m_bufferSize != oldBufferSize, m_configuredSize != oldConfiguredSize, m_scale != oldBufferScale};
+
+ if (changes.surfaceSizeChanged)
+ {
+ CLog::LogF(LOGINFO, "Surface size changed: {}x{} -> {}x{}", oldSurfaceSize.Width(),
+ oldSurfaceSize.Height(), m_surfaceSize.Width(), m_surfaceSize.Height());
+ }
+ if (changes.bufferSizeChanged)
+ {
+ CLog::LogF(LOGINFO, "Buffer size changed: {}x{} -> {}x{}", oldBufferSize.Width(),
+ oldBufferSize.Height(), m_bufferSize.Width(), m_bufferSize.Height());
+ }
+ if (changes.configuredSizeChanged)
+ {
+ CLog::LogF(LOGINFO, "Configured size changed: {}x{} -> {}x{}", oldConfiguredSize.Width(),
+ oldConfiguredSize.Height(), m_configuredSize.Width(), m_configuredSize.Height());
+ }
+ if (changes.bufferScaleChanged)
+ {
+ CLog::LogF(LOGINFO, "Buffer scale changed: {} -> {}", oldBufferScale, m_scale);
+ }
+
+ return changes;
+}
+
+std::string CWinSystemWayland::UserFriendlyOutputName(std::shared_ptr<COutput> const& output)
+{
+ std::vector<std::string> parts;
+ if (!output->GetMake().empty())
+ {
+ parts.emplace_back(output->GetMake());
+ }
+ if (!output->GetModel().empty())
+ {
+ parts.emplace_back(output->GetModel());
+ }
+ if (parts.empty())
+ {
+ // Fallback to "unknown" if no name received from compositor
+ parts.emplace_back(g_localizeStrings.Get(13205));
+ }
+
+ // Add position
+ auto pos = output->GetPosition();
+ if (pos.x != 0 || pos.y != 0)
+ {
+ parts.emplace_back(StringUtils::Format("@{}x{}", pos.x, pos.y));
+ }
+
+ return StringUtils::Join(parts, " ");
+}
+
+bool CWinSystemWayland::Minimize()
+{
+ m_shellSurface->SetMinimized();
+ return true;
+}
+
+bool CWinSystemWayland::HasCursor()
+{
+ std::unique_lock<CCriticalSection> lock(m_seatsMutex);
+ return std::any_of(m_seats.cbegin(), m_seats.cend(),
+ [](decltype(m_seats)::value_type const& entry)
+ {
+ return entry.second.HasPointerCapability();
+ });
+}
+
+void CWinSystemWayland::ShowOSMouse(bool show)
+{
+ m_osCursorVisible = show;
+}
+
+void CWinSystemWayland::LoadDefaultCursor()
+{
+ if (!m_cursorSurface)
+ {
+ // Load default cursor theme and default cursor
+ // Size of 24px is what most themes seem to have
+ m_cursorTheme = wayland::cursor_theme_t("", 24, m_shm);
+ wayland::cursor_t cursor;
+ try
+ {
+ cursor = CCursorUtil::LoadFromTheme(m_cursorTheme, "default");
+ }
+ catch (std::exception const& e)
+ {
+ CLog::Log(LOGWARNING, "Could not load default cursor from theme, continuing without OS cursor");
+ }
+ // Just use the first image, do not handle animation
+ m_cursorImage = cursor.image(0);
+ m_cursorBuffer = m_cursorImage.get_buffer();
+ m_cursorSurface = m_compositor.create_surface();
+ }
+ // Attach buffer to a surface - it seems that the compositor may change
+ // the cursor surface when the pointer leaves our surface, so we reattach the
+ // buffer each time
+ m_cursorSurface.attach(m_cursorBuffer, 0, 0);
+ m_cursorSurface.damage(0, 0, m_cursorImage.width(), m_cursorImage.height());
+ m_cursorSurface.commit();
+}
+
+void CWinSystemWayland::Register(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_dispResourcesMutex);
+ m_dispResources.emplace(resource);
+}
+
+void CWinSystemWayland::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_dispResourcesMutex);
+ m_dispResources.erase(resource);
+}
+
+void CWinSystemWayland::OnSeatAdded(std::uint32_t name, wayland::proxy_t&& proxy)
+{
+ std::unique_lock<CCriticalSection> lock(m_seatsMutex);
+
+ wayland::seat_t seat(proxy);
+ auto newSeatEmplace = m_seats.emplace(std::piecewise_construct,
+ std::forward_as_tuple(name),
+ std::forward_as_tuple(name, seat, *m_connection));
+
+ auto& seatInst = newSeatEmplace.first->second;
+ m_seatInputProcessing->AddSeat(&seatInst);
+ m_windowDecorator->AddSeat(&seatInst);
+}
+
+void CWinSystemWayland::OnSeatRemoved(std::uint32_t name)
+{
+ std::unique_lock<CCriticalSection> lock(m_seatsMutex);
+
+ auto seatI = m_seats.find(name);
+ if (seatI != m_seats.end())
+ {
+ m_seatInputProcessing->RemoveSeat(&seatI->second);
+ m_windowDecorator->RemoveSeat(&seatI->second);
+ m_seats.erase(name);
+ }
+}
+
+void CWinSystemWayland::OnOutputAdded(std::uint32_t name, wayland::proxy_t&& proxy)
+{
+ wayland::output_t output(proxy);
+ // This is not accessed from multiple threads
+ m_outputsInPreparation.emplace(name, std::make_shared<COutput>(name, output, std::bind(&CWinSystemWayland::OnOutputDone, this, name)));
+}
+
+void CWinSystemWayland::OnOutputDone(std::uint32_t name)
+{
+ auto it = m_outputsInPreparation.find(name);
+ if (it != m_outputsInPreparation.end())
+ {
+ // This output was added for the first time - done is also sent when
+ // output parameters change later
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ // Move from m_outputsInPreparation to m_outputs
+ m_outputs.emplace(std::move(*it));
+ m_outputsInPreparation.erase(it);
+ }
+
+ m_protocol.SendOutMessage(WinSystemWaylandProtocol::OUTPUT_HOTPLUG);
+ }
+
+ UpdateBufferScale();
+}
+
+void CWinSystemWayland::OnOutputRemoved(std::uint32_t name)
+{
+ m_outputsInPreparation.erase(name);
+
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ if (m_outputs.erase(name) != 0)
+ {
+ // Theoretically, the compositor should automatically put us on another
+ // (visible and connected) output if the output we were on is lost,
+ // so there is nothing in particular to do here
+ }
+}
+
+void CWinSystemWayland::SendFocusChange(bool focus)
+{
+ g_application.m_AppFocused = focus;
+ std::unique_lock<CCriticalSection> lock(m_dispResourcesMutex);
+ for (auto dispResource : m_dispResources)
+ {
+ dispResource->OnAppFocusChange(focus);
+ }
+}
+
+void CWinSystemWayland::OnEnter(InputType type)
+{
+ // Couple to keyboard focus
+ if (type == InputType::KEYBOARD)
+ {
+ SendFocusChange(true);
+ }
+ if (type == InputType::POINTER)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(true);
+ }
+}
+
+void CWinSystemWayland::OnLeave(InputType type)
+{
+ // Couple to keyboard focus
+ if (type == InputType::KEYBOARD)
+ {
+ SendFocusChange(false);
+ }
+ if (type == InputType::POINTER)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ }
+}
+
+void CWinSystemWayland::OnEvent(InputType type, XBMC_Event& event)
+{
+ // FIXME
+ dynamic_cast<CWinEventsWayland&>(*m_winEvents).MessagePush(&event);
+}
+
+void CWinSystemWayland::OnSetCursor(std::uint32_t seatGlobalName, std::uint32_t serial)
+{
+ auto seatI = m_seats.find(seatGlobalName);
+ if (seatI == m_seats.end())
+ {
+ return;
+ }
+
+ if (m_osCursorVisible)
+ {
+ LoadDefaultCursor();
+ if (m_cursorSurface) // Cursor loading could have failed
+ {
+ seatI->second.SetCursor(serial, m_cursorSurface, m_cursorImage.hotspot_x(), m_cursorImage.hotspot_y());
+ }
+ }
+ else
+ {
+ seatI->second.SetCursor(serial, wayland::surface_t{}, 0, 0);
+ }
+}
+
+void CWinSystemWayland::UpdateBufferScale()
+{
+ // Adjust our surface size to the output with the biggest scale in order
+ // to get the best quality
+ auto const maxBufferScaleIt = std::max_element(m_surfaceOutputs.cbegin(), m_surfaceOutputs.cend(), OutputScaleComparer());
+ if (maxBufferScaleIt != m_surfaceOutputs.cend())
+ {
+ WinSystemWaylandProtocol::MsgBufferScale msg{(*maxBufferScaleIt)->GetScale()};
+ m_protocol.SendOutMessage(WinSystemWaylandProtocol::BUFFER_SCALE, &msg, sizeof(msg));
+ }
+}
+
+void CWinSystemWayland::ApplyBufferScale()
+{
+ CLog::LogF(LOGINFO, "Setting Wayland buffer scale to {}", m_scale);
+ m_surface.set_buffer_scale(m_scale);
+ m_windowDecorator->SetState(m_configuredSize, m_scale, m_shellSurfaceState);
+ m_seatInputProcessing->SetCoordinateScale(m_scale);
+}
+
+void CWinSystemWayland::UpdateTouchDpi()
+{
+ // If we have multiple outputs with wildly different DPI, this is really just
+ // guesswork to get a halfway reasonable value. min/max would probably also be OK.
+ float dpiSum = std::accumulate(m_surfaceOutputs.cbegin(), m_surfaceOutputs.cend(), 0.0f,
+ [](float acc, std::shared_ptr<COutput> const& output)
+ {
+ return acc + output->GetCurrentDpi();
+ });
+ float dpi = dpiSum / m_surfaceOutputs.size();
+ CLog::LogF(LOGDEBUG, "Computed average dpi of {:.3f} for touch handler", dpi);
+ CGenericTouchInputHandler::GetInstance().SetScreenDPI(dpi);
+}
+
+CWinSystemWayland::SurfaceSubmission::SurfaceSubmission(timespec const& submissionTime, wayland::presentation_feedback_t const& feedback)
+: submissionTime{submissionTime}, feedback{feedback}
+{
+}
+
+timespec CWinSystemWayland::GetPresentationClockTime()
+{
+ timespec time;
+ if (clock_gettime(m_presentationClock, &time) != 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "Error getting time from Wayland presentation clock with clock_gettime");
+ }
+ return time;
+}
+
+void CWinSystemWayland::PrepareFramePresentation()
+{
+ // Continuously measure display latency (i.e. time between when the frame was rendered
+ // and when it becomes visible to the user) to correct AV sync
+ if (m_presentation)
+ {
+ auto tStart = GetPresentationClockTime();
+ // wp_presentation_feedback creation is coupled to the surface's commit().
+ // eglSwapBuffers() (which will be called after this) will call commit().
+ // This creates a new Wayland protocol object in the main thread, but this
+ // will not result in a race since the corresponding events are never sent
+ // before commit() on the surface, which only occurs afterwards.
+ auto feedback = m_presentation.feedback(m_surface);
+ // Save feedback objects in list so they don't get destroyed upon exit of this function
+ // Hand iterator to lambdas so they do not hold a (then circular) reference
+ // to the actual object
+ decltype(m_surfaceSubmissions)::iterator iter;
+ {
+ std::unique_lock<CCriticalSection> lock(m_surfaceSubmissionsMutex);
+ iter = m_surfaceSubmissions.emplace(m_surfaceSubmissions.end(), tStart, feedback);
+ }
+
+ feedback.on_sync_output() = [this](const wayland::output_t& wloutput) {
+ m_syncOutputID = wloutput.get_id();
+ auto output = FindOutputByWaylandOutput(wloutput);
+ if (output)
+ {
+ m_syncOutputRefreshRate = output->GetCurrentMode().GetRefreshInHz();
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Could not find Wayland output that is supposedly the sync output");
+ }
+ };
+ feedback.on_presented() = [this, iter](std::uint32_t tvSecHi, std::uint32_t tvSecLo,
+ std::uint32_t tvNsec, std::uint32_t refresh,
+ std::uint32_t seqHi, std::uint32_t seqLo,
+ const wayland::presentation_feedback_kind& flags) {
+ timespec tv = { .tv_sec = static_cast<std::time_t> ((static_cast<std::uint64_t>(tvSecHi) << 32) + tvSecLo), .tv_nsec = static_cast<long>(tvNsec) };
+ std::int64_t latency{KODI::LINUX::TimespecDifference(iter->submissionTime, tv)};
+ std::uint64_t msc{(static_cast<std::uint64_t>(seqHi) << 32) + seqLo};
+ m_presentationFeedbackHandlers.Invoke(tv, refresh, m_syncOutputID, m_syncOutputRefreshRate, msc);
+
+ iter->latency = latency / 1e9f; // nanoseconds to seconds
+ float adjust{};
+ {
+ std::unique_lock<CCriticalSection> lock(m_surfaceSubmissionsMutex);
+ if (m_surfaceSubmissions.size() > LATENCY_MOVING_AVERAGE_SIZE)
+ {
+ adjust = - m_surfaceSubmissions.front().latency / LATENCY_MOVING_AVERAGE_SIZE;
+ m_surfaceSubmissions.pop_front();
+ }
+ }
+ m_latencyMovingAverage = m_latencyMovingAverage + iter->latency / LATENCY_MOVING_AVERAGE_SIZE + adjust;
+
+ CLog::Log(LOGDEBUG, LOGAVTIMING, "Presentation feedback: {} ns -> moving average {:f} s",
+ latency, static_cast<double>(m_latencyMovingAverage));
+ };
+ feedback.on_discarded() = [this,iter]()
+ {
+ CLog::Log(LOGDEBUG, "Presentation: Frame was discarded by compositor");
+ std::unique_lock<CCriticalSection> lock(m_surfaceSubmissionsMutex);
+ m_surfaceSubmissions.erase(iter);
+ };
+ }
+
+ // Now wait for the frame callback that tells us that it is a good time to start drawing
+ //
+ // To sum up, we:
+ // 1. wait until a frame() drawing hint from the compositor arrives,
+ // 2. request a new frame() hint for the next presentation
+ // 2. then commit the backbuffer to the surface and immediately
+ // return, i.e. drawing can start again
+ // This means that rendering is optimized for maximum time available for
+ // our repaint and reliable timing rather than latency. With weston, latency
+ // will usually be on the order of two frames plus a few milliseconds.
+ // The frame timings become irregular though when nothing is rendered because
+ // kodi then sleeps for a fixed time without swapping buffers. This makes us
+ // immediately attach the next buffer because the frame callback has already arrived when
+ // this function is called and step 1. above is skipped. As we render with full
+ // FPS during video playback anyway and the timing is otherwise not relevant,
+ // this should not be a problem.
+ if (m_frameCallback)
+ {
+ // If the window is e.g. minimized, chances are that we will *never* get frame
+ // callbacks from the compositor for optimization reasons.
+ // Still, the app should remain functional, which means that we can't
+ // just block forever here - if the render thread is blocked, Kodi will not
+ // function normally. It would also be impossible to close the application
+ // while it is minimized (since the wait needs to be interrupted for that).
+ // -> Use Wait with timeout here so we can maintain a reasonable frame rate
+ // even when the window is not visible and we do not get any frame callbacks.
+ if (m_frameCallbackEvent.Wait(50ms))
+ {
+ // Only reset frame callback object a callback was received so a
+ // new one is not requested continuously
+ m_frameCallback = {};
+ m_frameCallbackEvent.Reset();
+ }
+ }
+
+ if (!m_frameCallback)
+ {
+ // Get frame callback event for checking in the next call to this function
+ m_frameCallback = m_surface.frame();
+ m_frameCallback.on_done() = [this](std::uint32_t)
+ {
+ m_frameCallbackEvent.Set();
+ };
+ }
+}
+
+void CWinSystemWayland::FinishFramePresentation()
+{
+ ProcessMessages();
+
+ m_frameStartTime = std::chrono::steady_clock::now();
+}
+
+float CWinSystemWayland::GetFrameLatencyAdjustment()
+{
+ const auto now = std::chrono::steady_clock::now();
+ const std::chrono::duration<float, std::milli> duration = now - m_frameStartTime;
+ return duration.count();
+}
+
+float CWinSystemWayland::GetDisplayLatency()
+{
+ if (m_presentation)
+ {
+ return m_latencyMovingAverage * 1000.0f;
+ }
+ else
+ {
+ return CWinSystemBase::GetDisplayLatency();
+ }
+}
+
+float CWinSystemWayland::GetSyncOutputRefreshRate()
+{
+ return m_syncOutputRefreshRate;
+}
+
+KODI::CSignalRegistration CWinSystemWayland::RegisterOnPresentationFeedback(
+ const PresentationFeedbackHandler& handler)
+{
+ return m_presentationFeedbackHandlers.Register(handler);
+}
+
+std::unique_ptr<CVideoSync> CWinSystemWayland::GetVideoSync(void* clock)
+{
+ if (m_surface && m_presentation)
+ {
+ CLog::LogF(LOGINFO, "Using presentation protocol for video sync");
+ return std::unique_ptr<CVideoSync>(new CVideoSyncWpPresentation(clock, *this));
+ }
+ else
+ {
+ CLog::LogF(LOGINFO, "No supported method for video sync found");
+ return nullptr;
+ }
+}
+
+std::unique_ptr<IOSScreenSaver> CWinSystemWayland::GetOSScreenSaverImpl()
+{
+ if (m_surface)
+ {
+ std::unique_ptr<IOSScreenSaver> ptr;
+ ptr.reset(COSScreenSaverIdleInhibitUnstableV1::TryCreate(*m_connection, m_surface));
+ if (ptr)
+ {
+ CLog::LogF(LOGINFO, "Using idle-inhibit-unstable-v1 protocol for screen saver inhibition");
+ return ptr;
+ }
+ }
+
+#if defined(HAS_DBUS)
+ if (KODI::WINDOWING::LINUX::COSScreenSaverFreedesktop::IsAvailable())
+ {
+ CLog::LogF(LOGINFO, "Using freedesktop.org DBus interface for screen saver inhibition");
+ return std::unique_ptr<IOSScreenSaver>(new KODI::WINDOWING::LINUX::COSScreenSaverFreedesktop);
+ }
+#endif
+
+ CLog::LogF(LOGINFO, "No supported method for screen saver inhibition found");
+ return std::unique_ptr<IOSScreenSaver>(new CDummyOSScreenSaver);
+}
+
+std::string CWinSystemWayland::GetClipboardText()
+{
+ std::unique_lock<CCriticalSection> lock(m_seatsMutex);
+ // Get text of first seat with non-empty selection
+ // Actually, the value of the seat that received the Ctrl+V keypress should be used,
+ // but this would need a workaround or proper multi-seat support in Kodi - it's
+ // probably just not that relevant in practice
+ for (auto const& seat : m_seats)
+ {
+ auto text = seat.second.GetSelectionText();
+ if (text != "")
+ {
+ return text;
+ }
+ }
+ return "";
+}
+
+void CWinSystemWayland::OnWindowMove(const wayland::seat_t& seat, std::uint32_t serial)
+{
+ m_shellSurface->StartMove(seat, serial);
+}
+
+void CWinSystemWayland::OnWindowResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge)
+{
+ m_shellSurface->StartResize(seat, serial, edge);
+}
+
+void CWinSystemWayland::OnWindowShowContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position)
+{
+ m_shellSurface->ShowShellContextMenu(seat, serial, position);
+}
+
+void CWinSystemWayland::OnWindowClose()
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+}
+
+void CWinSystemWayland::OnWindowMinimize()
+{
+ m_shellSurface->SetMinimized();
+}
+
+void CWinSystemWayland::OnWindowMaximize()
+{
+ if (m_shellSurfaceState.test(IShellSurface::STATE_MAXIMIZED))
+ {
+ m_shellSurface->UnsetMaximized();
+ }
+ else
+ {
+ m_shellSurface->SetMaximized();
+ }
+}
+
+void CWinSystemWayland::OnClose()
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+}
+
+bool CWinSystemWayland::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
diff --git a/xbmc/windowing/wayland/WinSystemWayland.h b/xbmc/windowing/wayland/WinSystemWayland.h
new file mode 100644
index 0000000..514b8d6
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWayland.h
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Connection.h"
+#include "Output.h"
+#include "Seat.h"
+#include "SeatInputProcessing.h"
+#include "ShellSurface.h"
+#include "Signals.h"
+#include "WindowDecorationHandler.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "utils/ActorProtocol.h"
+#include "windowing/WinSystem.h"
+
+#include <atomic>
+#include <chrono>
+#include <ctime>
+#include <list>
+#include <map>
+#include <set>
+#include <time.h>
+
+#include <wayland-client.hpp>
+#include <wayland-cursor.hpp>
+#include <wayland-extra-protocols.hpp>
+
+class IDispResource;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CRegistry;
+class CWindowDecorator;
+
+class CWinSystemWayland : public CWinSystemBase, IInputHandler, IWindowDecorationHandler, IShellSurfaceHandler
+{
+public:
+ CWinSystemWayland();
+ ~CWinSystemWayland() noexcept override;
+
+ const std::string GetName() override { return "wayland"; }
+
+ bool InitWindowSystem() override;
+ bool DestroyWindowSystem() override;
+
+ bool CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res) override;
+
+ bool DestroyWindow() override;
+
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ void FinishModeChange(RESOLUTION res) override;
+ void FinishWindowResize(int newWidth, int newHeight) override;
+
+ bool UseLimitedColor() override;
+
+ void UpdateResolutions() override;
+
+ bool CanDoWindowed() override;
+ bool Minimize() override;
+
+ bool HasCursor() override;
+ void ShowOSMouse(bool show) override;
+
+ std::string GetClipboardText() override;
+
+ float GetSyncOutputRefreshRate();
+ float GetDisplayLatency() override;
+ float GetFrameLatencyAdjustment() override;
+ std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override;
+
+ void Register(IDispResource* resource) override;
+ void Unregister(IDispResource* resource) override;
+
+ using PresentationFeedbackHandler = std::function<void(timespec /* tv */, std::uint32_t /* refresh */, std::uint32_t /* sync output id */, float /* sync output fps */, std::uint64_t /* msc */)>;
+ CSignalRegistration RegisterOnPresentationFeedback(const PresentationFeedbackHandler& handler);
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+ // winevents override
+ bool MessagePump() override;
+
+protected:
+ std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override;
+ CSizeInt GetBufferSize() const
+ {
+ return m_bufferSize;
+ }
+ std::unique_ptr<CConnection> const& GetConnection()
+ {
+ return m_connection;
+ }
+ wayland::surface_t GetMainSurface()
+ {
+ return m_surface;
+ }
+
+ void PrepareFramePresentation();
+ void FinishFramePresentation();
+ virtual void SetContextSize(CSizeInt size) = 0;
+
+private:
+ // IInputHandler
+ void OnEnter(InputType type) override;
+ void OnLeave(InputType type) override;
+ void OnEvent(InputType type, XBMC_Event& event) override;
+ void OnSetCursor(std::uint32_t seatGlobalName, std::uint32_t serial) override;
+
+ // IWindowDecorationHandler
+ void OnWindowMove(const wayland::seat_t& seat, std::uint32_t serial) override;
+ void OnWindowResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge) override;
+ void OnWindowShowContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position) override;
+ void OnWindowClose() override;
+ void OnWindowMaximize() override;
+ void OnWindowMinimize() override;
+
+ // IShellSurfaceHandler
+ void OnConfigure(std::uint32_t serial, CSizeInt size, IShellSurface::StateBitset state) override;
+ void OnClose() override;
+
+ // Registry handlers
+ void OnSeatAdded(std::uint32_t name, wayland::proxy_t&& seat);
+ void OnSeatRemoved(std::uint32_t name);
+ void OnOutputAdded(std::uint32_t name, wayland::proxy_t&& output);
+ void OnOutputRemoved(std::uint32_t name);
+
+ void LoadDefaultCursor();
+ void SendFocusChange(bool focus);
+ bool SetResolutionExternal(bool fullScreen, RESOLUTION_INFO const& res);
+ void SetResolutionInternal(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration, bool mustAck = false, std::uint32_t configureSerial = 0u);
+ struct Sizes
+ {
+ CSizeInt surfaceSize;
+ CSizeInt bufferSize;
+ CSizeInt configuredSize;
+ };
+ Sizes CalculateSizes(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration);
+ struct SizeUpdateInformation
+ {
+ bool surfaceSizeChanged : 1;
+ bool bufferSizeChanged : 1;
+ bool configuredSizeChanged : 1;
+ bool bufferScaleChanged : 1;
+ };
+ SizeUpdateInformation UpdateSizeVariables(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration);
+ void ApplySizeUpdate(SizeUpdateInformation update);
+ void ApplyNextState();
+
+ std::string UserFriendlyOutputName(std::shared_ptr<COutput> const& output);
+ std::shared_ptr<COutput> FindOutputByUserFriendlyName(std::string const& name);
+ std::shared_ptr<COutput> FindOutputByWaylandOutput(wayland::output_t const& output);
+
+ // Called when wl_output::done is received for an output, i.e. associated
+ // information like modes is available
+ void OnOutputDone(std::uint32_t name);
+ void UpdateBufferScale();
+ void ApplyBufferScale();
+ void ApplyOpaqueRegion();
+ void ApplyWindowGeometry();
+ void UpdateTouchDpi();
+ void ApplyShellSurfaceState(IShellSurface::StateBitset state);
+
+ void ProcessMessages();
+ void AckConfigure(std::uint32_t serial);
+
+ timespec GetPresentationClockTime();
+
+ // Globals
+ // -------
+ std::unique_ptr<CConnection> m_connection;
+ std::unique_ptr<CRegistry> m_registry;
+ /**
+ * Registry used exclusively for wayland::seat_t
+ *
+ * Use extra registry because seats can only be registered after the surface
+ * has been created
+ */
+ std::unique_ptr<CRegistry> m_seatRegistry;
+ wayland::compositor_t m_compositor;
+ wayland::shm_t m_shm;
+ wayland::presentation_t m_presentation;
+
+ std::unique_ptr<IShellSurface> m_shellSurface;
+
+ // Frame callback handling
+ // -----------------------
+ wayland::callback_t m_frameCallback;
+ CEvent m_frameCallbackEvent;
+
+ // Seat handling
+ // -------------
+ std::map<std::uint32_t, CSeat> m_seats;
+ CCriticalSection m_seatsMutex;
+ std::unique_ptr<CSeatInputProcessing> m_seatInputProcessing;
+ std::map<std::uint32_t, std::shared_ptr<COutput>> m_outputs;
+ /// outputs that did not receive their done event yet
+ std::map<std::uint32_t, std::shared_ptr<COutput>> m_outputsInPreparation;
+ CCriticalSection m_outputsMutex;
+
+ // Windowed mode
+ // -------------
+ std::unique_ptr<CWindowDecorator> m_windowDecorator;
+
+ // Cursor
+ // ------
+ bool m_osCursorVisible{true};
+ wayland::cursor_theme_t m_cursorTheme;
+ wayland::buffer_t m_cursorBuffer;
+ wayland::cursor_image_t m_cursorImage;
+ wayland::surface_t m_cursorSurface;
+
+ // Presentation feedback
+ // ---------------------
+ clockid_t m_presentationClock;
+ struct SurfaceSubmission
+ {
+ timespec submissionTime;
+ float latency;
+ wayland::presentation_feedback_t feedback;
+ SurfaceSubmission(timespec const& submissionTime, wayland::presentation_feedback_t const& feedback);
+ };
+ std::list<SurfaceSubmission> m_surfaceSubmissions;
+ CCriticalSection m_surfaceSubmissionsMutex;
+ /// Protocol object ID of the sync output returned by wp_presentation
+ std::uint32_t m_syncOutputID;
+ /// Refresh rate of sync output returned by wp_presentation
+ std::atomic<float> m_syncOutputRefreshRate{0.0f};
+ static constexpr int LATENCY_MOVING_AVERAGE_SIZE{30};
+ std::atomic<float> m_latencyMovingAverage;
+ CSignalHandlerList<PresentationFeedbackHandler> m_presentationFeedbackHandlers;
+
+ std::chrono::steady_clock::time_point m_frameStartTime{};
+
+ // IDispResource
+ // -------------
+ std::set<IDispResource*> m_dispResources;
+ CCriticalSection m_dispResourcesMutex;
+
+ // Surface state
+ // -------------
+ wayland::surface_t m_surface;
+ wayland::output_t m_lastSetOutput;
+ /// Set of outputs that show some part of our main surface as indicated by
+ /// compositor
+ std::set<std::shared_ptr<COutput>> m_surfaceOutputs;
+ CCriticalSection m_surfaceOutputsMutex;
+ /// Size of our surface in "surface coordinates" (i.e. without scaling applied)
+ /// and without window decorations
+ CSizeInt m_surfaceSize;
+ /// Size of the buffer that should back the surface (i.e. with scaling applied)
+ CSizeInt m_bufferSize;
+ /// Size of the whole window including window decorations as given by configure
+ CSizeInt m_configuredSize;
+ /// Scale in use for main surface buffer
+ int m_scale{1};
+ /// Shell surface state last acked
+ IShellSurface::StateBitset m_shellSurfaceState;
+ /// Whether the shell surface is waiting for initial configure
+ bool m_shellSurfaceInitializing{false};
+ struct
+ {
+ bool mustBeAcked{false};
+ std::uint32_t configureSerial{};
+ CSizeInt configuredSize;
+ int scale{1};
+ IShellSurface::StateBitset shellSurfaceState;
+ } m_next;
+ bool m_waitingForApply{false};
+
+ // Internal communication
+ // ----------------------
+ /// Protocol for communicating events to the main thread
+ Actor::Protocol m_protocol;
+
+ // Configure state
+ // ---------------
+ bool m_firstSerialAcked{false};
+ std::uint32_t m_lastAckedSerial{0u};
+ /// Whether this is the first call to SetFullScreen
+ bool m_isInitialSetFullScreen{true};
+};
+
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContext.cpp b/xbmc/windowing/wayland/WinSystemWaylandEGLContext.cpp
new file mode 100644
index 0000000..a32cd14
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContext.cpp
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WinSystemWaylandEGLContext.h"
+
+#include "Connection.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <EGL/eglext.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace KODI::WINDOWING::LINUX;
+
+CWinSystemWaylandEGLContext::CWinSystemWaylandEGLContext()
+ : CWinSystemEGL{EGL_PLATFORM_WAYLAND_EXT, "EGL_EXT_platform_wayland"}
+{}
+
+bool CWinSystemWaylandEGLContext::InitWindowSystemEGL(EGLint renderableType, EGLint apiType)
+{
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CDVDFactoryCodec::ClearHWAccels();
+
+ if (!CWinSystemWayland::InitWindowSystem())
+ {
+ return false;
+ }
+
+ if (!m_eglContext.CreatePlatformDisplay(GetConnection()->GetDisplay(), GetConnection()->GetDisplay()))
+ {
+ return false;
+ }
+
+ if (!m_eglContext.InitializeDisplay(apiType))
+ {
+ return false;
+ }
+
+ if (!m_eglContext.ChooseConfig(renderableType))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinSystemWaylandEGLContext::CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res)
+{
+ if (!CWinSystemWayland::CreateNewWindow(name, fullScreen, res))
+ {
+ return false;
+ }
+
+ if (!CreateContext())
+ {
+ return false;
+ }
+
+ m_nativeWindow = wayland::egl_window_t{GetMainSurface(), GetBufferSize().Width(), GetBufferSize().Height()};
+
+ // CWinSystemWayland::CreateNewWindow sets internal m_bufferSize
+ // to the resolution that should be used for the initial surface size
+ // - the compositor might want something other than the resolution given
+ if (!m_eglContext.CreatePlatformSurface(
+ m_nativeWindow.c_ptr(), reinterpret_cast<khronos_uintptr_t>(m_nativeWindow.c_ptr())))
+ {
+ return false;
+ }
+
+ if (!m_eglContext.BindContext())
+ {
+ return false;
+ }
+
+ // Never enable the vsync of the EGL implementation, we handle that ourselves
+ // in WinSystemWayland
+ m_eglContext.SetVSync(false);
+
+ return true;
+}
+
+bool CWinSystemWaylandEGLContext::DestroyWindow()
+{
+ m_eglContext.DestroySurface();
+ m_nativeWindow = {};
+
+ return CWinSystemWayland::DestroyWindow();
+}
+
+bool CWinSystemWaylandEGLContext::DestroyWindowSystem()
+{
+ m_eglContext.Destroy();
+
+ return CWinSystemWayland::DestroyWindowSystem();
+}
+
+CSizeInt CWinSystemWaylandEGLContext::GetNativeWindowAttachedSize()
+{
+ int width, height;
+ m_nativeWindow.get_attached_size(width, height);
+ return {width, height};
+}
+
+void CWinSystemWaylandEGLContext::SetContextSize(CSizeInt size)
+{
+ // Change EGL surface size if necessary
+ if (GetNativeWindowAttachedSize() != size)
+ {
+ CLog::LogF(LOGDEBUG, "Updating egl_window size to {}x{}", size.Width(), size.Height());
+ m_nativeWindow.resize(size.Width(), size.Height(), 0, 0);
+ }
+}
+
+void CWinSystemWaylandEGLContext::PresentFrame(bool rendered)
+{
+ PrepareFramePresentation();
+
+ if (rendered)
+ {
+ if (!m_eglContext.TrySwapBuffers())
+ {
+ // For now we just hard fail if this fails
+ // Theoretically, EGL_CONTEXT_LOST could be handled, but it needs to be checked
+ // whether egl implementations actually use it (mesa does not)
+ CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed");
+ throw std::runtime_error("eglSwapBuffers failed");
+ }
+ // eglSwapBuffers() (hopefully) calls commit on the surface and flushes
+ // ... well mesa does anyway
+ }
+ else
+ {
+ // For presentation feedback: Get notification of the next vblank even
+ // when contents did not change
+ GetMainSurface().commit();
+ // Make sure it reaches the compositor
+ GetConnection()->GetDisplay().flush();
+ }
+
+ FinishFramePresentation();
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContext.h b/xbmc/windowing/wayland/WinSystemWaylandEGLContext.h
new file mode 100644
index 0000000..097b3e7
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContext.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "WinSystemWayland.h"
+#include "utils/EGLUtils.h"
+#include "windowing/linux/WinSystemEGL.h"
+
+#include <wayland-egl.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CWinSystemWaylandEGLContext : public KODI::WINDOWING::LINUX::CWinSystemEGL,
+ public CWinSystemWayland
+{
+public:
+ CWinSystemWaylandEGLContext();
+ ~CWinSystemWaylandEGLContext() override = default;
+
+ bool CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res) override;
+ bool DestroyWindow() override;
+ bool DestroyWindowSystem() override;
+
+protected:
+ /**
+ * Inheriting classes should override InitWindowSystem() without parameters
+ * and call this function there with appropriate parameters
+ */
+ bool InitWindowSystemEGL(EGLint renderableType, EGLint apiType);
+
+ CSizeInt GetNativeWindowAttachedSize();
+ void PresentFrame(bool rendered);
+ void SetContextSize(CSizeInt size) override;
+
+ virtual bool CreateContext() = 0;
+
+ wayland::egl_window_t m_nativeWindow;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.cpp b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.cpp
new file mode 100644
index 0000000..6e5a3b8
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.cpp
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WinSystemWaylandEGLContextGL.h"
+
+#include "OptionalsReg.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+#include "rendering/gl/ScreenshotSurfaceGL.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/DMAHeapBufferObject.h"
+#include "utils/UDMABufferObject.h"
+#include "utils/log.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include <EGL/eglext.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+void CWinSystemWaylandEGLContextGL::Register()
+{
+ CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "wayland");
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemWaylandEGLContextGL::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemWaylandEGLContextGL>();
+}
+
+bool CWinSystemWaylandEGLContextGL::InitWindowSystem()
+{
+ if (!CWinSystemWaylandEGLContext::InitWindowSystemEGL(EGL_OPENGL_BIT, EGL_OPENGL_API))
+ {
+ return false;
+ }
+
+ CLinuxRendererGL::Register();
+ RETRO::CRPProcessInfo::RegisterRendererFactory(new RETRO::CRendererFactoryDMA);
+ RETRO::CRPProcessInfo::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL);
+
+ bool general, deepColor;
+ m_vaapiProxy.reset(WAYLAND::VaapiProxyCreate());
+ WAYLAND::VaapiProxyConfig(m_vaapiProxy.get(), GetConnection()->GetDisplay(),
+ m_eglContext.GetEGLDisplay());
+ WAYLAND::VAAPIRegisterRenderGL(m_vaapiProxy.get(), general, deepColor);
+ if (general)
+ {
+ WAYLAND::VAAPIRegister(m_vaapiProxy.get(), deepColor);
+ }
+
+ CBufferObjectFactory::ClearBufferObjects();
+#if defined(HAVE_LINUX_MEMFD) && defined(HAVE_LINUX_UDMABUF)
+ CUDMABufferObject::Register();
+#endif
+#if defined(HAVE_LINUX_DMA_HEAP)
+ CDMAHeapBufferObject::Register();
+#endif
+
+ CScreenshotSurfaceGL::Register();
+
+ return true;
+}
+
+bool CWinSystemWaylandEGLContextGL::CreateContext()
+{
+ const EGLint glMajor = 3;
+ const EGLint glMinor = 2;
+
+ CEGLAttributesVec contextAttribs;
+ contextAttribs.Add({{EGL_CONTEXT_MAJOR_VERSION_KHR, glMajor},
+ {EGL_CONTEXT_MINOR_VERSION_KHR, glMinor},
+ {EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR}});
+
+ if (!m_eglContext.CreateContext(contextAttribs))
+ {
+ CEGLAttributesVec fallbackContextAttribs;
+ fallbackContextAttribs.Add({{EGL_CONTEXT_CLIENT_VERSION, 2}});
+
+ if (!m_eglContext.CreateContext(fallbackContextAttribs))
+ {
+ CLog::Log(LOGERROR, "EGL context creation failed");
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Your OpenGL drivers do not support OpenGL {}.{} core profile. Kodi will run in compatibility mode, but performance may suffer.", glMajor, glMinor);
+ }
+ }
+
+ return true;
+}
+
+void CWinSystemWaylandEGLContextGL::SetContextSize(CSizeInt size)
+{
+ CWinSystemWaylandEGLContext::SetContextSize(size);
+
+ // Propagate changed dimensions to render system if necessary
+ if (CRenderSystemGL::m_width != size.Width() || CRenderSystemGL::m_height != size.Height())
+ {
+ CLog::LogF(LOGDEBUG, "Resetting render system to {}x{}", size.Width(), size.Height());
+ CRenderSystemGL::ResetRenderSystem(size.Width(), size.Height());
+ }
+}
+
+void CWinSystemWaylandEGLContextGL::SetVSyncImpl(bool enable)
+{
+ // Unsupported
+}
+
+void CWinSystemWaylandEGLContextGL::PresentRenderImpl(bool rendered)
+{
+ PresentFrame(rendered);
+}
+
+void CWinSystemWaylandEGLContextGL::delete_CVaapiProxy::operator()(CVaapiProxy *p) const
+{
+ WAYLAND::VaapiProxyDelete(p);
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.h b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.h
new file mode 100644
index 0000000..540a7b8
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "WinSystemWaylandEGLContext.h"
+#include "rendering/gl/RenderSystemGL.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CVaapiProxy;
+
+class CWinSystemWaylandEGLContextGL : public CWinSystemWaylandEGLContext, public CRenderSystemGL
+{
+public:
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemWaylandEGLContext
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool InitWindowSystem() override;
+
+protected:
+ bool CreateContext() override;
+ void SetContextSize(CSizeInt size) override;
+ void SetVSyncImpl(bool enable) override;
+ void PresentRenderImpl(bool rendered) override;
+ struct delete_CVaapiProxy
+ {
+ void operator()(CVaapiProxy *p) const;
+ };
+ std::unique_ptr<CVaapiProxy, delete_CVaapiProxy> m_vaapiProxy;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.cpp b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.cpp
new file mode 100644
index 0000000..d7283a7
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WinSystemWaylandEGLContextGLES.h"
+
+#include "OptionalsReg.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "rendering/gles/ScreenshotSurfaceGLES.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/DMAHeapBufferObject.h"
+#include "utils/UDMABufferObject.h"
+#include "utils/log.h"
+#include "windowing/WindowSystemFactory.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+void CWinSystemWaylandEGLContextGLES::Register()
+{
+ CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "wayland");
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemWaylandEGLContextGLES::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemWaylandEGLContextGLES>();
+}
+
+bool CWinSystemWaylandEGLContextGLES::InitWindowSystem()
+{
+ if (!CWinSystemWaylandEGLContext::InitWindowSystemEGL(EGL_OPENGL_ES2_BIT, EGL_OPENGL_ES_API))
+ {
+ return false;
+ }
+
+ CLinuxRendererGLES::Register();
+
+ CDVDVideoCodecDRMPRIME::Register();
+ CRendererDRMPRIMEGLES::Register();
+
+ RETRO::CRPProcessInfo::RegisterRendererFactory(new RETRO::CRendererFactoryDMA);
+ RETRO::CRPProcessInfo::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES);
+
+ bool general, deepColor;
+ m_vaapiProxy.reset(WAYLAND::VaapiProxyCreate());
+ WAYLAND::VaapiProxyConfig(m_vaapiProxy.get(), GetConnection()->GetDisplay(),
+ m_eglContext.GetEGLDisplay());
+ WAYLAND::VAAPIRegisterRenderGLES(m_vaapiProxy.get(), general, deepColor);
+ if (general)
+ {
+ WAYLAND::VAAPIRegister(m_vaapiProxy.get(), deepColor);
+ }
+
+ CBufferObjectFactory::ClearBufferObjects();
+#if defined(HAVE_LINUX_MEMFD) && defined(HAVE_LINUX_UDMABUF)
+ CUDMABufferObject::Register();
+#endif
+#if defined(HAVE_LINUX_DMA_HEAP)
+ CDMAHeapBufferObject::Register();
+#endif
+
+ CScreenshotSurfaceGLES::Register();
+
+ return true;
+}
+
+bool CWinSystemWaylandEGLContextGLES::CreateContext()
+{
+ CEGLAttributesVec contextAttribs;
+ contextAttribs.Add({{EGL_CONTEXT_CLIENT_VERSION, 2}});
+
+ if (!m_eglContext.CreateContext(contextAttribs))
+ {
+ CLog::Log(LOGERROR, "EGL context creation failed");
+ return false;
+ }
+ return true;
+}
+
+void CWinSystemWaylandEGLContextGLES::SetContextSize(CSizeInt size)
+{
+ CWinSystemWaylandEGLContext::SetContextSize(size);
+
+ // Propagate changed dimensions to render system if necessary
+ if (CRenderSystemGLES::m_width != size.Width() || CRenderSystemGLES::m_height != size.Height())
+ {
+ CLog::LogF(LOGDEBUG, "Resetting render system to {}x{}", size.Width(), size.Height());
+ CRenderSystemGLES::ResetRenderSystem(size.Width(), size.Height());
+ }
+}
+
+void CWinSystemWaylandEGLContextGLES::SetVSyncImpl(bool enable)
+{
+ // Unsupported
+}
+
+void CWinSystemWaylandEGLContextGLES::PresentRenderImpl(bool rendered)
+{
+ PresentFrame(rendered);
+}
+
+void CWinSystemWaylandEGLContextGLES::delete_CVaapiProxy::operator()(CVaapiProxy *p) const
+{
+ WAYLAND::VaapiProxyDelete(p);
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.h b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.h
new file mode 100644
index 0000000..0a3c71c
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "WinSystemWaylandEGLContext.h"
+#include "rendering/gles/RenderSystemGLES.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CVaapiProxy;
+
+class CWinSystemWaylandEGLContextGLES : public CWinSystemWaylandEGLContext, public CRenderSystemGLES
+{
+public:
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemWaylandEGLContext
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool InitWindowSystem() override;
+
+protected:
+ bool CreateContext() override;
+ void SetContextSize(CSizeInt size) override;
+ void SetVSyncImpl(bool enable) override;
+ void PresentRenderImpl(bool rendered) override;
+ struct delete_CVaapiProxy
+ {
+ void operator()(CVaapiProxy *p) const;
+ };
+ std::unique_ptr<CVaapiProxy, delete_CVaapiProxy> m_vaapiProxy;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WindowDecorationHandler.h b/xbmc/windowing/wayland/WindowDecorationHandler.h
new file mode 100644
index 0000000..b02f39b
--- /dev/null
+++ b/xbmc/windowing/wayland/WindowDecorationHandler.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <cstdint>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Handler for reacting to events originating in window decorations, such as
+ * moving the window by clicking and dragging
+ */
+class IWindowDecorationHandler
+{
+public:
+ virtual void OnWindowMove(wayland::seat_t const& seat, std::uint32_t serial) = 0;
+ virtual void OnWindowResize(wayland::seat_t const& seat, std::uint32_t serial, wayland::shell_surface_resize edge) = 0;
+ virtual void OnWindowShowContextMenu(wayland::seat_t const& seat, std::uint32_t serial, CPointInt position) = 0;
+ virtual void OnWindowMinimize() = 0;
+ virtual void OnWindowMaximize() = 0;
+ virtual void OnWindowClose() = 0;
+
+ virtual ~IWindowDecorationHandler() = default;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WindowDecorator.cpp b/xbmc/windowing/wayland/WindowDecorator.cpp
new file mode 100644
index 0000000..9f61481
--- /dev/null
+++ b/xbmc/windowing/wayland/WindowDecorator.cpp
@@ -0,0 +1,1043 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WindowDecorator.h"
+
+#include "Util.h"
+#include "utils/EndianSwap.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+#include <mutex>
+#include <vector>
+
+#include <linux/input-event-codes.h>
+
+using namespace KODI::UTILS::POSIX;
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+
+namespace
+{
+
+/// Bytes per pixel in shm storage
+constexpr int BYTES_PER_PIXEL{4};
+/// Width of the visible border around the whole window
+constexpr int VISIBLE_BORDER_WIDTH{5};
+/// Width of the invisible border around the whole window for easier resizing
+constexpr int RESIZE_BORDER_WIDTH{10};
+/// Total width of the border around the window
+constexpr int BORDER_WIDTH{VISIBLE_BORDER_WIDTH + RESIZE_BORDER_WIDTH};
+/// Height of the top bar
+constexpr int TOP_BAR_HEIGHT{33};
+/// Maximum distance from the window corner to consider position valid for resize
+constexpr int RESIZE_MAX_CORNER_DISTANCE{BORDER_WIDTH};
+/// Distance of buttons from edges of the top bar
+constexpr int BUTTONS_EDGE_DISTANCE{6};
+/// Distance from button inner edge to button content
+constexpr int BUTTON_INNER_SEPARATION{4};
+/// Button size
+constexpr int BUTTON_SIZE{21};
+
+constexpr std::uint32_t TRANSPARENT{0x00000000u};
+constexpr std::uint32_t BORDER_COLOR{0xFF000000u};
+constexpr std::uint32_t BUTTON_COLOR_ACTIVE{0xFFFFFFFFu};
+constexpr std::uint32_t BUTTON_COLOR_INACTIVE{0xFF777777u};
+constexpr std::uint32_t BUTTON_HOVER_COLOR{0xFF555555u};
+
+static_assert(BUTTON_SIZE <= TOP_BAR_HEIGHT - BUTTONS_EDGE_DISTANCE * 2, "Buttons must fit in top bar");
+
+/*
+ * Decorations consist of four surfaces, one for each edge of the window. It would
+ * also be possible to position one single large surface behind the main surface
+ * instead, but that would waste a lot of memory on big/high-density screens.
+ *
+ * The four surfaces are laid out as follows: Top and bottom surfaces go over the
+ * whole width of the main surface plus the left and right borders.
+ * Left and right surfaces only go over the height of the main surface without
+ * the top and bottom borders.
+ *
+ * ---------------------------------------------
+ * | TOP |
+ * ---------------------------------------------
+ * | | | |
+ * | L | | R |
+ * | E | | I |
+ * | F | Main surface | G |
+ * | T | | H |
+ * | | | T |
+ * | | | |
+ * ---------------------------------------------
+ * | BOTTOM |
+ * ---------------------------------------------
+ */
+
+
+CRectInt SurfaceGeometry(SurfaceIndex type, CSizeInt mainSurfaceSize)
+{
+ // Coordinates are relative to main surface
+ switch (type)
+ {
+ case SURFACE_TOP:
+ return {
+ CPointInt{-BORDER_WIDTH, -(BORDER_WIDTH + TOP_BAR_HEIGHT)},
+ CSizeInt{mainSurfaceSize.Width() + 2 * BORDER_WIDTH, TOP_BAR_HEIGHT + BORDER_WIDTH}
+ };
+ case SURFACE_RIGHT:
+ return {
+ CPointInt{mainSurfaceSize.Width(), 0},
+ CSizeInt{BORDER_WIDTH, mainSurfaceSize.Height()}
+ };
+ case SURFACE_BOTTOM:
+ return {
+ CPointInt{-BORDER_WIDTH, mainSurfaceSize.Height()},
+ CSizeInt{mainSurfaceSize.Width() + 2 * BORDER_WIDTH, BORDER_WIDTH}
+ };
+ case SURFACE_LEFT:
+ return {
+ CPointInt{-BORDER_WIDTH, 0},
+ CSizeInt{BORDER_WIDTH, mainSurfaceSize.Height()}
+ };
+ default:
+ throw std::logic_error("Invalid surface type");
+ }
+}
+
+CRectInt SurfaceOpaqueRegion(SurfaceIndex type, CSizeInt mainSurfaceSize)
+{
+ // Coordinates are relative to main surface
+ auto size = SurfaceGeometry(type, mainSurfaceSize).ToSize();
+ switch (type)
+ {
+ case SURFACE_TOP:
+ return {
+ CPointInt{RESIZE_BORDER_WIDTH, RESIZE_BORDER_WIDTH},
+ size - CSizeInt{RESIZE_BORDER_WIDTH * 2, RESIZE_BORDER_WIDTH}
+ };
+ case SURFACE_RIGHT:
+ return {
+ CPointInt{},
+ size - CSizeInt{RESIZE_BORDER_WIDTH, 0}
+ };
+ case SURFACE_BOTTOM:
+ return {
+ CPointInt{RESIZE_BORDER_WIDTH, 0},
+ size - CSizeInt{RESIZE_BORDER_WIDTH * 2, RESIZE_BORDER_WIDTH}
+ };
+ case SURFACE_LEFT:
+ return {
+ CPointInt{RESIZE_BORDER_WIDTH, 0},
+ size - CSizeInt{RESIZE_BORDER_WIDTH, 0}
+ };
+ default:
+ throw std::logic_error("Invalid surface type");
+ }
+}
+
+CRectInt SurfaceWindowRegion(SurfaceIndex type, CSizeInt mainSurfaceSize)
+{
+ return SurfaceOpaqueRegion(type, mainSurfaceSize);
+}
+
+/**
+ * Full size of decorations to be added to the main surface size
+ */
+CSizeInt DecorationSize()
+{
+ return {2 * VISIBLE_BORDER_WIDTH, 2 * VISIBLE_BORDER_WIDTH + TOP_BAR_HEIGHT};
+}
+
+std::size_t MemoryBytesForSize(CSizeInt windowSurfaceSize, int scale)
+{
+ std::size_t size{};
+
+ for (auto surface : { SURFACE_TOP, SURFACE_RIGHT, SURFACE_BOTTOM, SURFACE_LEFT })
+ {
+ size += SurfaceGeometry(surface, windowSurfaceSize).Area();
+ }
+
+ size *= scale * scale;
+
+ size *= BYTES_PER_PIXEL;
+
+ return size;
+}
+
+std::size_t PositionInBuffer(CWindowDecorator::Buffer& buffer, CPointInt position)
+{
+ if (position.x < 0 || position.y < 0)
+ {
+ throw std::invalid_argument("Position out of bounds");
+ }
+ std::size_t offset{static_cast<std::size_t> (buffer.size.Width() * position.y + position.x)};
+ if (offset * BYTES_PER_PIXEL >= buffer.dataSize)
+ {
+ throw std::invalid_argument("Position out of bounds");
+ }
+ return offset;
+}
+
+void DrawHorizontalLine(CWindowDecorator::Buffer& buffer, std::uint32_t color, CPointInt position, int length)
+{
+ auto offsetStart = PositionInBuffer(buffer, position);
+ auto offsetEnd = PositionInBuffer(buffer, position + CPointInt{length - 1, 0});
+ if (offsetEnd < offsetStart)
+ {
+ throw std::invalid_argument("Invalid drawing coordinates");
+ }
+ std::fill(buffer.RgbaBuffer() + offsetStart, buffer.RgbaBuffer() + offsetEnd + 1, Endian_SwapLE32(color));
+}
+
+void DrawLineWithStride(CWindowDecorator::Buffer& buffer, std::uint32_t color, CPointInt position, int length, int stride)
+{
+ auto offsetStart = PositionInBuffer(buffer, position);
+ auto offsetEnd = offsetStart + stride * (length - 1);
+ if (offsetEnd * BYTES_PER_PIXEL >= buffer.dataSize)
+ {
+ throw std::invalid_argument("Position out of bounds");
+ }
+ if (offsetEnd < offsetStart)
+ {
+ throw std::invalid_argument("Invalid drawing coordinates");
+ }
+ auto* memory = buffer.RgbaBuffer();
+ for (std::size_t offset = offsetStart; offset <= offsetEnd; offset += stride)
+ {
+ *(memory + offset) = Endian_SwapLE32(color);
+ }
+}
+
+void DrawVerticalLine(CWindowDecorator::Buffer& buffer, std::uint32_t color, CPointInt position, int length)
+{
+ DrawLineWithStride(buffer, color, position, length, buffer.size.Width());
+}
+
+/**
+ * Draw rectangle inside the specified buffer coordinates
+ */
+void DrawRectangle(CWindowDecorator::Buffer& buffer, std::uint32_t color, CRectInt rect)
+{
+ DrawHorizontalLine(buffer, color, rect.P1(), rect.Width());
+ DrawVerticalLine(buffer, color, rect.P1(), rect.Height());
+ DrawHorizontalLine(buffer, color, rect.P1() + CPointInt{1, rect.Height() - 1}, rect.Width() - 1);
+ DrawVerticalLine(buffer, color, rect.P1() + CPointInt{rect.Width() - 1, 1}, rect.Height() - 1);
+}
+
+void FillRectangle(CWindowDecorator::Buffer& buffer, std::uint32_t color, CRectInt rect)
+{
+ for (int y{rect.y1}; y <= rect.y2; y++)
+ {
+ DrawHorizontalLine(buffer, color, {rect.x1, y}, rect.Width() + 1);
+ }
+}
+
+void DrawHorizontalLine(CWindowDecorator::Surface& surface, std::uint32_t color, CPointInt position, int length)
+{
+ for (int i{0}; i < surface.scale; i++)
+ {
+ DrawHorizontalLine(surface.buffer, color, position * surface.scale + CPointInt{0, i}, length * surface.scale);
+ }
+}
+
+void DrawAngledLine(CWindowDecorator::Surface& surface, std::uint32_t color, CPointInt position, int length, int stride)
+{
+ for (int i{0}; i < surface.scale; i++)
+ {
+ DrawLineWithStride(surface.buffer, color, position * surface.scale + CPointInt{i, 0}, length * surface.scale, surface.buffer.size.Width() + stride);
+ }
+}
+
+void DrawVerticalLine(CWindowDecorator::Surface& surface, std::uint32_t color, CPointInt position, int length)
+{
+ DrawAngledLine(surface, color, position, length, 0);
+}
+
+/**
+ * Draw rectangle inside the specified surface coordinates
+ */
+void DrawRectangle(CWindowDecorator::Surface& surface, std::uint32_t color, CRectInt rect)
+{
+ for (int i{0}; i < surface.scale; i++)
+ {
+ DrawRectangle(surface.buffer, color, {rect.P1() * surface.scale + CPointInt{i, i}, (rect.P2() + CPointInt{1, 1}) * surface.scale - CPointInt{i, i} - CPointInt{1, 1}});
+ }
+}
+
+void FillRectangle(CWindowDecorator::Surface& surface, std::uint32_t color, CRectInt rect)
+{
+ FillRectangle(surface.buffer, color, {rect.P1() * surface.scale, (rect.P2() + CPointInt{1, 1}) * surface.scale - CPointInt{1, 1}});
+}
+
+void DrawButton(CWindowDecorator::Surface& surface, std::uint32_t lineColor, CRectInt rect, bool hover)
+{
+ if (hover)
+ {
+ FillRectangle(surface, BUTTON_HOVER_COLOR, rect);
+ }
+ DrawRectangle(surface, lineColor, rect);
+}
+
+wayland::shell_surface_resize ResizeEdgeForPosition(SurfaceIndex surface, CSizeInt surfaceSize, CPointInt position)
+{
+ switch (surface)
+ {
+ case SURFACE_TOP:
+ if (position.y <= RESIZE_MAX_CORNER_DISTANCE)
+ {
+ if (position.x <= RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::top_left;
+ }
+ else if (position.x >= surfaceSize.Width() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::top_right;
+ }
+ else
+ {
+ return wayland::shell_surface_resize::top;
+ }
+ }
+ else
+ {
+ if (position.x <= RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::left;
+ }
+ else if (position.x >= surfaceSize.Width() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::right;
+ }
+ else
+ {
+ // Inside title bar, not resizing
+ return wayland::shell_surface_resize::none;
+ }
+ }
+ case SURFACE_RIGHT:
+ if (position.y >= surfaceSize.Height() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::bottom_right;
+ }
+ else
+ {
+ return wayland::shell_surface_resize::right;
+ }
+ case SURFACE_BOTTOM:
+ if (position.x <= RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::bottom_left;
+ }
+ else if (position.x >= surfaceSize.Width() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::bottom_right;
+ }
+ else
+ {
+ return wayland::shell_surface_resize::bottom;
+ }
+ case SURFACE_LEFT:
+ if (position.y >= surfaceSize.Height() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::bottom_left;
+ }
+ else
+ {
+ return wayland::shell_surface_resize::left;
+ }
+
+ default:
+ return wayland::shell_surface_resize::none;
+ }
+}
+
+/**
+ * Get name for resize cursor according to xdg cursor-spec
+ */
+std::string CursorForResizeEdge(wayland::shell_surface_resize edge)
+{
+ if (edge == wayland::shell_surface_resize::top)
+ return "n-resize";
+ else if (edge == wayland::shell_surface_resize::bottom)
+ return "s-resize";
+ else if (edge == wayland::shell_surface_resize::left)
+ return "w-resize";
+ else if (edge == wayland::shell_surface_resize::top_left)
+ return "nw-resize";
+ else if (edge == wayland::shell_surface_resize::bottom_left)
+ return "sw-resize";
+ else if (edge == wayland::shell_surface_resize::right)
+ return "e-resize";
+ else if (edge == wayland::shell_surface_resize::top_right)
+ return "ne-resize";
+ else if (edge == wayland::shell_surface_resize::bottom_right)
+ return "se-resize";
+ else
+ return "";
+}
+
+template<typename T, typename InstanceProviderT>
+bool HandleCapabilityChange(const wayland::seat_capability& caps,
+ const wayland::seat_capability& cap,
+ T& proxy,
+ InstanceProviderT instanceProvider)
+{
+ bool hasCapability = caps & cap;
+
+ if ((!!proxy) != hasCapability)
+ {
+ // Capability changed
+
+ if (hasCapability)
+ {
+ // The capability was added
+ proxy = instanceProvider();
+ return true;
+ }
+ else
+ {
+ // The capability was removed
+ proxy.proxy_release();
+ }
+ }
+
+ return false;
+}
+
+}
+
+CWindowDecorator::CWindowDecorator(IWindowDecorationHandler& handler, CConnection& connection, wayland::surface_t const& mainSurface)
+: m_handler{handler}, m_registry{connection}, m_mainSurface{mainSurface}, m_buttonColor{BUTTON_COLOR_ACTIVE}
+{
+ static_assert(std::tuple_size<decltype(m_borderSurfaces)>::value == SURFACE_COUNT, "SURFACE_COUNT must match surfaces array size");
+
+ m_registry.RequestSingleton(m_compositor, 1, 4);
+ m_registry.RequestSingleton(m_subcompositor, 1, 1, false);
+ m_registry.RequestSingleton(m_shm, 1, 1);
+
+ m_registry.Bind();
+}
+
+void CWindowDecorator::AddSeat(CSeat* seat)
+{
+ m_seats.emplace(std::piecewise_construct, std::forward_as_tuple(seat->GetGlobalName()), std::forward_as_tuple(seat));
+ seat->AddRawInputHandlerTouch(this);
+ seat->AddRawInputHandlerPointer(this);
+}
+
+void CWindowDecorator::RemoveSeat(CSeat* seat)
+{
+ seat->RemoveRawInputHandlerTouch(this);
+ seat->RemoveRawInputHandlerPointer(this);
+ m_seats.erase(seat->GetGlobalName());
+ UpdateButtonHoverState();
+}
+
+void CWindowDecorator::OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ auto& seatState = seatStateI->second;
+ // Reset first so we ignore events for surfaces we don't handle
+ seatState.currentSurface = SURFACE_COUNT;
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ for (std::size_t i{0}; i < m_borderSurfaces.size(); i++)
+ {
+ if (m_borderSurfaces[i].surface.wlSurface == surface)
+ {
+ seatState.pointerEnterSerial = serial;
+ seatState.currentSurface = static_cast<SurfaceIndex> (i);
+ seatState.pointerPosition = {static_cast<float> (surfaceX), static_cast<float> (surfaceY)};
+ UpdateSeatCursor(seatState);
+ UpdateButtonHoverState();
+ break;
+ }
+ }
+}
+
+void CWindowDecorator::OnPointerLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ auto& seatState = seatStateI->second;
+ seatState.currentSurface = SURFACE_COUNT;
+ // Recreate cursor surface on reenter
+ seatState.cursorName.clear();
+ seatState.cursor.proxy_release();
+ UpdateButtonHoverState();
+}
+
+void CWindowDecorator::OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ auto& seatState = seatStateI->second;
+ if (seatState.currentSurface != SURFACE_COUNT)
+ {
+ seatState.pointerPosition = {static_cast<float> (surfaceX), static_cast<float> (surfaceY)};
+ UpdateSeatCursor(seatState);
+ UpdateButtonHoverState();
+ }
+}
+
+void CWindowDecorator::OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ const auto& seatState = seatStateI->second;
+ if (seatState.currentSurface != SURFACE_COUNT && state == wayland::pointer_button_state::pressed)
+ {
+ HandleSeatClick(seatState, seatState.currentSurface, serial, button, seatState.pointerPosition);
+ }
+}
+
+void CWindowDecorator::OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ auto& seatState = seatStateI->second;
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ for (std::size_t i{0}; i < m_borderSurfaces.size(); i++)
+ {
+ if (m_borderSurfaces[i].surface.wlSurface == surface)
+ {
+ HandleSeatClick(seatState, static_cast<SurfaceIndex> (i), serial, BTN_LEFT, {static_cast<float> (x), static_cast<float> (y)});
+ }
+ }
+}
+
+void CWindowDecorator::UpdateSeatCursor(SeatState& seatState)
+{
+ if (seatState.currentSurface == SURFACE_COUNT)
+ {
+ // Don't set anything if not on any surface
+ return;
+ }
+
+ LoadCursorTheme();
+
+ std::string cursorName{"default"};
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ auto resizeEdge = ResizeEdgeForPosition(seatState.currentSurface, SurfaceGeometry(seatState.currentSurface, m_mainSurfaceSize).ToSize(), CPointInt{seatState.pointerPosition});
+ if (resizeEdge != wayland::shell_surface_resize::none)
+ {
+ cursorName = CursorForResizeEdge(resizeEdge);
+ }
+ }
+
+ if (cursorName == seatState.cursorName)
+ {
+ // Don't reload cursor all the time when nothing is changing
+ return;
+ }
+ seatState.cursorName = cursorName;
+
+ wayland::cursor_t cursor;
+ try
+ {
+ cursor = CCursorUtil::LoadFromTheme(m_cursorTheme, cursorName);
+ }
+ catch (std::exception const& e)
+ {
+ CLog::LogF(LOGERROR, "Could not get required cursor {} from cursor theme: {}", cursorName,
+ e.what());
+ return;
+ }
+ auto cursorImage = cursor.image(0);
+
+ if (!seatState.cursor)
+ {
+ seatState.cursor = m_compositor.create_surface();
+ }
+ int calcScale{seatState.cursor.can_set_buffer_scale() ? m_scale : 1};
+
+ seatState.seat->SetCursor(seatState.pointerEnterSerial, seatState.cursor, cursorImage.hotspot_x() / calcScale, cursorImage.hotspot_y() / calcScale);
+ seatState.cursor.attach(cursorImage.get_buffer(), 0, 0);
+ seatState.cursor.damage(0, 0, cursorImage.width() / calcScale, cursorImage.height() / calcScale);
+ if (seatState.cursor.can_set_buffer_scale())
+ {
+ seatState.cursor.set_buffer_scale(m_scale);
+ }
+ seatState.cursor.commit();
+}
+
+void CWindowDecorator::UpdateButtonHoverState()
+{
+ std::vector<CPoint> pointerPositions;
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ for (auto const& seatPair : m_seats)
+ {
+ auto const& seat = seatPair.second;
+ if (seat.currentSurface == SURFACE_TOP)
+ {
+ pointerPositions.emplace_back(seat.pointerPosition);
+ }
+ }
+
+ bool changed{false};
+ for (auto& button : m_buttons)
+ {
+ bool wasHovered{button.hovered};
+ button.hovered = std::any_of(pointerPositions.cbegin(), pointerPositions.cend(), [&](CPoint point) { return button.position.PtInRect(CPointInt{point}); });
+ changed = changed || (button.hovered != wasHovered);
+ }
+
+ if (changed)
+ {
+ // Repaint!
+ Reset(false);
+ }
+}
+
+void CWindowDecorator::HandleSeatClick(SeatState const& seatState, SurfaceIndex surface, std::uint32_t serial, std::uint32_t button, CPoint position)
+{
+ switch (button)
+ {
+ case BTN_LEFT:
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ auto resizeEdge = ResizeEdgeForPosition(surface, SurfaceGeometry(surface, m_mainSurfaceSize).ToSize(), CPointInt{position});
+ if (resizeEdge == wayland::shell_surface_resize::none)
+ {
+ for (auto const& button : m_buttons)
+ {
+ if (button.position.PtInRect(CPointInt{position}))
+ {
+ button.onClick();
+ return;
+ }
+ }
+
+ m_handler.OnWindowMove(seatState.seat->GetWlSeat(), serial);
+ }
+ else
+ {
+ m_handler.OnWindowResize(seatState.seat->GetWlSeat(), serial, resizeEdge);
+ }
+ }
+ break;
+ case BTN_RIGHT:
+ if (surface == SURFACE_TOP)
+ {
+ m_handler.OnWindowShowContextMenu(seatState.seat->GetWlSeat(), serial, CPointInt{position} - CPointInt{BORDER_WIDTH, BORDER_WIDTH + TOP_BAR_HEIGHT});
+ }
+ break;
+ }
+}
+
+CWindowDecorator::BorderSurface CWindowDecorator::MakeBorderSurface()
+{
+ Surface surface;
+ surface.wlSurface = m_compositor.create_surface();
+ auto subsurface = m_subcompositor.get_subsurface(surface.wlSurface, m_mainSurface);
+
+ CWindowDecorator::BorderSurface boarderSurface;
+ boarderSurface.surface = surface;
+ boarderSurface.subsurface = subsurface;
+
+ return boarderSurface;
+}
+
+bool CWindowDecorator::IsDecorationActive() const
+{
+ return StateHasWindowDecorations(m_windowState);
+}
+
+bool CWindowDecorator::StateHasWindowDecorations(IShellSurface::StateBitset state) const
+{
+ // No decorations possible if subcompositor not available
+ return m_subcompositor && !state.test(IShellSurface::STATE_FULLSCREEN);
+}
+
+CSizeInt CWindowDecorator::CalculateMainSurfaceSize(CSizeInt size, IShellSurface::StateBitset state) const
+{
+ if (StateHasWindowDecorations(state))
+ {
+ // Subtract decorations
+ return size - DecorationSize();
+ }
+ else
+ {
+ // Fullscreen -> no decorations
+ return size;
+ }
+}
+
+CSizeInt CWindowDecorator::CalculateFullSurfaceSize(CSizeInt size, IShellSurface::StateBitset state) const
+{
+ if (StateHasWindowDecorations(state))
+ {
+ // Add decorations
+ return size + DecorationSize();
+ }
+ else
+ {
+ // Fullscreen -> no decorations
+ return size;
+ }
+}
+
+void CWindowDecorator::SetState(CSizeInt size, int scale, IShellSurface::StateBitset state)
+{
+ CSizeInt mainSurfaceSize{CalculateMainSurfaceSize(size, state)};
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ if (mainSurfaceSize == m_mainSurfaceSize && scale == m_scale && state == m_windowState)
+ {
+ return;
+ }
+
+ bool wasDecorations{IsDecorationActive()};
+ m_windowState = state;
+
+ m_buttonColor = m_windowState.test(IShellSurface::STATE_ACTIVATED) ? BUTTON_COLOR_ACTIVE : BUTTON_COLOR_INACTIVE;
+
+ CLog::Log(LOGDEBUG,
+ "CWindowDecorator::SetState: Setting full surface size {}x{} scale {} (main surface "
+ "size {}x{}), decorations active: {}",
+ size.Width(), size.Height(), scale, mainSurfaceSize.Width(), mainSurfaceSize.Height(),
+ IsDecorationActive());
+
+ if (mainSurfaceSize != m_mainSurfaceSize || scale != m_scale || wasDecorations != IsDecorationActive())
+ {
+ if (scale != m_scale)
+ {
+ // Reload cursor theme
+ CLog::Log(LOGDEBUG, "CWindowDecorator::SetState: Buffer scale changed, reloading cursor theme");
+ m_cursorTheme = wayland::cursor_theme_t{};
+ for (auto& seat : m_seats)
+ {
+ UpdateSeatCursor(seat.second);
+ }
+ }
+
+ m_mainSurfaceSize = mainSurfaceSize;
+ m_scale = scale;
+ CLog::Log(LOGDEBUG, "CWindowDecorator::SetState: Resetting decorations");
+ Reset(true);
+ }
+ else if (IsDecorationActive())
+ {
+ CLog::Log(LOGDEBUG, "CWindowDecorator::SetState: Repainting decorations");
+ // Only state differs, no reallocation needed
+ Reset(false);
+ }
+}
+
+void CWindowDecorator::Reset(bool reallocate)
+{
+ // The complete reset operation should be seen as one atomic update to the
+ // internal state, otherwise buffer/surface state might be mismatched
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (reallocate)
+ {
+ ResetButtons();
+ ResetSurfaces();
+ ResetShm();
+ if (IsDecorationActive())
+ {
+ AllocateBuffers();
+ ReattachSubsurfaces();
+ PositionButtons();
+ }
+ }
+
+ if (IsDecorationActive())
+ {
+ Repaint();
+ }
+}
+
+void CWindowDecorator::ResetButtons()
+{
+ if (IsDecorationActive())
+ {
+ if (m_buttons.empty())
+ {
+ // Minimize
+ m_buttons.emplace_back();
+ Button& minimize = m_buttons.back();
+ minimize.draw = [this](Surface& surface, CRectInt position, bool hover)
+ {
+ DrawButton(surface, m_buttonColor, position, hover);
+ DrawHorizontalLine(surface, m_buttonColor, position.P1() + CPointInt{BUTTON_INNER_SEPARATION, position.Height() - BUTTON_INNER_SEPARATION - 1}, position.Width() - 2 * BUTTON_INNER_SEPARATION);
+ };
+ minimize.onClick = [this] { m_handler.OnWindowMinimize(); };
+
+ // Maximize
+ m_buttons.emplace_back();
+ Button& maximize = m_buttons.back();
+ maximize.draw = [this](Surface& surface, CRectInt position, bool hover)
+ {
+ DrawButton(surface, m_buttonColor, position, hover);
+ DrawRectangle(surface, m_buttonColor, {position.P1() + CPointInt{BUTTON_INNER_SEPARATION, BUTTON_INNER_SEPARATION}, position.P2() - CPointInt{BUTTON_INNER_SEPARATION, BUTTON_INNER_SEPARATION}});
+ DrawHorizontalLine(surface, m_buttonColor, position.P1() + CPointInt{BUTTON_INNER_SEPARATION, BUTTON_INNER_SEPARATION + 1}, position.Width() - 2 * BUTTON_INNER_SEPARATION);
+ };
+ maximize.onClick = [this] { m_handler.OnWindowMaximize(); };
+
+ // Close
+ m_buttons.emplace_back();
+ Button& close = m_buttons.back();
+ close.draw = [this](Surface& surface, CRectInt position, bool hover)
+ {
+ DrawButton(surface, m_buttonColor, position, hover);
+ auto height = position.Height() - 2 * BUTTON_INNER_SEPARATION;
+ DrawAngledLine(surface, m_buttonColor, position.P1() + CPointInt{BUTTON_INNER_SEPARATION, BUTTON_INNER_SEPARATION}, height, 1);
+ DrawAngledLine(surface, m_buttonColor, position.P1() + CPointInt{position.Width() - BUTTON_INNER_SEPARATION - 1, BUTTON_INNER_SEPARATION}, height, -1);
+ };
+ close.onClick = [this] { m_handler.OnWindowClose(); };
+ }
+ }
+ else
+ {
+ m_buttons.clear();
+ }
+}
+
+void CWindowDecorator::ResetSurfaces()
+{
+ if (IsDecorationActive())
+ {
+ if (!m_borderSurfaces.front().surface.wlSurface)
+ {
+ std::generate(m_borderSurfaces.begin(), m_borderSurfaces.end(), std::bind(&CWindowDecorator::MakeBorderSurface, this));
+ }
+ }
+ else
+ {
+ for (auto& borderSurface : m_borderSurfaces)
+ {
+ auto& wlSurface = borderSurface.surface.wlSurface;
+ if (wlSurface)
+ {
+ // Destroying the surface would cause some flicker because it takes effect
+ // immediately, before the next commit on the main surface - just make it
+ // invisible by attaching a NULL buffer
+ wlSurface.attach(wayland::buffer_t{}, 0, 0);
+ wlSurface.set_opaque_region(wayland::region_t{});
+ wlSurface.commit();
+ }
+ }
+ }
+}
+
+void CWindowDecorator::ReattachSubsurfaces()
+{
+ for (auto& surface : m_borderSurfaces)
+ {
+ surface.subsurface.set_position(surface.geometry.x1, surface.geometry.y1);
+ }
+}
+
+CRectInt CWindowDecorator::GetWindowGeometry() const
+{
+ CRectInt geometry{{0, 0}, m_mainSurfaceSize};
+
+ if (IsDecorationActive())
+ {
+ for (auto const& surface : m_borderSurfaces)
+ {
+ geometry.Union(surface.windowRect + surface.geometry.P1());
+ }
+ }
+
+ return geometry;
+}
+
+void CWindowDecorator::ResetShm()
+{
+ if (IsDecorationActive())
+ {
+ m_memory.reset(new CSharedMemory(MemoryBytesForSize(m_mainSurfaceSize, m_scale)));
+ m_memoryAllocatedSize = 0;
+ m_shmPool = m_shm.create_pool(m_memory->Fd(), m_memory->Size());
+ }
+ else
+ {
+ m_memory.reset();
+ m_shmPool.proxy_release();
+ }
+
+ for (auto& borderSurface : m_borderSurfaces)
+ {
+ // Buffers are invalid now, reset
+ borderSurface.surface.buffer = Buffer{};
+ }
+}
+
+void CWindowDecorator::PositionButtons()
+{
+ CPointInt position{m_borderSurfaces[SURFACE_TOP].surface.size.Width() - BORDER_WIDTH, BORDER_WIDTH + BUTTONS_EDGE_DISTANCE};
+ for (auto iter = m_buttons.rbegin(); iter != m_buttons.rend(); iter++)
+ {
+ position.x -= (BUTTONS_EDGE_DISTANCE + BUTTON_SIZE);
+ // Clamp if not enough space
+ position.x = std::max(0, position.x);
+
+ iter->position = CRectInt{position, position + CPointInt{BUTTON_SIZE, BUTTON_SIZE}};
+ }
+}
+
+CWindowDecorator::Buffer CWindowDecorator::GetBuffer(CSizeInt size)
+{
+ // We ignore tearing on the decorations for now.
+ // We can always implement a clever buffer management scheme later... :-)
+
+ auto totalSize{size.Area() * BYTES_PER_PIXEL};
+ if (m_memory->Size() < m_memoryAllocatedSize + totalSize)
+ {
+ // We miscalculated something
+ throw std::logic_error("Remaining SHM pool size is too small for requested buffer");
+ }
+ // argb8888 support is mandatory
+ auto buffer = m_shmPool.create_buffer(m_memoryAllocatedSize, size.Width(), size.Height(), size.Width() * BYTES_PER_PIXEL, wayland::shm_format::argb8888);
+
+ void* data{static_cast<std::uint8_t*> (m_memory->Data()) + m_memoryAllocatedSize};
+ m_memoryAllocatedSize += totalSize;
+
+ return { data, static_cast<std::size_t> (totalSize), size, std::move(buffer) };
+}
+
+void CWindowDecorator::AllocateBuffers()
+{
+ for (std::size_t i{0}; i < m_borderSurfaces.size(); i++)
+ {
+ auto& borderSurface = m_borderSurfaces[i];
+ if (!borderSurface.surface.buffer.data)
+ {
+ borderSurface.geometry = SurfaceGeometry(static_cast<SurfaceIndex> (i), m_mainSurfaceSize);
+ borderSurface.windowRect = SurfaceWindowRegion(static_cast<SurfaceIndex> (i), m_mainSurfaceSize);
+ borderSurface.surface.buffer = GetBuffer(borderSurface.geometry.ToSize() * m_scale);
+ borderSurface.surface.scale = m_scale;
+ borderSurface.surface.size = borderSurface.geometry.ToSize();
+ auto opaqueRegionGeometry = SurfaceOpaqueRegion(static_cast<SurfaceIndex> (i), m_mainSurfaceSize);
+ auto region = m_compositor.create_region();
+ region.add(opaqueRegionGeometry.x1, opaqueRegionGeometry.y1, opaqueRegionGeometry.Width(), opaqueRegionGeometry.Height());
+ borderSurface.surface.wlSurface.set_opaque_region(region);
+ if (borderSurface.surface.wlSurface.can_set_buffer_scale())
+ {
+ borderSurface.surface.wlSurface.set_buffer_scale(m_scale);
+ }
+ }
+ }
+}
+
+void CWindowDecorator::Repaint()
+{
+ // Fill transparent (outer) and color (inner)
+ for (auto& borderSurface : m_borderSurfaces)
+ {
+ std::fill_n(static_cast<std::uint32_t*> (borderSurface.surface.buffer.data), borderSurface.surface.buffer.size.Area(), Endian_SwapLE32(TRANSPARENT));
+ FillRectangle(borderSurface.surface, BORDER_COLOR, borderSurface.windowRect - CSizeInt{1, 1});
+ }
+ auto& topSurface = m_borderSurfaces[SURFACE_TOP].surface;
+ auto innerBorderColor = m_buttonColor;
+ // Draw rectangle
+ DrawHorizontalLine(topSurface, innerBorderColor, {BORDER_WIDTH - 1, BORDER_WIDTH - 1}, topSurface.size.Width() - 2 * BORDER_WIDTH + 2);
+ DrawVerticalLine(topSurface, innerBorderColor, {BORDER_WIDTH - 1, BORDER_WIDTH - 1}, topSurface.size.Height() - BORDER_WIDTH + 1);
+ DrawVerticalLine(topSurface, innerBorderColor, {topSurface.size.Width() - BORDER_WIDTH, BORDER_WIDTH - 1}, topSurface.size.Height() - BORDER_WIDTH + 1);
+ DrawVerticalLine(m_borderSurfaces[SURFACE_LEFT].surface, innerBorderColor, {BORDER_WIDTH - 1, 0}, m_borderSurfaces[SURFACE_LEFT].surface.size.Height());
+ DrawVerticalLine(m_borderSurfaces[SURFACE_RIGHT].surface, innerBorderColor, {0, 0}, m_borderSurfaces[SURFACE_RIGHT].surface.size.Height());
+ DrawHorizontalLine(m_borderSurfaces[SURFACE_BOTTOM].surface, innerBorderColor, {BORDER_WIDTH - 1, 0}, m_borderSurfaces[SURFACE_BOTTOM].surface.size.Width() - 2 * BORDER_WIDTH + 2);
+ // Draw white line into top bar as separator
+ DrawHorizontalLine(topSurface, innerBorderColor, {BORDER_WIDTH - 1, topSurface.size.Height() - 1}, topSurface.size.Width() - 2 * BORDER_WIDTH + 2);
+ // Draw buttons
+ for (auto& button : m_buttons)
+ {
+ button.draw(topSurface, button.position, button.hovered);
+ }
+
+ // Finally make everything visible
+ CommitAllBuffers();
+}
+
+void CWindowDecorator::CommitAllBuffers()
+{
+ std::unique_lock<CCriticalSection> lock(m_pendingBuffersMutex);
+
+ for (auto& borderSurface : m_borderSurfaces)
+ {
+ auto& wlSurface = borderSurface.surface.wlSurface;
+ auto& wlBuffer = borderSurface.surface.buffer.wlBuffer;
+ // Keep buffers in list so they are kept alive even when the Buffer gets
+ // recreated on size change
+ auto emplaceResult = m_pendingBuffers.emplace(wlBuffer);
+ if (emplaceResult.second)
+ {
+ // Buffer was not pending already
+ auto wlBufferC = reinterpret_cast<wl_buffer*> (wlBuffer.c_ptr());
+ // We can refer to the buffer neither by iterator (might be invalidated) nor by
+ // capturing the C++ instance in the lambda (would create a reference loop and
+ // never allow the object to be freed), so use the raw pointer for now
+ wlBuffer.on_release() = [this, wlBufferC]()
+ {
+ std::unique_lock<CCriticalSection> lock(m_pendingBuffersMutex);
+ // Construct a dummy object for searching the set
+ wayland::buffer_t findDummy(wlBufferC, wayland::proxy_t::wrapper_type::foreign);
+ auto iter = m_pendingBuffers.find(findDummy);
+ if (iter == m_pendingBuffers.end())
+ {
+ throw std::logic_error("Cannot release buffer that is not pending");
+ }
+
+ // Do not erase again until buffer is reattached (should not happen anyway, just to be safe)
+ // const_cast is OK since changing the function pointer does not affect
+ // the key in the set
+ const_cast<wayland::buffer_t&>(*iter).on_release() = nullptr;
+ m_pendingBuffers.erase(iter);
+ };
+ }
+
+ wlSurface.attach(wlBuffer, 0, 0);
+ wlSurface.damage(0, 0, borderSurface.surface.size.Width(), borderSurface.surface.size.Height());
+ wlSurface.commit();
+ }
+}
+
+void CWindowDecorator::LoadCursorTheme()
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ if (!m_cursorTheme)
+ {
+ // Load default cursor theme
+ // Base size of 24px is what most cursor themes seem to have
+ m_cursorTheme = wayland::cursor_theme_t("", 24 * m_scale, m_shm);
+ }
+}
diff --git a/xbmc/windowing/wayland/WindowDecorator.h b/xbmc/windowing/wayland/WindowDecorator.h
new file mode 100644
index 0000000..23db237
--- /dev/null
+++ b/xbmc/windowing/wayland/WindowDecorator.h
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Connection.h"
+#include "Registry.h"
+#include "Seat.h"
+#include "ShellSurface.h"
+#include "Util.h"
+#include "WindowDecorationHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/Geometry.h"
+
+#include "platform/posix/utils/SharedMemory.h"
+
+#include <array>
+#include <memory>
+#include <set>
+
+#include <wayland-client-protocol.hpp>
+#include <wayland-cursor.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+enum SurfaceIndex
+{
+ SURFACE_TOP = 0,
+ SURFACE_RIGHT,
+ SURFACE_BOTTOM,
+ SURFACE_LEFT,
+ SURFACE_COUNT
+};
+
+/**
+ * Paint decorations around windows with subcompositing
+ *
+ * With Wayland, applications are responsible for drawing borders on their windows
+ * themselves (client-side decorations). To keep the impact on the overall architecture
+ * low, the Wayland platform implementation uses this very simple renderer to
+ * build ugly but fully functional decorations around the Kodi window. Since Kodi as a
+ * media center is usually not used in windowed mode anyway (except maybe for
+ * development), the impact of the visually not-so-pleasing decorations should
+ * be limited. Nice decorations would require more effort and external libraries for
+ * proper 2D drawing.
+ *
+ * If more platforms decide that client-side decorations would be a good idea to
+ * implement, the decorations could also be integrated with the Kodi skin system.
+ * Then this class could be removed.
+ *
+ * The decorations are positioned around the main surface automatically.
+ */
+class CWindowDecorator final : IRawInputHandlerTouch, IRawInputHandlerPointer
+{
+public:
+ /**
+ * Construct window decorator
+ * \param handler handler for window decoration events
+ * \param connection connection to get Wayland globals
+ * \param mainSurface main surface that decorations are constructed around
+ * \param windowSize full size of the window including decorations
+ * \param scale scale to use for buffers
+ * \param state surface state for adjusting decoration appearance
+ */
+ CWindowDecorator(IWindowDecorationHandler& handler, CConnection& connection, wayland::surface_t const& mainSurface);
+
+ /**
+ * Set decoration state and size by providing full surface size including decorations
+ *
+ * Calculates size of the main surface from size of all surfaces combined (including
+ * window decorations) by subtracting the decoration size
+ *
+ * Decorations will be disabled if state includes STATE_FULLSCREEN
+ *
+ * Call only from main thread
+ */
+ void SetState(CSizeInt size, int scale, IShellSurface::StateBitset state);
+ /**
+ * Get calculated size of main surface
+ */
+ CSizeInt GetMainSurfaceSize() const
+ {
+ return m_mainSurfaceSize;
+ }
+ /**
+ * Get full geometry of the window, including decorations if active
+ */
+ CRectInt GetWindowGeometry() const;
+ /**
+ * Calculate size of main surface given the size of the full area
+ * including decorations and a state
+ */
+ CSizeInt CalculateMainSurfaceSize(CSizeInt size, IShellSurface::StateBitset state) const;
+ /**
+ * Calculate size of full surface including decorations given the size of the
+ * main surface and a state
+ */
+ CSizeInt CalculateFullSurfaceSize(CSizeInt mainSurfaceSize, IShellSurface::StateBitset state) const;
+
+ bool IsDecorationActive() const;
+
+ void AddSeat(CSeat* seat);
+ void RemoveSeat(CSeat* seat);
+
+ struct Buffer
+ {
+ void* data{};
+ std::size_t dataSize{};
+ CSizeInt size{};
+ wayland::buffer_t wlBuffer;
+
+ Buffer() noexcept {}
+
+ Buffer(void* data, std::size_t dataSize, CSizeInt size, wayland::buffer_t&& buffer)
+ : data{data}, dataSize{dataSize}, size{size}, wlBuffer{std::move(buffer)}
+ {}
+
+ std::uint32_t* RgbaBuffer()
+ {
+ return static_cast<std::uint32_t*> (data);
+ }
+ };
+
+ struct Surface
+ {
+ wayland::surface_t wlSurface;
+ Buffer buffer;
+ CSizeInt size;
+ int scale{1};
+ };
+
+private:
+ CWindowDecorator(CWindowDecorator const& other) = delete;
+ CWindowDecorator& operator=(CWindowDecorator const& other) = delete;
+
+ // IRawInputHandlerTouch
+ void OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y) override;
+ // IRawInputHandlerPointer
+ void OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY) override;
+ void OnPointerLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface) override;
+ void OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY) override;
+ void OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state) override;
+
+ void Reset(bool reallocate);
+
+ // These functions should not be called directly as they may leave internal
+ // structures in an inconsistent state when called individually - only call
+ // Reset().
+ void ResetButtons();
+ void ResetSurfaces();
+ void ResetShm();
+ void ReattachSubsurfaces();
+ void PositionButtons();
+ void AllocateBuffers();
+ void Repaint();
+ void CommitAllBuffers();
+
+ bool StateHasWindowDecorations(IShellSurface::StateBitset state) const;
+
+ Buffer GetBuffer(CSizeInt size);
+
+ IWindowDecorationHandler& m_handler;
+
+ CSizeInt m_mainSurfaceSize;
+ int m_scale{1};
+ IShellSurface::StateBitset m_windowState;
+
+ CRegistry m_registry;
+ wayland::shm_t m_shm;
+ wayland::shm_pool_t m_shmPool;
+ wayland::compositor_t m_compositor;
+ wayland::subcompositor_t m_subcompositor;
+ wayland::surface_t m_mainSurface;
+
+ std::unique_ptr<KODI::UTILS::POSIX::CSharedMemory> m_memory;
+ std::size_t m_memoryAllocatedSize{};
+
+ struct BorderSurface
+ {
+ Surface surface;
+ wayland::subsurface_t subsurface;
+ CRectInt geometry;
+ /// Region of the surface that should count as being part of the window
+ CRectInt windowRect;
+ };
+ BorderSurface MakeBorderSurface();
+
+ /**
+ * Mutex for all surface/button state that is accessed from the event pump thread via seat events
+ * and the main thread:
+ * m_surfaces, m_buttons, m_windowSize, m_cursorTheme
+ *
+ * If size etc. is changing, mutex should be locked for the entire duration of the
+ * change until the internal state (surface, button size etc.) is consistent again.
+ */
+ CCriticalSection m_mutex;
+
+ std::array<BorderSurface, 4> m_borderSurfaces;
+
+ std::set<wayland::buffer_t, WaylandCPtrCompare> m_pendingBuffers;
+ CCriticalSection m_pendingBuffersMutex;
+
+ struct SeatState
+ {
+ CSeat* seat;
+ SurfaceIndex currentSurface{SURFACE_COUNT};
+ CPoint pointerPosition;
+
+ std::uint32_t pointerEnterSerial{};
+ std::string cursorName;
+ wayland::surface_t cursor;
+
+ explicit SeatState(CSeat* seat)
+ : seat{seat}
+ {}
+ };
+ std::map<std::uint32_t, SeatState> m_seats;
+
+ struct Button
+ {
+ CRectInt position;
+ bool hovered{};
+ std::function<void(Surface&, CRectInt, bool /* hover */)> draw;
+ std::function<void()> onClick;
+ };
+ std::vector<Button> m_buttons;
+
+ wayland::cursor_theme_t m_cursorTheme;
+ std::uint32_t m_buttonColor;
+
+ void LoadCursorTheme();
+
+ void UpdateSeatCursor(SeatState& seatState);
+ void UpdateButtonHoverState();
+ void HandleSeatClick(SeatState const& seatState, SurfaceIndex surface, std::uint32_t serial, std::uint32_t button, CPoint position);
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/XkbcommonKeymap.cpp b/xbmc/windowing/wayland/XkbcommonKeymap.cpp
new file mode 100644
index 0000000..eb27cc9
--- /dev/null
+++ b/xbmc/windowing/wayland/XkbcommonKeymap.cpp
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "XkbcommonKeymap.h"
+
+#include "Util.h"
+#include "utils/log.h"
+
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+#include <vector>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+struct ModifierNameXBMCMapping
+{
+ const char* name;
+ XBMCMod xbmc;
+};
+
+static const std::vector<ModifierNameXBMCMapping> ModifierNameXBMCMappings = {
+ { XKB_MOD_NAME_CTRL, XBMCKMOD_LCTRL },
+ { XKB_MOD_NAME_SHIFT, XBMCKMOD_LSHIFT },
+ { XKB_MOD_NAME_LOGO, XBMCKMOD_LSUPER },
+ { XKB_MOD_NAME_ALT, XBMCKMOD_LALT },
+ { "Meta", XBMCKMOD_LMETA },
+ { "RControl", XBMCKMOD_RCTRL },
+ { "RShift", XBMCKMOD_RSHIFT },
+ { "Hyper", XBMCKMOD_RSUPER },
+ { "AltGr", XBMCKMOD_RALT },
+ { XKB_LED_NAME_CAPS, XBMCKMOD_CAPS },
+ { XKB_LED_NAME_NUM, XBMCKMOD_NUM },
+ { XKB_LED_NAME_SCROLL, XBMCKMOD_MODE }
+};
+
+static const std::map<xkb_keycode_t, XBMCKey> XkbKeycodeXBMCMappings = {
+ // Function keys before start of ASCII printable character range
+ { XKB_KEY_BackSpace, XBMCK_BACKSPACE },
+ { XKB_KEY_Tab, XBMCK_TAB },
+ { XKB_KEY_Clear, XBMCK_CLEAR },
+ { XKB_KEY_Return, XBMCK_RETURN },
+ { XKB_KEY_Pause, XBMCK_PAUSE },
+ { XKB_KEY_Escape, XBMCK_ESCAPE },
+
+ // ASCII printable range - not included here
+
+ // Function keys after end of ASCII printable character range
+ { XKB_KEY_Delete, XBMCK_DELETE },
+
+ // Multimedia keys
+ { XKB_KEY_XF86Back, XBMCK_BROWSER_BACK },
+ { XKB_KEY_XF86Forward, XBMCK_BROWSER_FORWARD },
+ { XKB_KEY_XF86Refresh, XBMCK_BROWSER_REFRESH },
+ { XKB_KEY_XF86Stop, XBMCK_BROWSER_STOP },
+ { XKB_KEY_XF86Search, XBMCK_BROWSER_SEARCH },
+ // XKB_KEY_XF86Favorites could be XBMCK_BROWSER_FAVORITES or XBMCK_FAVORITES,
+ // XBMCK_FAVORITES was chosen here because it is more general
+ { XKB_KEY_XF86HomePage, XBMCK_BROWSER_HOME },
+ { XKB_KEY_XF86AudioMute, XBMCK_VOLUME_MUTE },
+ { XKB_KEY_XF86AudioLowerVolume, XBMCK_VOLUME_DOWN },
+ { XKB_KEY_XF86AudioRaiseVolume, XBMCK_VOLUME_UP },
+ { XKB_KEY_XF86AudioNext, XBMCK_MEDIA_NEXT_TRACK },
+ { XKB_KEY_XF86AudioPrev, XBMCK_MEDIA_PREV_TRACK },
+ { XKB_KEY_XF86AudioStop, XBMCK_MEDIA_STOP },
+ { XKB_KEY_XF86AudioPause, XBMCK_MEDIA_PLAY_PAUSE },
+ { XKB_KEY_XF86Mail, XBMCK_LAUNCH_MAIL },
+ { XKB_KEY_XF86Select, XBMCK_LAUNCH_MEDIA_SELECT },
+ { XKB_KEY_XF86Launch0, XBMCK_LAUNCH_APP1 },
+ { XKB_KEY_XF86Launch1, XBMCK_LAUNCH_APP2 },
+ { XKB_KEY_XF86WWW, XBMCK_LAUNCH_FILE_BROWSER },
+ { XKB_KEY_XF86AudioMedia, XBMCK_LAUNCH_MEDIA_CENTER },
+ { XKB_KEY_XF86AudioRewind, XBMCK_MEDIA_REWIND },
+ { XKB_KEY_XF86AudioForward, XBMCK_MEDIA_FASTFORWARD },
+
+ // Numeric keypad
+ { XKB_KEY_KP_0, XBMCK_KP0 },
+ { XKB_KEY_KP_1, XBMCK_KP1 },
+ { XKB_KEY_KP_2, XBMCK_KP2 },
+ { XKB_KEY_KP_3, XBMCK_KP3 },
+ { XKB_KEY_KP_4, XBMCK_KP4 },
+ { XKB_KEY_KP_5, XBMCK_KP5 },
+ { XKB_KEY_KP_6, XBMCK_KP6 },
+ { XKB_KEY_KP_7, XBMCK_KP7 },
+ { XKB_KEY_KP_8, XBMCK_KP8 },
+ { XKB_KEY_KP_9, XBMCK_KP9 },
+ { XKB_KEY_KP_Decimal, XBMCK_KP_PERIOD },
+ { XKB_KEY_KP_Divide, XBMCK_KP_DIVIDE },
+ { XKB_KEY_KP_Multiply, XBMCK_KP_MULTIPLY },
+ { XKB_KEY_KP_Subtract, XBMCK_KP_MINUS },
+ { XKB_KEY_KP_Add, XBMCK_KP_PLUS },
+ { XKB_KEY_KP_Enter, XBMCK_KP_ENTER },
+ { XKB_KEY_KP_Equal, XBMCK_KP_EQUALS },
+
+ // Arrows + Home/End pad
+ { XKB_KEY_Up, XBMCK_UP },
+ { XKB_KEY_Down, XBMCK_DOWN },
+ { XKB_KEY_Right, XBMCK_RIGHT },
+ { XKB_KEY_Left, XBMCK_LEFT },
+ { XKB_KEY_Insert, XBMCK_INSERT },
+ { XKB_KEY_Home, XBMCK_HOME },
+ { XKB_KEY_End, XBMCK_END },
+ { XKB_KEY_Page_Up, XBMCK_PAGEUP },
+ { XKB_KEY_Page_Down, XBMCK_PAGEDOWN },
+
+ // Function keys
+ { XKB_KEY_F1, XBMCK_F1 },
+ { XKB_KEY_F2, XBMCK_F2 },
+ { XKB_KEY_F3, XBMCK_F3 },
+ { XKB_KEY_F4, XBMCK_F4 },
+ { XKB_KEY_F5, XBMCK_F5 },
+ { XKB_KEY_F6, XBMCK_F6 },
+ { XKB_KEY_F7, XBMCK_F7 },
+ { XKB_KEY_F8, XBMCK_F8 },
+ { XKB_KEY_F9, XBMCK_F9 },
+ { XKB_KEY_F10, XBMCK_F10 },
+ { XKB_KEY_F11, XBMCK_F11 },
+ { XKB_KEY_F12, XBMCK_F12 },
+ { XKB_KEY_F13, XBMCK_F13 },
+ { XKB_KEY_F14, XBMCK_F14 },
+ { XKB_KEY_F15, XBMCK_F15 },
+
+ // Key state modifier keys
+ { XKB_KEY_Num_Lock, XBMCK_NUMLOCK },
+ { XKB_KEY_Caps_Lock, XBMCK_CAPSLOCK },
+ { XKB_KEY_Scroll_Lock, XBMCK_SCROLLOCK },
+ { XKB_KEY_Shift_R, XBMCK_RSHIFT },
+ { XKB_KEY_Shift_L, XBMCK_LSHIFT },
+ { XKB_KEY_Control_R, XBMCK_RCTRL },
+ { XKB_KEY_Control_L, XBMCK_LCTRL },
+ { XKB_KEY_Alt_R, XBMCK_RALT },
+ { XKB_KEY_Alt_L, XBMCK_LALT },
+ { XKB_KEY_Meta_R, XBMCK_RMETA },
+ { XKB_KEY_Meta_L, XBMCK_LMETA },
+ { XKB_KEY_Super_R, XBMCK_RSUPER },
+ { XKB_KEY_Super_L, XBMCK_LSUPER },
+ // XKB does not have XBMCK_MODE/"Alt Gr" - probably equal to XKB_KEY_Alt_R
+ { XKB_KEY_Multi_key, XBMCK_COMPOSE },
+
+ // Miscellaneous function keys
+ { XKB_KEY_Help, XBMCK_HELP },
+ { XKB_KEY_Print, XBMCK_PRINT },
+ // Unmapped: XBMCK_SYSREQ
+ { XKB_KEY_Break, XBMCK_BREAK },
+ { XKB_KEY_Menu, XBMCK_MENU },
+ { XKB_KEY_XF86PowerOff, XBMCK_POWER },
+ { XKB_KEY_EcuSign, XBMCK_EURO },
+ { XKB_KEY_Undo, XBMCK_UNDO },
+ { XKB_KEY_XF86Sleep, XBMCK_SLEEP },
+ // Unmapped: XBMCK_GUIDE, XBMCK_SETTINGS, XBMCK_INFO
+ { XKB_KEY_XF86Red, XBMCK_RED },
+ { XKB_KEY_XF86Green, XBMCK_GREEN },
+ { XKB_KEY_XF86Yellow, XBMCK_YELLOW },
+ { XKB_KEY_XF86Blue, XBMCK_BLUE },
+ // Unmapped: XBMCK_ZOOM, XBMCK_TEXT
+ { XKB_KEY_XF86Favorites, XBMCK_FAVORITES },
+ { XKB_KEY_XF86HomePage, XBMCK_HOMEPAGE },
+ // Unmapped: XBMCK_CONFIG, XBMCK_EPG
+
+ // Media keys
+ { XKB_KEY_XF86Eject, XBMCK_EJECT },
+ // XBMCK_STOP clashes with XBMCK_MEDIA_STOP
+ { XKB_KEY_XF86AudioRecord, XBMCK_RECORD },
+ // XBMCK_REWIND clashes with XBMCK_MEDIA_REWIND
+ { XKB_KEY_XF86Phone, XBMCK_PHONE },
+ { XKB_KEY_XF86AudioPlay, XBMCK_PLAY },
+ { XKB_KEY_XF86AudioRandomPlay, XBMCK_SHUFFLE }
+ // XBMCK_FASTFORWARD clashes with XBMCK_MEDIA_FASTFORWARD
+};
+
+}
+
+CXkbcommonContext::CXkbcommonContext(xkb_context_flags flags)
+: m_context{xkb_context_new(flags), XkbContextDeleter()}
+{
+ if (!m_context)
+ {
+ throw std::runtime_error("Failed to create xkb context");
+ }
+}
+
+void CXkbcommonContext::XkbContextDeleter::operator()(xkb_context* ctx) const
+{
+ xkb_context_unref(ctx);
+}
+
+std::unique_ptr<CXkbcommonKeymap> CXkbcommonContext::KeymapFromString(std::string const& keymap)
+{
+ std::unique_ptr<xkb_keymap, CXkbcommonKeymap::XkbKeymapDeleter> xkbKeymap{xkb_keymap_new_from_string(m_context.get(), keymap.c_str(), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS), CXkbcommonKeymap::XkbKeymapDeleter()};
+
+ if (!xkbKeymap)
+ {
+ throw std::runtime_error("Failed to compile keymap");
+ }
+
+ return std::unique_ptr<CXkbcommonKeymap>{new CXkbcommonKeymap(std::move(xkbKeymap))};
+}
+
+std::unique_ptr<CXkbcommonKeymap> CXkbcommonContext::KeymapFromNames(const std::string& rules, const std::string& model, const std::string& layout, const std::string& variant, const std::string& options)
+{
+ xkb_rule_names names = {
+ rules.c_str(),
+ model.c_str(),
+ layout.c_str(),
+ variant.c_str(),
+ options.c_str()
+ };
+
+ std::unique_ptr<xkb_keymap, CXkbcommonKeymap::XkbKeymapDeleter> keymap{xkb_keymap_new_from_names(m_context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS), CXkbcommonKeymap::XkbKeymapDeleter()};
+
+ if (!keymap)
+ {
+ throw std::runtime_error("Failed to compile keymap");
+ }
+
+ return std::unique_ptr<CXkbcommonKeymap>{new CXkbcommonKeymap(std::move(keymap))};
+}
+
+std::unique_ptr<xkb_state, CXkbcommonKeymap::XkbStateDeleter> CXkbcommonKeymap::CreateXkbStateFromKeymap(xkb_keymap* keymap)
+{
+ std::unique_ptr<xkb_state, XkbStateDeleter> state{xkb_state_new(keymap), XkbStateDeleter()};
+
+ if (!state)
+ {
+ throw std::runtime_error("Failed to create keyboard state");
+ }
+
+ return state;
+}
+
+CXkbcommonKeymap::CXkbcommonKeymap(std::unique_ptr<xkb_keymap, XkbKeymapDeleter> keymap)
+: m_keymap{std::move(keymap)}, m_state{CreateXkbStateFromKeymap(m_keymap.get())}
+{
+ // Lookup modifier indices and create new map - this is more efficient
+ // than looking the modifiers up by name each time
+ for (auto const& nameMapping : ModifierNameXBMCMappings)
+ {
+ xkb_mod_index_t index = xkb_keymap_mod_get_index(m_keymap.get(), nameMapping.name);
+ if (index != XKB_MOD_INVALID)
+ {
+ m_modifierMappings.emplace_back(index, nameMapping.xbmc);
+ }
+ }
+}
+
+void CXkbcommonKeymap::XkbStateDeleter::operator()(xkb_state* state) const
+{
+ xkb_state_unref(state);
+}
+
+void CXkbcommonKeymap::XkbKeymapDeleter::operator()(xkb_keymap* keymap) const
+{
+ xkb_keymap_unref(keymap);
+}
+
+xkb_keysym_t CXkbcommonKeymap::KeysymForKeycode(xkb_keycode_t code) const
+{
+ return xkb_state_key_get_one_sym(m_state.get(), code);
+}
+
+xkb_mod_mask_t CXkbcommonKeymap::CurrentModifiers() const
+{
+ return xkb_state_serialize_mods(m_state.get(), XKB_STATE_MODS_EFFECTIVE);
+}
+
+void CXkbcommonKeymap::UpdateMask(xkb_mod_mask_t depressed, xkb_mod_mask_t latched, xkb_mod_mask_t locked, xkb_mod_mask_t group)
+{
+ xkb_state_update_mask(m_state.get(), depressed, latched, locked, 0, 0, group);
+}
+
+XBMCMod CXkbcommonKeymap::ActiveXBMCModifiers() const
+{
+ xkb_mod_mask_t mask(CurrentModifiers());
+ XBMCMod xbmcModifiers = XBMCKMOD_NONE;
+
+ for (auto const& mapping : m_modifierMappings)
+ {
+ if (mask & (1 << mapping.xkb))
+ {
+ xbmcModifiers = static_cast<XBMCMod> (xbmcModifiers | mapping.xbmc);
+ }
+ }
+
+ return xbmcModifiers;
+}
+
+XBMCKey CXkbcommonKeymap::XBMCKeyForKeysym(xkb_keysym_t sym)
+{
+ if (sym >= 'A' && sym <= 'Z')
+ {
+ // Uppercase ASCII characters must be lowercased as XBMCKey is modifier-invariant
+ return static_cast<XBMCKey> (sym + 'a' - 'A');
+ }
+ else if (sym >= 0x20 /* ASCII space */ && sym <= 0x7E /* ASCII tilde */)
+ {
+ // Rest of ASCII printable character range is code-compatible
+ return static_cast<XBMCKey> (sym);
+ }
+
+ // Try mapping
+ auto mapping = XkbKeycodeXBMCMappings.find(sym);
+ if (mapping != XkbKeycodeXBMCMappings.end())
+ {
+ return mapping->second;
+ }
+ else
+ {
+ return XBMCK_UNKNOWN;
+ }
+}
+
+XBMCKey CXkbcommonKeymap::XBMCKeyForKeycode(xkb_keycode_t code) const
+{
+ return XBMCKeyForKeysym(KeysymForKeycode(code));
+}
+
+std::uint32_t CXkbcommonKeymap::UnicodeCodepointForKeycode(xkb_keycode_t code) const
+{
+ return xkb_state_key_get_utf32(m_state.get(), code);
+}
+
+bool CXkbcommonKeymap::ShouldKeycodeRepeat(xkb_keycode_t code) const
+{
+ return xkb_keymap_key_repeats(m_keymap.get(), code);
+}
diff --git a/xbmc/windowing/wayland/XkbcommonKeymap.h b/xbmc/windowing/wayland/XkbcommonKeymap.h
new file mode 100644
index 0000000..7718b02
--- /dev/null
+++ b/xbmc/windowing/wayland/XkbcommonKeymap.h
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2017-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "input/XBMC_keysym.h"
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include <xkbcommon/xkbcommon.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * A wrapper class around an xkbcommon keymap and state tracker.
+ *
+ * This class knows about some common modifier combinations and keeps
+ * track of the currently pressed keys and modifiers. It also has
+ * some utility functions to transform hardware keycodes into
+ * a common representation.
+ *
+ * Since this class is keeping track of all the pressed and depressed
+ * modifiers, IT MUST ALWAYS BE KEPT UP TO DATE WITH ANY NEWLY
+ * PRESSED MODIFIERS. Undefined behaviour will result if it is not
+ * kept up to date.
+ *
+ * Instances can be easily created from keymap strings with \ref CXkbcommonContext
+ */
+class CXkbcommonKeymap
+{
+public:
+ struct XkbKeymapDeleter
+ {
+ void operator()(xkb_keymap* keymap) const;
+ };
+
+ /**
+ * Construct for known xkb_keymap
+ */
+ explicit CXkbcommonKeymap(std::unique_ptr<xkb_keymap, XkbKeymapDeleter> keymap);
+
+ /**
+ * Get xkb keysym for keycode - only a single keysym is supported
+ */
+ xkb_keysym_t KeysymForKeycode(xkb_keycode_t code) const;
+ /**
+ * Updates the currently depressed, latched, locked and group
+ * modifiers for a keyboard being tracked.
+ *
+ * This function must be called whenever modifiers change, or the state will
+ * be wrong and keysym translation will be off.
+ */
+ void UpdateMask(xkb_mod_mask_t depressed,
+ xkb_mod_mask_t latched,
+ xkb_mod_mask_t locked,
+ xkb_mod_mask_t group);
+ /**
+ * Gets the currently depressed, latched and locked modifiers
+ * for the keyboard
+ */
+ xkb_mod_mask_t CurrentModifiers() const;
+ /**
+ * Get XBMCKey for provided keycode
+ */
+ XBMCKey XBMCKeyForKeycode(xkb_keycode_t code) const;
+ /**
+ * \ref CurrentModifiers with XBMC flags
+ */
+ XBMCMod ActiveXBMCModifiers() const;
+ /**
+ * Get Unicode codepoint/UTF32 code for provided keycode
+ */
+ std::uint32_t UnicodeCodepointForKeycode(xkb_keycode_t code) const;
+ /**
+ * Check whether a given keycode should have key repeat
+ */
+ bool ShouldKeycodeRepeat(xkb_keycode_t code) const;
+
+ static XBMCKey XBMCKeyForKeysym(xkb_keysym_t sym);
+
+private:
+ struct XkbStateDeleter
+ {
+ void operator()(xkb_state* state) const;
+ };
+ static std::unique_ptr<xkb_state, XkbStateDeleter> CreateXkbStateFromKeymap(xkb_keymap* keymap);
+
+ std::unique_ptr<xkb_keymap, XkbKeymapDeleter> m_keymap;
+ std::unique_ptr<xkb_state, XkbStateDeleter> m_state;
+
+ struct ModifierMapping
+ {
+ xkb_mod_index_t xkb;
+ XBMCMod xbmc;
+ ModifierMapping(xkb_mod_index_t xkb, XBMCMod xbmc)
+ : xkb{xkb}, xbmc{xbmc}
+ {}
+ };
+ std::vector<ModifierMapping> m_modifierMappings;
+};
+
+class CXkbcommonContext
+{
+public:
+ explicit CXkbcommonContext(xkb_context_flags flags = XKB_CONTEXT_NO_FLAGS);
+
+ /**
+ * Opens a shared memory region and parses the data in it to an
+ * xkbcommon keymap.
+ *
+ * This function does not own the file descriptor. It must not be closed
+ * from this function.
+ */
+ std::unique_ptr<CXkbcommonKeymap> KeymapFromString(std::string const& keymap);
+ std::unique_ptr<CXkbcommonKeymap> KeymapFromNames(const std::string &rules, const std::string &model, const std::string &layout, const std::string &variant, const std::string &options);
+
+private:
+ struct XkbContextDeleter
+ {
+ void operator()(xkb_context* ctx) const;
+ };
+ std::unique_ptr<xkb_context, XkbContextDeleter> m_context;
+};
+
+
+}
+}
+}