diff options
Diffstat (limited to 'xbmc/windowing/wayland')
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; +}; + + +} +} +} |