diff --git a/xbmc/guilib/GraphicContext.cpp b/xbmc/guilib/GraphicContext.cpp index 6065bb4bc31ba..09b4d2322b2e1 100644 --- a/xbmc/guilib/GraphicContext.cpp +++ b/xbmc/guilib/GraphicContext.cpp @@ -415,20 +415,37 @@ void CGraphicContext::SetVideoResolutionInternal(RESOLUTION res, bool forceUpdat RESOLUTION_INFO info_mod = GetResInfo(res); - m_iScreenWidth = info_mod.iWidth; + // FIXME Wayland windowing needs some way to "deny" resolution updates since what Kodi + // requests might not get actually set by the compositor. + // So in theory, m_iScreenWidth etc. would not need to be updated at all before the + // change is confirmed. + // But other windowing code expects these variables to be already set when + // SetFullScreen() is called, so set them anyway and remember the old values. + int origScreenWidth = m_iScreenWidth, origScreenHeight = m_iScreenHeight, origScreenId = m_iScreenId; + float origFPSOverride = m_fFPSOverride; + + m_iScreenWidth = info_mod.iWidth; m_iScreenHeight = info_mod.iHeight; - m_iScreenId = info_mod.iScreen; - m_scissors.SetRect(0, 0, (float)m_iScreenWidth, (float)m_iScreenHeight); - m_Resolution = res; - m_fFPSOverride = 0 ; + m_iScreenId = info_mod.iScreen; + m_Resolution = res; + m_fFPSOverride = 0; + bool switched = true; if (g_advancedSettings.m_fullScreen) { #if defined (TARGET_DARWIN) || defined (TARGET_WINDOWS) bool blankOtherDisplays = CServiceBroker::GetSettings().GetBool(CSettings::SETTING_VIDEOSCREEN_BLANKDISPLAYS); g_Windowing.SetFullScreen(true, info_org, blankOtherDisplays); #else - g_Windowing.SetFullScreen(true, info_org, false); + switched = g_Windowing.SetFullScreen(true, info_org, false); + // FIXME At the moment only Wayland expects the return value to be interpreted + // - all other windowing implementations might still assume that it does + // not matter what they return as it was before. + // This needs to get fixed when the resolution switching code is refactored. + if (g_Windowing.GetWinSystem() != WINDOW_SYSTEM_WAYLAND) + { + switched = true; + } #endif } else if (lastRes >= RES_DESKTOP ) @@ -436,12 +453,38 @@ void CGraphicContext::SetVideoResolutionInternal(RESOLUTION res, bool forceUpdat else g_Windowing.ResizeWindow(info_org.iWidth, info_org.iHeight, -1, -1); - // make sure all stereo stuff are correctly setup - SetStereoView(RENDER_STEREO_VIEW_OFF); + if (switched) + { + m_scissors.SetRect(0, 0, (float)m_iScreenWidth, (float)m_iScreenHeight); - // update anyone that relies on sizing information - CInputManager::GetInstance().SetMouseResolution(info_org.iWidth, info_org.iHeight, 1, 1); - g_windowManager.SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_WINDOW_RESIZE); + // make sure all stereo stuff are correctly setup + SetStereoView(RENDER_STEREO_VIEW_OFF); + + // update anyone that relies on sizing information + CInputManager::GetInstance().SetMouseResolution(info_org.iWidth, info_org.iHeight, 1, 1); + g_windowManager.SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_WINDOW_RESIZE); + } + else + { + // Reset old state + m_iScreenWidth = origScreenWidth; + m_iScreenHeight = origScreenHeight; + m_iScreenId = origScreenId; + m_fFPSOverride = origFPSOverride; + if (IsValidResolution(lastRes)) + { + m_Resolution = lastRes; + } + else + { + // FIXME Resolution has become invalid + // This happens e.g. when switching monitors and the new monitor has fewer + // resolutions than the old one. Fall back to RES_DESKTOP and hope that + // the real resolution is set soon. + // Again, must be fixed as part of a greater refactor. + m_Resolution = RES_DESKTOP; + } + } Unlock(); } diff --git a/xbmc/settings/DisplaySettings.cpp b/xbmc/settings/DisplaySettings.cpp index 2e5b92f0289ac..05df9853fd82c 100644 --- a/xbmc/settings/DisplaySettings.cpp +++ b/xbmc/settings/DisplaySettings.cpp @@ -288,14 +288,30 @@ bool CDisplaySettings::OnSettingChanging(std::shared_ptr setting SetCurrentResolution(newRes, false); g_graphicsContext.SetVideoResolution(newRes); + if (m_resolutionChangeInProgress) + { + // Do not recurse into showing dialogs + return true; + } + // check if the old or the new resolution was/is windowed // in which case we don't show any prompt to the user if (oldRes != RES_WINDOW && newRes != RES_WINDOW && oldRes != newRes) { if (!m_resolutionChangeAborted) { - if (HELPERS::ShowYesNoDialogText(CVariant{13110}, CVariant{13111}, CVariant{""}, CVariant{""}, 15000) != - DialogResponse::YES) + m_resolutionChangeInProgress = true; +#if defined(HAVE_WAYLAND) + // If ReloadSkin() is called when a dialog is shown, it will get aborted, + // so reload the skin later + g_Windowing.SetInhibitSkinReload(true); +#endif + auto response = HELPERS::ShowYesNoDialogText(CVariant{13110}, CVariant{13111}, CVariant{""}, CVariant{""}, 15000); +#if defined(HAVE_WAYLAND) + g_Windowing.SetInhibitSkinReload(false); +#endif + m_resolutionChangeInProgress = false; + if (response != DialogResponse::YES) { m_resolutionChangeAborted = true; return false; @@ -313,10 +329,26 @@ bool CDisplaySettings::OnSettingChanging(std::shared_ptr setting SetCurrentResolution(newRes, false); g_graphicsContext.SetVideoResolution(newRes, true); + if (m_resolutionChangeInProgress) + { + // Do not recurse into showing dialogs + return true; + } + if (!m_resolutionChangeAborted) { - if (HELPERS::ShowYesNoDialogText(CVariant{13110}, CVariant{13111}, CVariant{""}, CVariant{""}, 10000) != - DialogResponse::YES) + m_resolutionChangeInProgress = true; +#if defined(HAVE_WAYLAND) + // If ReloadSkin() is called when a dialog is shown, it will get aborted, + // so reload the skin later + g_Windowing.SetInhibitSkinReload(true); +#endif + auto response = HELPERS::ShowYesNoDialogText(CVariant{13110}, CVariant{13111}, CVariant{""}, CVariant{""}, 10000); +#if defined(HAVE_WAYLAND) + g_Windowing.SetInhibitSkinReload(false); +#endif + m_resolutionChangeInProgress = false; + if (response != DialogResponse::YES) { m_resolutionChangeAborted = true; return false; diff --git a/xbmc/settings/DisplaySettings.h b/xbmc/settings/DisplaySettings.h index 186dd70bdf2f4..8d3c0810f9eb0 100644 --- a/xbmc/settings/DisplaySettings.h +++ b/xbmc/settings/DisplaySettings.h @@ -131,6 +131,7 @@ class CDisplaySettings : public ISettingCallback, public ISubSettings, float m_verticalShift; // current vertical shift bool m_nonLinearStretched; // current non-linear stretch + bool m_resolutionChangeInProgress = false; bool m_resolutionChangeAborted; CCriticalSection m_critical; }; diff --git a/xbmc/utils/MathUtils.h b/xbmc/utils/MathUtils.h index 556787ef8263f..519e56cbf6bd3 100644 --- a/xbmc/utils/MathUtils.h +++ b/xbmc/utils/MathUtils.h @@ -189,6 +189,20 @@ namespace MathUtils MathUtils::abs(0); } + /** + * Compare two floating-point numbers for equality and regard them + * as equal if their difference is below a given threshold. + * + * It is usually not useful to compare float numbers for equality with + * the standard operator== since very close numbers might have different + * representations. + */ + template + inline bool FloatEquals(FloatT f1, FloatT f2, FloatT maxDelta) + { + return (std::abs(f2 - f1) < maxDelta); + } + #if 0 /*! \brief test routine for round_int and truncate_int Must return true on all platforms. diff --git a/xbmc/windowing/wayland/Connection.cpp b/xbmc/windowing/wayland/Connection.cpp index db536cd61ffe2..5da0dfe389ece 100644 --- a/xbmc/windowing/wayland/Connection.cpp +++ b/xbmc/windowing/wayland/Connection.cpp @@ -62,10 +62,7 @@ CConnection::CConnection(IConnectionHandler* handler) HandleRegistry(); CLog::Log(LOGDEBUG, "Wayland connection: Waiting for global interfaces"); - if (m_display->roundtrip() < 0) - { - throw std::runtime_error("Wayland roundtrip failed"); - } + m_display->roundtrip(); CLog::Log(LOGDEBUG, "Wayland connection: Initial roundtrip complete"); CheckRequiredGlobals(); diff --git a/xbmc/windowing/wayland/Output.cpp b/xbmc/windowing/wayland/Output.cpp index bcdc1e53c9e0b..f0d58c55a9a26 100644 --- a/xbmc/windowing/wayland/Output.cpp +++ b/xbmc/windowing/wayland/Output.cpp @@ -32,6 +32,7 @@ COutput::COutput(std::uint32_t globalName, wayland::output_t const & output, std m_output.on_geometry() = [this](std::int32_t x, std::int32_t y, std::int32_t physWidth, std::int32_t physHeight, wayland::output_subpixel subpixel, std::string const& make, std::string const& model, wayland::output_transform transform) { + CSingleLock lock(m_geometryCriticalSection); m_x = x; m_y = y; m_physicalWidth = physWidth; @@ -45,6 +46,7 @@ COutput::COutput(std::uint32_t globalName, wayland::output_t const & output, std // element and boolean information whether the element was actually added // which we do not need auto modeIterator = m_modes.emplace(width, height, refresh).first; + CSingleLock 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) @@ -67,8 +69,39 @@ COutput::COutput(std::uint32_t globalName, wayland::output_t const & output, std }; } +COutput::~COutput() +{ + // 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 +{ + CSingleLock 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 +{ + CSingleLock 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 { + CSingleLock lock(m_geometryCriticalSection); if (m_physicalWidth == 0 || m_physicalHeight == 0 || mode.width == 0 || mode.height == 0) { return 1.0f; diff --git a/xbmc/windowing/wayland/Output.h b/xbmc/windowing/wayland/Output.h index 369a0d87cf895..aaaba3f17a741 100644 --- a/xbmc/windowing/wayland/Output.h +++ b/xbmc/windowing/wayland/Output.h @@ -19,6 +19,7 @@ */ #pragma once +#include #include #include #include @@ -26,6 +27,9 @@ #include +#include "threads/CriticalSection.h" +#include "threads/SingleLock.h" + namespace KODI { namespace WINDOWING @@ -41,7 +45,7 @@ class COutput { public: COutput(std::uint32_t globalName, wayland::output_t const & output, std::function doneHandler); - COutput(COutput&& other) = default; + ~COutput(); wayland::output_t const& GetWaylandOutput() const { @@ -57,6 +61,7 @@ class COutput */ std::tuple GetPosition() const { + CSingleLock lock(m_geometryCriticalSection); return std::make_tuple(m_x, m_y); } /** @@ -65,14 +70,17 @@ class COutput */ std::tuple GetPhysicalSize() const { + CSingleLock lock(m_geometryCriticalSection); return std::make_tuple(m_physicalWidth, m_physicalHeight); } std::string const& GetMake() const { + CSingleLock lock(m_geometryCriticalSection); return m_make; } std::string const& GetModel() const { + CSingleLock lock(m_geometryCriticalSection); return m_model; } std::int32_t GetScale() const @@ -113,22 +121,8 @@ class COutput { return m_modes; } - Mode const& GetCurrentMode() const - { - if (m_currentMode == m_modes.end()) - { - throw std::runtime_error("Current mode not set"); - } - return *m_currentMode; - } - Mode const& GetPreferredMode() const - { - if (m_preferredMode == m_modes.end()) - { - throw std::runtime_error("Preferred mode not set"); - } - return *m_preferredMode; - } + Mode const& GetCurrentMode() const; + Mode const& GetPreferredMode() const; float GetPixelRatioForMode(Mode const& mode) const; @@ -139,12 +133,15 @@ class COutput std::uint32_t m_globalName; wayland::output_t m_output; std::function m_doneHandler; - + + CCriticalSection m_geometryCriticalSection; + CCriticalSection m_iteratorCriticalSection; + std::int32_t m_x = 0, m_y = 0; std::int32_t m_physicalWidth = 0, m_physicalHeight = 0; std::string m_make, m_model; - std::int32_t m_scale = 1; // default scale of 1 if no wl_output::scale is sent - + std::atomic m_scale = {1}; // default scale of 1 if no wl_output::scale is sent + std::set m_modes; // For std::set, insertion never invalidates existing iterators, and modes are // never removed, so the usage of iterators is safe diff --git a/xbmc/windowing/wayland/SeatInputProcessor.cpp b/xbmc/windowing/wayland/SeatInputProcessor.cpp index 07b504ecdfcb6..09b43624b8ae4 100644 --- a/xbmc/windowing/wayland/SeatInputProcessor.cpp +++ b/xbmc/windowing/wayland/SeatInputProcessor.cpp @@ -106,7 +106,7 @@ constexpr int WL_KEYBOARD_XKB_CODE_OFFSET = 8; } CSeatInputProcessor::CSeatInputProcessor(std::uint32_t globalName, const wayland::seat_t& seat, IInputHandler* handler) -: m_globalName(globalName), m_seat(seat), m_handler(handler), m_keyRepeatTimer(&m_keyRepeatCallback), m_keyRepeatCallback(this) +: m_globalName(globalName), m_seat(seat), m_handler(handler), m_keyRepeatCallback(this), m_keyRepeatTimer(&m_keyRepeatCallback) { assert(m_handler); @@ -204,8 +204,8 @@ void CSeatInputProcessor::SendMouseMotion() event.type = XBMC_MOUSEMOTION; event.motion = { - .x = m_pointerX, - .y = m_pointerY + .x = static_cast (m_pointerX * m_coordinateScale), + .y = static_cast (m_pointerY * m_coordinateScale) }; m_handler->OnEvent(m_globalName, InputType::POINTER, event); } @@ -217,8 +217,8 @@ void CSeatInputProcessor::SendMouseButton(unsigned char button, bool pressed) event.button = { .button = button, - .x = m_pointerX, - .y = m_pointerY + .x = static_cast (m_pointerX * m_coordinateScale), + .y = static_cast (m_pointerY * m_coordinateScale) }; m_handler->OnEvent(m_globalName, InputType::POINTER, event); } diff --git a/xbmc/windowing/wayland/SeatInputProcessor.h b/xbmc/windowing/wayland/SeatInputProcessor.h index 5c8e90628de5a..ad13c242df93a 100644 --- a/xbmc/windowing/wayland/SeatInputProcessor.h +++ b/xbmc/windowing/wayland/SeatInputProcessor.h @@ -115,6 +115,10 @@ class CSeatInputProcessor { return m_touch; } + void SetCoordinateScale(std::int32_t scale) + { + m_coordinateScale = scale; + } private: CSeatInputProcessor(CSeatInputProcessor const& other) = delete; @@ -141,6 +145,7 @@ class CSeatInputProcessor wayland::keyboard_t m_keyboard; wayland::touch_t m_touch; + std::int32_t m_coordinateScale = 1; std::uint16_t m_pointerX = 0; std::uint16_t m_pointerY = 0; @@ -152,7 +157,6 @@ class CSeatInputProcessor // 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; class CKeyRepeatCallback : public ITimerCallback { @@ -162,8 +166,9 @@ class CSeatInputProcessor void OnTimeout() override; }; CKeyRepeatCallback m_keyRepeatCallback; + CTimer m_keyRepeatTimer; }; } } -} \ No newline at end of file +} diff --git a/xbmc/windowing/wayland/ShellSurface.cpp b/xbmc/windowing/wayland/ShellSurface.cpp index 97e98b24ad933..b1f7ddf20e93a 100644 --- a/xbmc/windowing/wayland/ShellSurface.cpp +++ b/xbmc/windowing/wayland/ShellSurface.cpp @@ -27,10 +27,10 @@ IShellSurface::ConfigureHandler& IShellSurface::OnConfigure() return m_onConfigure; } -void IShellSurface::InvokeOnConfigure(std::int32_t width, std::int32_t height) +void IShellSurface::InvokeOnConfigure(std::uint32_t serial, std::int32_t width, std::int32_t height) { if (m_onConfigure) { - m_onConfigure(width, height); + m_onConfigure(serial, width, height); } } diff --git a/xbmc/windowing/wayland/ShellSurface.h b/xbmc/windowing/wayland/ShellSurface.h index 58c70fa78122d..56521191ae197 100644 --- a/xbmc/windowing/wayland/ShellSurface.h +++ b/xbmc/windowing/wayland/ShellSurface.h @@ -38,7 +38,7 @@ namespace WAYLAND class IShellSurface { protected: - void InvokeOnConfigure(std::int32_t width, std::int32_t height); + void InvokeOnConfigure(std::uint32_t serial, std::int32_t width, std::int32_t height); public: /** @@ -63,12 +63,13 @@ class IShellSurface */ virtual void Initialize() = 0; - using ConfigureHandler = std::function; + using ConfigureHandler = std::function; virtual void SetFullScreen(wayland::output_t const& output, float refreshRate) = 0; virtual void SetWindowed() = 0; ConfigureHandler& OnConfigure(); + virtual void AckConfigure(std::uint32_t serial) = 0; private: ConfigureHandler m_onConfigure; diff --git a/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp b/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp index 71d7bfba0f720..d5bd483bc6b01 100644 --- a/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp +++ b/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp @@ -36,10 +36,15 @@ CShellSurfaceWlShell::CShellSurfaceWlShell(const wayland::shell_t& shell, const }; m_shellSurface.on_configure() = [this](wayland::shell_surface_resize, std::int32_t width, std::int32_t height) { - InvokeOnConfigure(width, height); + // wl_shell does not have serials + InvokeOnConfigure(0, width, height); }; } +void CShellSurfaceWlShell::AckConfigure(std::uint32_t) +{ +} + void CShellSurfaceWlShell::Initialize() { // Nothing to do here - constructor already handles it diff --git a/xbmc/windowing/wayland/ShellSurfaceWlShell.h b/xbmc/windowing/wayland/ShellSurfaceWlShell.h index 96fdd268eb5fe..cb5e7d013d642 100644 --- a/xbmc/windowing/wayland/ShellSurfaceWlShell.h +++ b/xbmc/windowing/wayland/ShellSurfaceWlShell.h @@ -53,6 +53,7 @@ class CShellSurfaceWlShell : public IShellSurface void SetFullScreen(wayland::output_t const& output, float refreshRate) override; void SetWindowed() override; + void AckConfigure(std::uint32_t serial) override; }; } diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp index d216a28d745cd..b73aca6ca1fd0 100644 --- a/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp +++ b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp @@ -36,9 +36,14 @@ CShellSurfaceXdgShellUnstableV6::CShellSurfaceXdgShellUnstableV6(wayland::displa // surface to be if (m_configuredWidth != 0 && m_configuredHeight != 0) { - InvokeOnConfigure(m_configuredWidth, m_configuredHeight); + InvokeOnConfigure(serial, m_configuredWidth, m_configuredHeight); + } + else + { + // WinSystem does not get the configure notification, so ack must be sent + // here + AckConfigure(serial); } - m_xdgSurface.ack_configure(serial); }; m_xdgToplevel.on_close() = [this]() { @@ -62,6 +67,11 @@ void CShellSurfaceXdgShellUnstableV6::Initialize() m_display->roundtrip(); } +void CShellSurfaceXdgShellUnstableV6::AckConfigure(std::uint32_t serial) +{ + m_xdgSurface.ack_configure(serial); +} + CShellSurfaceXdgShellUnstableV6::~CShellSurfaceXdgShellUnstableV6() { // xdg_shell is picky: must destroy toplevel role before surface diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h index 4cd0108913b6a..42b54b4ccefac 100644 --- a/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h +++ b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h @@ -64,6 +64,7 @@ class CShellSurfaceXdgShellUnstableV6 : public IShellSurface void SetFullScreen(wayland::output_t const& output, float refreshRate) override; void SetWindowed() override; + void AckConfigure(std::uint32_t serial) override; }; } diff --git a/xbmc/windowing/wayland/WinEventsWayland.cpp b/xbmc/windowing/wayland/WinEventsWayland.cpp index f9e6fc3816aa2..88e5da8144396 100644 --- a/xbmc/windowing/wayland/WinEventsWayland.cpp +++ b/xbmc/windowing/wayland/WinEventsWayland.cpp @@ -127,10 +127,7 @@ class CWinEventsWaylandThread : CThread // Read events and release intent; this does not block readIntent.read(); // Dispatch default event queue - if (m_display->dispatch_pending() < 0) - { - throw std::system_error(errno, std::generic_category(), "Error dispatching Wayland events"); - } + m_display->dispatch_pending(); } CLog::Log(LOGDEBUG, "Wayland message pump stopped"); diff --git a/xbmc/windowing/wayland/WinSystemWayland.cpp b/xbmc/windowing/wayland/WinSystemWayland.cpp index 64448eaa76426..fd0b7194a7feb 100644 --- a/xbmc/windowing/wayland/WinSystemWayland.cpp +++ b/xbmc/windowing/wayland/WinSystemWayland.cpp @@ -42,10 +42,48 @@ #include "utils/log.h" #include "utils/StringUtils.h" #include "WinEventsWayland.h" +#include "utils/MathUtils.h" using namespace KODI::WINDOWING::WAYLAND; using namespace std::placeholders; +namespace +{ + +RESOLUTION FindMatchingCustomResolution(int width, int height, float refreshRate) +{ + CSingleLock lock(g_graphicsContext); + for (size_t res = RES_DESKTOP; res < CDisplaySettings::GetInstance().ResolutionInfoSize(); ++res) + { + auto const& resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(res); + if (resInfo.iWidth == width && resInfo.iHeight == height && MathUtils::FloatEquals(resInfo.fRefreshRate, refreshRate, 0.0005f)) + { + return static_cast (res); + } + } + return RES_INVALID; +} + +struct OutputScaleComparer +{ + bool operator()(std::shared_ptr const& output1, std::shared_ptr const& output2) + { + return output1->GetScale() < output2->GetScale(); + } +}; + +struct OutputCurrentRefreshRateComparer +{ + bool operator()(std::shared_ptr const& output1, std::shared_ptr const& output2) + { + return output1->GetCurrentMode().refreshMilliHz < output2->GetCurrentMode().refreshMilliHz; + } +}; + +const std::string CONFIGURE_RES_ID = "configure"; + +} + CWinSystemWayland::CWinSystemWayland() : CWinSystemBase() { @@ -70,19 +108,12 @@ bool CWinSystemWayland::InitWindowSystem() CLog::Log(LOGWARNING, "Wayland compositor did not announce a wl_seat - you will not have any input devices for the time being"); } // Do another roundtrip to get initial wl_output information - int tries = 0; - while (m_outputs.empty()) + m_connection->GetDisplay().roundtrip(); + if (m_outputs.empty()) { - if (tries++ > 5) - { - throw std::runtime_error("No outputs received from compositor"); - } - if (m_connection->GetDisplay().roundtrip() < 0) - { - throw std::runtime_error("Wayland roundtrip failed"); - } + 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 @@ -105,7 +136,9 @@ bool CWinSystemWayland::DestroyWindowSystem() m_cursorImage = wayland::cursor_image_t(); m_cursorTheme = wayland::cursor_theme_t(); m_seatProcessors.clear(); + m_outputsInPreparation.clear(); m_outputs.clear(); + m_surfaceOutputs.clear(); m_connection.reset(); return CWinSystemBase::DestroyWindowSystem(); @@ -116,11 +149,36 @@ bool CWinSystemWayland::CreateNewWindow(const std::string& name, RESOLUTION_INFO& res) { m_surface = m_connection->GetCompositor().create_surface(); - - // Try to get this resolution if compositor does not say otherwise - m_nWidth = res.iWidth; - m_nHeight = res.iHeight; - + m_surface.on_enter() = [this](wayland::output_t wloutput) + { + if (auto output = FindOutputByWaylandOutput(wloutput)) + { + CLog::Log(LOGDEBUG, "Entering output \"%s\" with scale %d", UserFriendlyOutputName(output).c_str(), output->GetScale()); + m_surfaceOutputs.emplace(output); + UpdateBufferScale(); + } + else + { + CLog::Log(LOGWARNING, "Entering output that was not configured yet, ignoring"); + } + }; + m_surface.on_leave() = [this](wayland::output_t wloutput) + { + if (auto output = FindOutputByWaylandOutput(wloutput)) + { + CLog::Log(LOGDEBUG, "Leaving output \"%s\" with scale %d", UserFriendlyOutputName(output).c_str(), output->GetScale()); + m_surfaceOutputs.erase(output); + UpdateBufferScale(); + } + else + { + CLog::Log(LOGWARNING, "Leaving output that was not configured yet, ignoring"); + } + }; + + // Try with this resolution if compositor does not say otherwise + SetSizeFromSurfaceSize(res.iWidth, res.iHeight); + auto xdgShell = m_connection->GetXdgShellUnstableV6(); if (xdgShell) { @@ -128,51 +186,42 @@ bool CWinSystemWayland::CreateNewWindow(const std::string& name, } else { - CLog::Log(LOGWARNING, "Compositor does not support xdg_shell unstable v6 protocol - falling back to wl_shell, not all features might work"); - m_shellSurface.reset(new CShellSurfaceWlShell(m_connection->GetShell(), m_surface, name, "kodi")); + CLog::LogF(LOGWARNING, "Compositor does not support xdg_shell unstable v6 protocol - falling back to wl_shell, not all features might work"); + m_shellSurface.reset(new CShellSurfaceWlShell(m_connection->GetShell(), m_surface, name, "kodi")); } - + // Just remember initial width/height for context creation // This is used for sizing the EGLSurface - m_shellSurface->OnConfigure() = [this](std::int32_t width, std::int32_t height) + m_shellSurface->OnConfigure() = [this](std::uint32_t serial, std::int32_t width, std::int32_t height) { CLog::Log(LOGINFO, "Got initial Wayland surface size %dx%d", width, height); - m_nWidth = width; - m_nHeight = height; + SetSizeFromSurfaceSize(width, height); + AckConfigure(serial); }; - + if (fullScreen) { - // Try to start on correct monitor - COutput* output = FindOutputByUserFriendlyName(CServiceBroker::GetSettings().GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + // Try to start on correct monitor and with correct buffer scale + auto output = FindOutputByUserFriendlyName(CServiceBroker::GetSettings().GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); if (output) { m_shellSurface->SetFullScreen(output->GetWaylandOutput(), res.fRefreshRate); + if (m_surface.can_set_buffer_scale()) + { + m_scale = output->GetScale(); + ApplyBufferScale(m_scale); + } } } - + m_shellSurface->Initialize(); - + + // Update resolution with real size as it could have changed due to configure() + UpdateDesktopResolution(res, 0, m_nWidth, m_nHeight, res.fRefreshRate); + // Set real handler during runtime - m_shellSurface->OnConfigure() = std::bind(&CWinSystemWayland::HandleSurfaceConfigure, this, _1, _2); - - if (m_nWidth == 0 || m_nHeight == 0) - { - // Adopt size from resolution if compositor did not specify anything - m_nWidth = res.iWidth; - m_nHeight = res.iHeight; - } - else - { - // Update resolution with real size - res.iWidth = m_nWidth; - res.iHeight = m_nHeight; - res.iScreenWidth = m_nWidth; - res.iScreenHeight = m_nHeight; - res.iSubtitles = (int) (0.965 * m_nHeight); - g_graphicsContext.ResetOverscan(res); - } - + m_shellSurface->OnConfigure() = std::bind(&CWinSystemWayland::HandleSurfaceConfigure, this, _1, _2, _3); + // Now start processing events // // There are two stages to the event handling: @@ -194,7 +243,7 @@ bool CWinSystemWayland::CreateNewWindow(const std::string& name, // wl_output and wl_seat and thus to most if not all runtime object creation // cases we have to support. CWinEventsWayland::SetDisplay(&m_connection->GetDisplay()); - + return true; } @@ -249,14 +298,14 @@ void CWinSystemWayland::UpdateResolutions() std::string userOutput = CServiceBroker::GetSettings().GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR); CSingleLock lock(m_outputsMutex); - + if (m_outputs.empty()) { // *Usually* this should not happen - just give up return; } - - COutput* output = FindOutputByUserFriendlyName(userOutput); + + auto output = FindOutputByUserFriendlyName(userOutput); if (!output) { // Fallback to current output @@ -265,30 +314,26 @@ void CWinSystemWayland::UpdateResolutions() if (!output) { // Well just use the first one - output = &m_outputs.begin()->second; + output = m_outputs.begin()->second; } - - std::string outputName = UserFriendlyOutputName(*output); + + std::string outputName = UserFriendlyOutputName(output); auto const& modes = output->GetModes(); - // TODO wait until output has all information auto const& currentMode = output->GetCurrentMode(); auto physicalSize = output->GetPhysicalSize(); - CLog::Log(LOGINFO, "User wanted output \"%s\", we now have \"%s\" size %dx%d mm with %zu mode(s):", userOutput.c_str(), outputName.c_str(), std::get<0>(physicalSize), std::get<1>(physicalSize), modes.size()); + CLog::LogF(LOGINFO, "User wanted output \"%s\", we now have \"%s\" size %dx%d mm with %zu mode(s):", userOutput.c_str(), outputName.c_str(), std::get<0>(physicalSize), std::get<1>(physicalSize), modes.size()); for (auto const& mode : modes) { bool isCurrent = (mode == currentMode); float pixelRatio = output->GetPixelRatioForMode(mode); - CLog::Log(LOGINFO, "- %dx%d @%.3f Hz pixel ratio %.3f%s", mode.width, mode.height, mode.refreshMilliHz / 1000.0f, pixelRatio, isCurrent ? " current" : ""); + CLog::LogF(LOGINFO, "- %dx%d @%.3f Hz pixel ratio %.3f%s", mode.width, mode.height, mode.refreshMilliHz / 1000.0f, pixelRatio, isCurrent ? " current" : ""); - RESOLUTION_INFO res(mode.width, mode.height); - res.bFullScreen = true; - res.iScreen = 0; // not used + RESOLUTION_INFO res; + UpdateDesktopResolution(res, 0, mode.width, mode.height, mode.refreshMilliHz / 1000.0f); res.strOutput = outputName; res.fPixelRatio = pixelRatio; - res.fRefreshRate = mode.refreshMilliHz / 1000.0f; - g_graphicsContext.ResetOverscan(res); if (isCurrent) { @@ -309,7 +354,7 @@ bool CWinSystemWayland::ResizeWindow(int newWidth, int newHeight, int newLeft, i return false; } -COutput* CWinSystemWayland::FindOutputByUserFriendlyName(const std::string& name) +std::shared_ptr CWinSystemWayland::FindOutputByUserFriendlyName(const std::string& name) { CSingleLock lock(m_outputsMutex); auto outputIt = std::find_if(m_outputs.begin(), m_outputs.end(), @@ -318,65 +363,168 @@ COutput* CWinSystemWayland::FindOutputByUserFriendlyName(const std::string& name return (name == UserFriendlyOutputName(entry.second)); }); - return (outputIt == m_outputs.end() ? nullptr : &outputIt->second); + return (outputIt == m_outputs.end() ? nullptr : outputIt->second); +} + +std::shared_ptr CWinSystemWayland::FindOutputByWaylandOutput(wayland::output_t const& output) +{ + CSingleLock lock(m_outputsMutex); + auto outputIt = std::find_if(m_outputs.begin(), m_outputs.end(), + [this, &output](decltype(m_outputs)::value_type const& entry) + { + return (output == entry.second->GetWaylandOutput()); + }); + + return (outputIt == m_outputs.end() ? nullptr : outputIt->second); } bool CWinSystemWayland::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) { - CSingleLock lock(m_configurationMutex); - - if (m_currentOutput == res.strOutput && m_nWidth == res.iWidth && m_nHeight == res.iHeight && m_fRefreshRate == res.fRefreshRate && m_bFullScreen == fullScreen) + // FIXME Our configuration is protected by graphicsContext lock + // If we'd use a mutex private to this class, we would have to lock both + // that one and graphicsContext (because the resolutions get updated), + // leading to a possible deadlock. + CSingleLock lock(g_graphicsContext); + + CLog::LogF(LOGINFO, "Wayland asked to switch mode to %dx%d @%.3f Hz on output \"%s\"", res.iWidth, res.iHeight, res.fRefreshRate, res.strOutput.c_str()); + + // 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. + + m_bFullScreen = fullScreen; + + bool wasConfigure = (res.strId == CONFIGURE_RES_ID); + // Reset configure flag + // Setting it in res will not modify the global information in CDisplaySettings + // and we don't know which resolution index this is, so just reset all + for (size_t resIdx = RES_DESKTOP; resIdx < CDisplaySettings::GetInstance().ResolutionInfoSize(); resIdx++) { - // Nothing to do - return true; + CDisplaySettings::GetInstance().GetResolutionInfo(resIdx).strId = ""; } - CLog::Log(LOGINFO, "Wayland trying to switch mode to %dx%d @%.3f Hz on output \"%s\"", res.iWidth, res.iHeight, res.fRefreshRate, res.strOutput.c_str()); - - // Try to match output - wayland::output_t output; + if (fullScreen) { - CSingleLock lock(m_outputsMutex); - - COutput* outputHandler = FindOutputByUserFriendlyName(res.strOutput); - if (outputHandler) + if (!wasConfigure || m_currentOutput != res.strOutput) { - output = outputHandler->GetWaylandOutput(); - CLog::Log(LOGDEBUG, "Resolved output \"%s\" to bound Wayland global %u", res.strOutput.c_str(), outputHandler->GetGlobalName()); + // There is -no- guarantee that the compositor will put the surface on this + // screen, but pretend that it does so we have any information at all + m_currentOutput = res.strOutput; + + // Try to match output + auto output = FindOutputByUserFriendlyName(res.strOutput); + if (output) + { + CLog::LogF(LOGDEBUG, "Resolved output \"%s\" to bound Wayland global %u", res.strOutput.c_str(), output->GetGlobalName()); + } + else + { + CLog::LogF(LOGINFO, "Could not match output \"%s\" to a currently available Wayland output, falling back to default output", res.strOutput.c_str()); + } + + CLog::LogF(LOGDEBUG, "Setting full-screen with refresh rate %.3f", res.fRefreshRate); + m_shellSurface->SetFullScreen(output ? output->GetWaylandOutput() : wayland::output_t(), res.fRefreshRate); } else { - CLog::Log(LOGINFO, "Could not match output \"%s\" to a currently available Wayland output, falling back to default output", res.strOutput.c_str()); + // Switch done, do not SetFullScreen() again - otherwise we would + // get an endless repetition of setting full screen and configure events + CLog::LogF(LOGDEBUG, "Called in response to surface configure, not calling set_fullscreen on surface"); } - // Release lock only when output has been assigned to local variable so it - // cannot go away + } + else + { + // Shouldn't happen since we claim not to support windowed modes + CLog::LogF(LOGWARNING, "Wayland windowing system asked to switch to windowed mode which is not really supported"); + m_shellSurface->SetWindowed(); } - m_nWidth = res.iWidth; - m_nHeight = res.iHeight; - m_bFullScreen = fullScreen; - // This is just a guess since the compositor is free to ignore our frame rate - // request - m_fRefreshRate = res.fRefreshRate; - // There is -no- guarantee that the compositor will put the surface on this - // screen, but pretend that it does so we have any information at all - m_currentOutput = res.strOutput; + if (wasConfigure) + { + // Mark everything opaque so the compositor can render it faster + // Do it here so size always matches the configured egl surface + CLog::LogF(LOGDEBUG, "Setting opaque region size %dx%d", m_surfaceWidth, m_surfaceHeight); + wayland::region_t opaqueRegion = m_connection->GetCompositor().create_region(); + opaqueRegion.add(0, 0, m_surfaceWidth, m_surfaceHeight); + m_surface.set_opaque_region(opaqueRegion); + if (m_surface.can_set_buffer_scale()) + { + // Buffer scale must also match egl size configuration + ApplyBufferScale(m_scale); + } - if (fullScreen) + // FIXME This assumes that the resolution has already been set. Should + // be moved to some post-change callback when resolution setting is refactored. + if (!m_inhibitSkinReload) + { + g_application.ReloadSkin(); + } + + // Next buffer that the graphic context attaches will have the size corresponding + // to this configure, so go and ack it + AckConfigure(m_currentConfigureSerial); + } + + bool wasInitialSetFullScreen = m_isInitialSetFullScreen; + m_isInitialSetFullScreen = false; + + // Need to return true + // * when this SetFullScreen() call was initiated by a configure() event + // * on first SetFullScreen so GraphicsContext gets resolution + // Otherwise, Kodi must keep the old resolution. + return wasConfigure || wasInitialSetFullScreen; +} + + +void CWinSystemWayland::SetInhibitSkinReload(bool inhibit) +{ + m_inhibitSkinReload = inhibit; + if (!inhibit) { - m_shellSurface->SetFullScreen(output, m_fRefreshRate); + g_application.ReloadSkin(); } - else +} +void CWinSystemWayland::HandleSurfaceConfigure(std::uint32_t serial, std::int32_t width, std::int32_t height) +{ + CSingleLock lock(g_graphicsContext); + CLog::LogF(LOGDEBUG, "Configure serial %u: size %dx%d", serial, width, height); + m_currentConfigureSerial = serial; + if (!ResetSurfaceSize(width, height, m_scale)) { - // Shouldn't happen since we claim not to support windowed modes - CLog::Log(LOGWARNING, "Wayland windowing system asked to switch to windowed mode which is not really supported"); - m_shellSurface->SetWindowed(); + // nothing changed, ack immediately + AckConfigure(serial); } + // configure is acked when the Kodi surface has actually been reconfigured +} - return true; +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 %u", serial); + m_shellSurface->AckConfigure(serial); + m_lastAckedSerial = serial; + m_firstSerialAcked = true; + } } -void CWinSystemWayland::HandleSurfaceConfigure(std::int32_t width, std::int32_t height) +/** + * Set the internal surface size variables and perform resolution change + * + * Call only from Wayland event processing thread! + * + * \return Whether surface parameters changed and video resolution change was + * performed + */ +bool CWinSystemWayland::ResetSurfaceSize(std::int32_t width, std::int32_t height, std::int32_t scale) { // 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 @@ -384,55 +532,107 @@ void CWinSystemWayland::HandleSurfaceConfigure(std::int32_t width, std::int32_t // 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. - - CLog::Log(LOGINFO, "Got Wayland surface size %dx%d", width, height); + + RESOLUTION switchToRes = RES_INVALID; + + // FIXME See comment in SetFullScreen + CSingleLock lock(g_graphicsContext); + + // Now update actual resolution with configured one + bool scaleChanged = (scale != m_scale); + m_scale = scale; + bool sizeChanged = SetSizeFromSurfaceSize(width, height); + + // Get actual frame rate from monitor, take highest frame rate if multiple + // m_surfaceOutputs is only updated from event handling thread, so no lock + auto maxRefreshIt = std::max_element(m_surfaceOutputs.cbegin(), m_surfaceOutputs.cend(), OutputCurrentRefreshRateComparer()); + float refreshRate = m_fRefreshRate; + if (maxRefreshIt != m_surfaceOutputs.cend()) { - CSingleLock lock(m_configurationMutex); + refreshRate = (*maxRefreshIt)->GetCurrentMode().refreshMilliHz / 1000.0f; + CLog::LogF(LOGDEBUG, "Resolved actual (maximum) refresh rate to %.3f Hz on output \"%s\"", refreshRate, UserFriendlyOutputName(*maxRefreshIt).c_str()); + } - // Mark everything opaque so the compositor can render it faster - wayland::region_t opaqueRegion = m_connection->GetCompositor().create_region(); - opaqueRegion.add(0, 0, width, height); - m_surface.set_opaque_region(opaqueRegion); - // No surface commit, EGL context will do that when it changes the buffer + if (refreshRate == m_fRefreshRate && !scaleChanged && !sizeChanged) + { + CLog::LogF(LOGDEBUG, "No change in size, refresh rate, and scale, returning"); + return false; + } - if (m_nWidth == width && m_nHeight == height) - { - // Nothing to do - return; - } + m_fRefreshRate = refreshRate; + + // Find matching Kodi resolution member + switchToRes = FindMatchingCustomResolution(m_nWidth, m_nHeight, m_fRefreshRate); - m_nWidth = width; - m_nHeight = height; - // Update desktop resolution - auto& res = CDisplaySettings::GetInstance().GetCurrentResolutionInfo(); - res.iWidth = width; - res.iHeight = height; - res.iScreenWidth = width; - res.iScreenHeight = height; - res.iSubtitles = (int) (0.965 * height); - g_graphicsContext.ResetOverscan(res); + if (switchToRes == RES_INVALID) + { + // Add new resolution if none found + RESOLUTION_INFO newResInfo; + UpdateDesktopResolution(newResInfo, 0, m_nWidth, m_nHeight, m_fRefreshRate); + newResInfo.strOutput = m_currentOutput; // we just assume the compositor put us on the right output + CDisplaySettings::GetInstance().AddResolutionInfo(newResInfo); CDisplaySettings::GetInstance().ApplyCalibrations(); + switchToRes = static_cast (CDisplaySettings::GetInstance().ResolutionInfoSize() - 1); } - + + // RES_DESKTOP does not change usually, it is still the current resolution + // of the selected output + + assert(switchToRes != RES_INVALID); + + // Mark resolution so that we know it came from configure + CDisplaySettings::GetInstance().GetResolutionInfo(switchToRes).strId = CONFIGURE_RES_ID; + + CSingleExit exit(g_graphicsContext); + // Force resolution update // SetVideoResolution() automatically delegates to main thread via internal // message if called from other threads // This will call SetFullScreen() with the new resolution, which also updates - // the size of the egl_window etc. + // the size of the egl_window etc. from m_nWidth/m_nHeight. // The call always blocks, so the configuration lock must be released beforehand. - g_graphicsContext.SetVideoResolution(g_graphicsContext.GetVideoResolution(), true); + // FIXME Ideally this class would be completely decoupled from g_graphicsContext, + // but this is not possible at the moment before the refactoring is done. + g_graphicsContext.SetVideoResolution(switchToRes, true); + + return true; } -std::string CWinSystemWayland::UserFriendlyOutputName(const COutput& output) +/** + * Calculate internal resolution from surface size and set variables + * + * \return whether any size variable changed + */ +bool CWinSystemWayland::SetSizeFromSurfaceSize(std::int32_t surfaceWidth, std::int32_t surfaceHeight) +{ + std::int32_t newWidth = surfaceWidth * m_scale; + std::int32_t newHeight = surfaceHeight * m_scale; + + if (surfaceWidth != m_surfaceWidth || surfaceHeight != m_surfaceHeight || newWidth != m_nWidth || newHeight != m_nHeight) + { + m_surfaceWidth = surfaceWidth; + m_surfaceHeight = surfaceHeight; + m_nWidth = newWidth; + m_nHeight = newHeight; + CLog::LogF(LOGINFO, "Set surface size %dx%d at scale %d -> resolution %dx%d", m_surfaceWidth, m_surfaceHeight, m_scale, m_nWidth, m_nHeight); + return true; + } + else + { + return false; + } +} + +std::string CWinSystemWayland::UserFriendlyOutputName(std::shared_ptr const& output) { std::vector parts; - if (!output.GetMake().empty()) + if (!output->GetMake().empty()) { - parts.emplace_back(output.GetMake()); + parts.emplace_back(output->GetMake()); } - if (!output.GetModel().empty()) + if (!output->GetModel().empty()) { - parts.emplace_back(output.GetModel()); + parts.emplace_back(output->GetModel()); } if (parts.empty()) { @@ -442,7 +642,7 @@ std::string CWinSystemWayland::UserFriendlyOutputName(const COutput& output) // Add position std::int32_t x, y; - std::tie(x, y) = output.GetPosition(); + std::tie(x, y) = output->GetPosition(); if (x != 0 || y != 0) { parts.emplace_back(StringUtils::Format("@{}x{}", x, y)); @@ -524,29 +724,44 @@ void CWinSystemWayland::Unregister(IDispResource* resource) void CWinSystemWayland::OnSeatAdded(std::uint32_t name, wayland::seat_t& seat) { CSingleLock lock(m_seatProcessorsMutex); - m_seatProcessors.emplace(std::piecewise_construct, std::forward_as_tuple(name), std::forward_as_tuple(name, seat, this)); + auto newSeatEmplace = m_seatProcessors.emplace(std::piecewise_construct, std::forward_as_tuple(name), std::forward_as_tuple(name, seat, this)); + newSeatEmplace.first->second.SetCoordinateScale(m_scale); } void CWinSystemWayland::OnOutputAdded(std::uint32_t name, wayland::output_t& output) { // This is not accessed from multiple threads - m_outputsInPreparation.emplace(std::piecewise_construct, - std::forward_as_tuple(name), - std::forward_as_tuple(name, output, std::bind(&CWinSystemWayland::OnOutputDone, this, name))); + m_outputsInPreparation.emplace(name, new 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()) + if (it != m_outputsInPreparation.end()) { - return; + // This output was added for the first time - done is also sent when + // output parameters change later + + { + CSingleLock lock(m_outputsMutex); + // Move from m_outputsInPreparation to m_outputs + m_outputs.emplace(std::move(*it)); + m_outputsInPreparation.erase(it); + } + + // Maybe the output that was added was the one we should be on? + if (m_bFullScreen) + { + CSingleLock lock(g_graphicsContext); + UpdateResolutions(); + // This will call SetFullScreen(), which will match the output against + // the information from the resolution and call set_fullscreen on the + // surface if it changed. + g_graphicsContext.SetVideoResolution(g_graphicsContext.GetVideoResolution(), true); + } } - - CSingleLock lock(m_outputsMutex); - // Move from m_outputsInPreparation to m_outputs - m_outputs.emplace(std::move(*it)); - m_outputsInPreparation.erase(it); + + UpdateBufferScale(); } void CWinSystemWayland::OnGlobalRemoved(std::uint32_t name) @@ -560,7 +775,9 @@ void CWinSystemWayland::OnGlobalRemoved(std::uint32_t name) CSingleLock lock(m_outputsMutex); if (m_outputs.erase(name) != 0) { - // TODO Handle: Update resolution etc. + // 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 } } } @@ -622,6 +839,36 @@ void CWinSystemWayland::OnSetCursor(wayland::pointer_t& pointer, std::uint32_t s } } +void CWinSystemWayland::UpdateBufferScale() +{ + if (!m_surface || !m_surface.can_set_buffer_scale()) + { + // Never modify scale when we cannot set it + return; + } + + // 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()) + { + auto const newScale = (*maxBufferScaleIt)->GetScale(); + // Recalculate resolution with new scale if it changed + ResetSurfaceSize(m_surfaceWidth, m_surfaceHeight, newScale); + } +} + +void CWinSystemWayland::ApplyBufferScale(std::int32_t scale) +{ + CLog::LogF(LOGINFO, "Setting Wayland buffer scale to %d", scale); + m_surface.set_buffer_scale(scale); + CSingleLock lock(m_seatProcessorsMutex); + for (auto& seatProcessor : m_seatProcessors) + { + seatProcessor.second.SetCoordinateScale(scale); + } +} + #if defined(HAVE_LIBVA) void* CWinSystemWayland::GetVaDisplay() { diff --git a/xbmc/windowing/wayland/WinSystemWayland.h b/xbmc/windowing/wayland/WinSystemWayland.h index 69bef3f6ef713..2803f567b826a 100644 --- a/xbmc/windowing/wayland/WinSystemWayland.h +++ b/xbmc/windowing/wayland/WinSystemWayland.h @@ -69,6 +69,8 @@ class CWinSystemWayland : public CWinSystemBase, public IInputHandler, public IC bool HasCursor() override; void ShowOSMouse(bool show) override; + + void SetInhibitSkinReload(bool inhibit); void* GetVaDisplay(); @@ -92,17 +94,21 @@ class CWinSystemWayland : public CWinSystemBase, public IInputHandler, public IC protected: void LoadDefaultCursor(); void SendFocusChange(bool focus); - virtual void HandleSurfaceConfigure(std::int32_t width, std::int32_t height); + void HandleSurfaceConfigure(std::uint32_t serial, std::int32_t width, std::int32_t height); + bool ResetSurfaceSize(std::int32_t width, std::int32_t height, std::int32_t scale); + bool SetSizeFromSurfaceSize(std::int32_t surfaceWidth, std::int32_t surfaceHeight); - std::string UserFriendlyOutputName(COutput const& output); - COutput* FindOutputByUserFriendlyName(std::string const& name); + std::string UserFriendlyOutputName(std::shared_ptr const& output); + std::shared_ptr FindOutputByUserFriendlyName(std::string const& name); + std::shared_ptr 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); - - // Mutex for protecting modifications of m_nWidth, m_nHeight etc. - CCriticalSection m_configurationMutex; + void UpdateBufferScale(); + void ApplyBufferScale(std::int32_t scale); + + void AckConfigure(std::uint32_t serial); std::unique_ptr m_connection; wayland::surface_t m_surface; @@ -111,7 +117,7 @@ class CWinSystemWayland : public CWinSystemBase, public IInputHandler, public IC std::map m_seatProcessors; CCriticalSection m_seatProcessorsMutex; // m_outputsInPreparation did not receive their done event yet - std::map m_outputs, m_outputsInPreparation; + std::map> m_outputs, m_outputsInPreparation; CCriticalSection m_outputsMutex; bool m_osCursorVisible = true; @@ -122,8 +128,21 @@ class CWinSystemWayland : public CWinSystemBase, public IInputHandler, public IC std::set m_dispResources; CCriticalSection m_dispResourcesMutex; - + + bool m_inhibitSkinReload = false; + std::string m_currentOutput; + // Set of outputs that show some part of our main surface as indicated by + // compositor + std::set> m_surfaceOutputs; + // Size of our surface in "surface coordinates", i.e. without scaling applied + std::int32_t m_surfaceWidth, m_surfaceHeight; + std::int32_t m_scale = 1; + std::uint32_t m_currentConfigureSerial = 0; + bool m_firstSerialAcked = false; + std::uint32_t m_lastAckedSerial = 0; + // Whether this is the first call to SetFullScreen + bool m_isInitialSetFullScreen = true; }; diff --git a/xbmc/windowing/wayland/WinSystemWaylandGLContext.cpp b/xbmc/windowing/wayland/WinSystemWaylandGLContext.cpp index d69c7efd801fe..1e141dc0a9913 100644 --- a/xbmc/windowing/wayland/WinSystemWaylandGLContext.cpp +++ b/xbmc/windowing/wayland/WinSystemWaylandGLContext.cpp @@ -22,6 +22,7 @@ #include "Connection.h" #include "utils/log.h" +#include "guilib/GraphicContext.h" using namespace KODI::WINDOWING::WAYLAND; @@ -56,7 +57,7 @@ bool CWinSystemWaylandGLContext::CreateNewWindow(const std::string& name, return false; } - return SetFullScreen(fullScreen, res, false); + return true; } bool CWinSystemWaylandGLContext::DestroyWindow() @@ -75,25 +76,35 @@ bool CWinSystemWaylandGLContext::DestroyWindowSystem() bool CWinSystemWaylandGLContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) { - auto width = res.iWidth; - auto height = res.iHeight; - - int currWidth, currHeight; - m_glContext.GetAttachedSize(currWidth, currHeight); + // FIXME See CWinSystemWayland::SetFullScreen() + CSingleLock lock(g_graphicsContext); - if (width != currWidth || height != currHeight) - { - m_glContext.Resize(width, height); - } - if (!CWinSystemWayland::SetFullScreen(fullScreen, res, blankOtherDisplays)) { return false; } + + // Look only at m_nWidth and m_nHeight which represent the actual wl_surface + // size instead of res.iWidth and res.iHeight, which are only a "wish" - if (!CRenderSystemGL::ResetRenderSystem(width, height, fullScreen, res.fRefreshRate)) + int currWidth, currHeight; + m_glContext.GetAttachedSize(currWidth, currHeight); + + // Change EGL surface size if necessary + if (currWidth != m_nWidth || currHeight != m_nHeight) { - return false; + CLog::LogF(LOGDEBUG, "Updating egl_window size to %dx%d", m_nWidth, m_nHeight); + m_glContext.Resize(m_nWidth, m_nHeight); + } + + // Propagate changed dimensions to render system if necessary + if (m_nWidth != CRenderSystemGL::m_width || m_nHeight != CRenderSystemGL::m_height) + { + CLog::LogF(LOGDEBUG, "Resetting render system to %dx%d", m_nWidth, m_nHeight); + if (!CRenderSystemGL::ResetRenderSystem(m_nWidth, m_nHeight, fullScreen, res.fRefreshRate)) + { + return false; + } } return true;