diff options
Diffstat (limited to '')
207 files changed, 34773 insertions, 0 deletions
diff --git a/xbmc/windowing/CMakeLists.txt b/xbmc/windowing/CMakeLists.txt new file mode 100644 index 0000000..21c7611 --- /dev/null +++ b/xbmc/windowing/CMakeLists.txt @@ -0,0 +1,16 @@ +set(SOURCES GraphicContext.cpp + OSScreenSaver.cpp + Resolution.cpp + WindowSystemFactory.cpp + WinSystem.cpp) + +set(HEADERS GraphicContext.h + OSScreenSaver.h + Resolution.h + WinEvents.h + WindowSystemFactory.h + WinSystem.h + XBMC_events.h + VideoSync.h) + +core_add_library(windowing) diff --git a/xbmc/windowing/GraphicContext.cpp b/xbmc/windowing/GraphicContext.cpp new file mode 100644 index 0000000..423148c --- /dev/null +++ b/xbmc/windowing/GraphicContext.cpp @@ -0,0 +1,1011 @@ +/* + * 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 "GraphicContext.h" + +#include "ServiceBroker.h" +#include "WinSystem.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/TextureManager.h" +#include "guilib/gui3d.h" +#include "input/InputManager.h" +#include "messaging/ApplicationMessenger.h" +#include "rendering/RenderSystem.h" +#include "settings/AdvancedSettings.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "utils/log.h" + +#include <cassert> +#include <mutex> + +CGraphicContext::CGraphicContext() = default; +CGraphicContext::~CGraphicContext() = default; + +void CGraphicContext::SetOrigin(float x, float y) +{ + if (!m_origins.empty()) + m_origins.push(CPoint(x,y) + m_origins.top()); + else + m_origins.push(CPoint(x,y)); + + AddTransform(TransformMatrix::CreateTranslation(x, y)); +} + +void CGraphicContext::RestoreOrigin() +{ + if (!m_origins.empty()) + m_origins.pop(); + RemoveTransform(); +} + +// add a new clip region, intersecting with the previous clip region. +bool CGraphicContext::SetClipRegion(float x, float y, float w, float h) +{ // transform from our origin + CPoint origin; + if (!m_origins.empty()) + origin = m_origins.top(); + + // ok, now intersect with our old clip region + CRect rect(x, y, x + w, y + h); + rect += origin; + if (!m_clipRegions.empty()) + { + // intersect with original clip region + rect.Intersect(m_clipRegions.top()); + } + + if (rect.IsEmpty()) + return false; + + m_clipRegions.push(rect); + + // here we could set the hardware clipping, if applicable + return true; +} + +void CGraphicContext::RestoreClipRegion() +{ + if (!m_clipRegions.empty()) + m_clipRegions.pop(); + + // here we could reset the hardware clipping, if applicable +} + +void CGraphicContext::ClipRect(CRect &vertex, CRect &texture, CRect *texture2) +{ + // this is the software clipping routine. If the graphics hardware is set to do the clipping + // (eg via SetClipPlane in D3D for instance) then this routine is unneeded. + if (!m_clipRegions.empty()) + { + // take a copy of the vertex rectangle and intersect + // it with our clip region (moved to the same coordinate system) + CRect clipRegion(m_clipRegions.top()); + if (!m_origins.empty()) + clipRegion -= m_origins.top(); + CRect original(vertex); + vertex.Intersect(clipRegion); + // and use the original to compute the texture coordinates + if (original != vertex) + { + float scaleX = texture.Width() / original.Width(); + float scaleY = texture.Height() / original.Height(); + texture.x1 += (vertex.x1 - original.x1) * scaleX; + texture.y1 += (vertex.y1 - original.y1) * scaleY; + texture.x2 += (vertex.x2 - original.x2) * scaleX; + texture.y2 += (vertex.y2 - original.y2) * scaleY; + if (texture2) + { + scaleX = texture2->Width() / original.Width(); + scaleY = texture2->Height() / original.Height(); + texture2->x1 += (vertex.x1 - original.x1) * scaleX; + texture2->y1 += (vertex.y1 - original.y1) * scaleY; + texture2->x2 += (vertex.x2 - original.x2) * scaleX; + texture2->y2 += (vertex.y2 - original.y2) * scaleY; + } + } + } +} + +CRect CGraphicContext::GetClipRegion() +{ + if (m_clipRegions.empty()) + return CRect(0, 0, m_iScreenWidth, m_iScreenHeight); + CRect clipRegion(m_clipRegions.top()); + if (!m_origins.empty()) + clipRegion -= m_origins.top(); + return clipRegion; +} + +void CGraphicContext::AddGUITransform() +{ + m_transforms.push(m_finalTransform); + m_finalTransform = m_guiTransform; +} + +TransformMatrix CGraphicContext::AddTransform(const TransformMatrix &matrix) +{ + m_transforms.push(m_finalTransform); + m_finalTransform.matrix *= matrix; + return m_finalTransform.matrix; +} + +void CGraphicContext::SetTransform(const TransformMatrix &matrix) +{ + m_transforms.push(m_finalTransform); + m_finalTransform.matrix = matrix; +} + +void CGraphicContext::SetTransform(const TransformMatrix &matrix, float scaleX, float scaleY) +{ + m_transforms.push(m_finalTransform); + m_finalTransform.matrix = matrix; + m_finalTransform.scaleX = scaleX; + m_finalTransform.scaleY = scaleY; +} + +void CGraphicContext::RemoveTransform() +{ + if (!m_transforms.empty()) + { + m_finalTransform = m_transforms.top(); + m_transforms.pop(); + } +} + +bool CGraphicContext::SetViewPort(float fx, float fy, float fwidth, float fheight, bool intersectPrevious /* = false */) +{ + // transform coordinates - we may have a rotation which changes the positioning of the + // minimal and maximal viewport extents. We currently go to the maximal extent. + float x[4], y[4]; + x[0] = x[3] = fx; + x[1] = x[2] = fx + fwidth; + y[0] = y[1] = fy; + y[2] = y[3] = fy + fheight; + float minX = (float)m_iScreenWidth; + float maxX = 0; + float minY = (float)m_iScreenHeight; + float maxY = 0; + for (int i = 0; i < 4; i++) + { + float z = 0; + ScaleFinalCoords(x[i], y[i], z); + if (x[i] < minX) minX = x[i]; + if (x[i] > maxX) maxX = x[i]; + if (y[i] < minY) minY = y[i]; + if (y[i] > maxY) maxY = y[i]; + } + + int newLeft = (int)(minX + 0.5f); + int newTop = (int)(minY + 0.5f); + int newRight = (int)(maxX + 0.5f); + int newBottom = (int)(maxY + 0.5f); + if (intersectPrevious) + { + CRect oldviewport = m_viewStack.top(); + // do the intersection + int oldLeft = (int)oldviewport.x1; + int oldTop = (int)oldviewport.y1; + int oldRight = (int)oldviewport.x2; + int oldBottom = (int)oldviewport.y2; + if (newLeft >= oldRight || newTop >= oldBottom || newRight <= oldLeft || newBottom <= oldTop) + { // empty intersection - return false to indicate no rendering should occur + return false; + } + // ok, they intersect, do the intersection + if (newLeft < oldLeft) newLeft = oldLeft; + if (newTop < oldTop) newTop = oldTop; + if (newRight > oldRight) newRight = oldRight; + if (newBottom > oldBottom) newBottom = oldBottom; + } + // check range against screen size + if (newRight <= 0 || newBottom <= 0 || + newTop >= m_iScreenHeight || newLeft >= m_iScreenWidth || + newLeft >= newRight || newTop >= newBottom) + { // no intersection with the screen + return false; + } + // intersection with the screen + if (newLeft < 0) newLeft = 0; + if (newTop < 0) newTop = 0; + if (newRight > m_iScreenWidth) newRight = m_iScreenWidth; + if (newBottom > m_iScreenHeight) newBottom = m_iScreenHeight; + + assert(newLeft < newRight); + assert(newTop < newBottom); + + CRect newviewport((float)newLeft, (float)newTop, (float)newRight, (float)newBottom); + + m_viewStack.push(newviewport); + + newviewport = StereoCorrection(newviewport); + CServiceBroker::GetRenderSystem()->SetViewPort(newviewport); + + + UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top()); + return true; +} + +void CGraphicContext::RestoreViewPort() +{ + if (m_viewStack.size() <= 1) return; + + m_viewStack.pop(); + CRect viewport = StereoCorrection(m_viewStack.top()); + CServiceBroker::GetRenderSystem()->SetViewPort(viewport); + + UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top()); +} + +CPoint CGraphicContext::StereoCorrection(const CPoint &point) const +{ + CPoint res(point); + + if(m_stereoMode == RENDER_STEREO_MODE_SPLIT_HORIZONTAL) + { + const RESOLUTION_INFO info = GetResInfo(); + + if(m_stereoView == RENDER_STEREO_VIEW_RIGHT) + res.y += info.iHeight + info.iBlanking; + } + if(m_stereoMode == RENDER_STEREO_MODE_SPLIT_VERTICAL) + { + const RESOLUTION_INFO info = GetResInfo(); + + if(m_stereoView == RENDER_STEREO_VIEW_RIGHT) + res.x += info.iWidth + info.iBlanking; + } + return res; +} + +CRect CGraphicContext::StereoCorrection(const CRect &rect) const +{ + CRect res(StereoCorrection(rect.P1()) + , StereoCorrection(rect.P2())); + return res; +} + +void CGraphicContext::SetScissors(const CRect &rect) +{ + m_scissors = rect; + m_scissors.Intersect(CRect(0,0,(float)m_iScreenWidth, (float)m_iScreenHeight)); + CServiceBroker::GetRenderSystem()->SetScissors(StereoCorrection(m_scissors)); +} + +const CRect &CGraphicContext::GetScissors() const +{ + return m_scissors; +} + +void CGraphicContext::ResetScissors() +{ + m_scissors.SetRect(0, 0, (float)m_iScreenWidth, (float)m_iScreenHeight); + CServiceBroker::GetRenderSystem()->SetScissors(StereoCorrection(m_scissors)); +} + +const CRect CGraphicContext::GetViewWindow() const +{ + if (m_bCalibrating || m_bFullScreenVideo) + { + CRect rect; + RESOLUTION_INFO info = GetResInfo(); + rect.x1 = (float)info.Overscan.left; + rect.y1 = (float)info.Overscan.top; + rect.x2 = (float)info.Overscan.right; + rect.y2 = (float)info.Overscan.bottom; + return rect; + } + return m_videoRect; +} + +void CGraphicContext::SetViewWindow(float left, float top, float right, float bottom) +{ + m_videoRect.x1 = ScaleFinalXCoord(left, top); + m_videoRect.y1 = ScaleFinalYCoord(left, top); + m_videoRect.x2 = ScaleFinalXCoord(right, bottom); + m_videoRect.y2 = ScaleFinalYCoord(right, bottom); +} + +void CGraphicContext::SetFullScreenVideo(bool bOnOff) +{ + std::unique_lock<CCriticalSection> lock(*this); + + m_bFullScreenVideo = bOnOff; + + if (m_bFullScreenRoot) + { + bool bTriggerUpdateRes = false; + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (m_bFullScreenVideo) + bTriggerUpdateRes = true; + else + { + bool allowDesktopRes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) == ADJUST_REFRESHRATE_ALWAYS; + if (!allowDesktopRes) + { + if (appPlayer->IsPlayingVideo()) + bTriggerUpdateRes = true; + } + } + + bool allowResolutionChangeOnStop = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) != ADJUST_REFRESHRATE_ON_START; + RESOLUTION targetResolutionOnStop = RES_DESKTOP; + if (bTriggerUpdateRes) + appPlayer->TriggerUpdateResolution(); + else if (CDisplaySettings::GetInstance().GetCurrentResolution() > RES_DESKTOP) + { + targetResolutionOnStop = CDisplaySettings::GetInstance().GetCurrentResolution(); + } + + if (allowResolutionChangeOnStop && !bTriggerUpdateRes) + { + SetVideoResolution(targetResolutionOnStop, false); + } + } + else + SetVideoResolution(RES_WINDOW, false); +} + +bool CGraphicContext::IsFullScreenVideo() const +{ + return m_bFullScreenVideo; +} + +bool CGraphicContext::IsCalibrating() const +{ + return m_bCalibrating; +} + +void CGraphicContext::SetCalibrating(bool bOnOff) +{ + m_bCalibrating = bOnOff; +} + +bool CGraphicContext::IsValidResolution(RESOLUTION res) +{ + if (res >= RES_WINDOW && (size_t) res < CDisplaySettings::GetInstance().ResolutionInfoSize()) + { + return true; + } + + return false; +} + +// call SetVideoResolutionInternal and ensure its done from mainthread +void CGraphicContext::SetVideoResolution(RESOLUTION res, bool forceUpdate) +{ + if (CServiceBroker::GetAppMessenger()->IsProcessThread()) + { + SetVideoResolutionInternal(res, forceUpdate); + } + else + { + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_SETVIDEORESOLUTION, res, forceUpdate ? 1 : 0); + } +} + +void CGraphicContext::SetVideoResolutionInternal(RESOLUTION res, bool forceUpdate) +{ + RESOLUTION lastRes = m_Resolution; + + // If the user asked us to guess, go with desktop + if (!IsValidResolution(res)) + { + res = RES_DESKTOP; + } + + // If we are switching to the same resolution and same window/full-screen, no need to do anything + if (!forceUpdate && res == lastRes && m_bFullScreenRoot == CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen) + { + return; + } + + if (res >= RES_DESKTOP) + { + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = true; + m_bFullScreenRoot = true; + } + else + { + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = false; + m_bFullScreenRoot = false; + } + + std::unique_lock<CCriticalSection> lock(*this); + + // 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; + int origScreenHeight = m_iScreenHeight; + float origFPSOverride = m_fFPSOverride; + + UpdateInternalStateWithResolution(res); + RESOLUTION_INFO info_org = CDisplaySettings::GetInstance().GetResolutionInfo(res); + + bool switched = false; + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen) + { +#if defined (TARGET_DARWIN) || defined (TARGET_WINDOWS) + bool blankOtherDisplays = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_BLANKDISPLAYS); + switched = CServiceBroker::GetWinSystem()->SetFullScreen(true, info_org, blankOtherDisplays); +#else + switched = CServiceBroker::GetWinSystem()->SetFullScreen(true, info_org, false); +#endif + } + else if (lastRes >= RES_DESKTOP ) + switched = CServiceBroker::GetWinSystem()->SetFullScreen(false, info_org, false); + else + switched = CServiceBroker::GetWinSystem()->ResizeWindow(info_org.iWidth, info_org.iHeight, -1, -1); + + if (switched) + { + m_scissors.SetRect(0, 0, (float)m_iScreenWidth, (float)m_iScreenHeight); + + // make sure all stereo stuff are correctly setup + SetStereoView(RENDER_STEREO_VIEW_OFF); + + // update anyone that relies on sizing information + CServiceBroker::GetInputManager().SetMouseResolution(info_org.iWidth, info_org.iHeight, 1, 1); + + CGUIComponent *gui = CServiceBroker::GetGUI(); + if (gui) + gui->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_WINDOW_RESIZE); + } + else + { + // Reset old state + m_iScreenWidth = origScreenWidth; + m_iScreenHeight = origScreenHeight; + 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; + } + } +} + +void CGraphicContext::ApplyVideoResolution(RESOLUTION res) +{ + if (!IsValidResolution(res)) + { + CLog::LogF(LOGWARNING, "Asked to apply invalid resolution {}, falling back to RES_DESKTOP", + res); + res = RES_DESKTOP; + } + + if (res >= RES_DESKTOP) + { + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = true; + m_bFullScreenRoot = true; + } + else + { + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = false; + m_bFullScreenRoot = false; + } + + std::unique_lock<CCriticalSection> lock(*this); + + UpdateInternalStateWithResolution(res); + + m_scissors.SetRect(0, 0, (float)m_iScreenWidth, (float)m_iScreenHeight); + + // make sure all stereo stuff are correctly setup + SetStereoView(RENDER_STEREO_VIEW_OFF); + + // update anyone that relies on sizing information + RESOLUTION_INFO info_org = CDisplaySettings::GetInstance().GetResolutionInfo(res); + CServiceBroker::GetInputManager().SetMouseResolution(info_org.iWidth, info_org.iHeight, 1, 1); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_WINDOW_RESIZE); +} + +void CGraphicContext::UpdateInternalStateWithResolution(RESOLUTION res) +{ + RESOLUTION_INFO info_mod = GetResInfo(res); + + m_iScreenWidth = info_mod.iWidth; + m_iScreenHeight = info_mod.iHeight; + m_Resolution = res; + m_fFPSOverride = 0; +} + +void CGraphicContext::ApplyModeChange(RESOLUTION res) +{ + ApplyVideoResolution(res); + CServiceBroker::GetWinSystem()->FinishModeChange(res); +} + +void CGraphicContext::ApplyWindowResize(int newWidth, int newHeight) +{ + CServiceBroker::GetWinSystem()->SetWindowResolution(newWidth, newHeight); + ApplyVideoResolution(RES_WINDOW); + CServiceBroker::GetWinSystem()->FinishWindowResize(newWidth, newHeight); +} + +RESOLUTION CGraphicContext::GetVideoResolution() const +{ + return m_Resolution; +} + +void CGraphicContext::ResetOverscan(RESOLUTION_INFO &res) +{ + res.Overscan.left = 0; + res.Overscan.top = 0; + res.Overscan.right = res.iWidth; + res.Overscan.bottom = res.iHeight; +} + +void CGraphicContext::ResetOverscan(RESOLUTION res, OVERSCAN &overscan) +{ + overscan.left = 0; + overscan.top = 0; + + RESOLUTION_INFO info = GetResInfo(res); + overscan.right = info.iWidth; + overscan.bottom = info.iHeight; +} + +void CGraphicContext::ResetScreenParameters(RESOLUTION res) +{ + RESOLUTION_INFO& info = CDisplaySettings::GetInstance().GetResolutionInfo(res); + + info.iSubtitles = info.iHeight; + info.fPixelRatio = 1.0f; + info.iScreenWidth = info.iWidth; + info.iScreenHeight = info.iHeight; + ResetOverscan(res, info.Overscan); +} + +void CGraphicContext::Clear(UTILS::COLOR::Color color) +{ + CServiceBroker::GetRenderSystem()->ClearBuffers(color); +} + +void CGraphicContext::CaptureStateBlock() +{ + CServiceBroker::GetRenderSystem()->CaptureStateBlock(); +} + +void CGraphicContext::ApplyStateBlock() +{ + CServiceBroker::GetRenderSystem()->ApplyStateBlock(); +} + +const RESOLUTION_INFO CGraphicContext::GetResInfo(RESOLUTION res) const +{ + RESOLUTION_INFO info = CDisplaySettings::GetInstance().GetResolutionInfo(res); + + if(m_stereoMode == RENDER_STEREO_MODE_SPLIT_HORIZONTAL) + { + if((info.dwFlags & D3DPRESENTFLAG_MODE3DTB) == 0) + { + info.fPixelRatio /= 2; + info.iBlanking = 0; + info.dwFlags |= D3DPRESENTFLAG_MODE3DTB; + } + info.iHeight = (info.iHeight - info.iBlanking) / 2; + info.Overscan.top /= 2; + info.Overscan.bottom = (info.Overscan.bottom - info.iBlanking) / 2; + info.iSubtitles = (info.iSubtitles - info.iBlanking) / 2; + } + + if(m_stereoMode == RENDER_STEREO_MODE_SPLIT_VERTICAL) + { + if((info.dwFlags & D3DPRESENTFLAG_MODE3DSBS) == 0) + { + info.fPixelRatio *= 2; + info.iBlanking = 0; + info.dwFlags |= D3DPRESENTFLAG_MODE3DSBS; + } + info.iWidth = (info.iWidth - info.iBlanking) / 2; + info.Overscan.left /= 2; + info.Overscan.right = (info.Overscan.right - info.iBlanking) / 2; + } + + if (res == m_Resolution && m_fFPSOverride != 0) + { + info.fRefreshRate = m_fFPSOverride; + } + + return info; +} + +void CGraphicContext::SetResInfo(RESOLUTION res, const RESOLUTION_INFO& info) +{ + RESOLUTION_INFO& curr = CDisplaySettings::GetInstance().GetResolutionInfo(res); + curr.Overscan = info.Overscan; + curr.iSubtitles = info.iSubtitles; + curr.fPixelRatio = info.fPixelRatio; + + if(info.dwFlags & D3DPRESENTFLAG_MODE3DSBS) + { + curr.Overscan.right = info.Overscan.right * 2 + info.iBlanking; + if((curr.dwFlags & D3DPRESENTFLAG_MODE3DSBS) == 0) + curr.fPixelRatio /= 2.0f; + } + + if(info.dwFlags & D3DPRESENTFLAG_MODE3DTB) + { + curr.Overscan.bottom = info.Overscan.bottom * 2 + info.iBlanking; + curr.iSubtitles = info.iSubtitles * 2 + info.iBlanking; + if((curr.dwFlags & D3DPRESENTFLAG_MODE3DTB) == 0) + curr.fPixelRatio *= 2.0f; + } +} + +const RESOLUTION_INFO CGraphicContext::GetResInfo() const +{ + return GetResInfo(m_Resolution); +} + +void CGraphicContext::GetGUIScaling(const RESOLUTION_INFO &res, float &scaleX, float &scaleY, TransformMatrix *matrix /* = NULL */) +{ + if (m_Resolution != RES_INVALID) + { + // calculate necessary scalings + RESOLUTION_INFO info = GetResInfo(); + float fFromWidth = (float)res.iWidth; + float fFromHeight = (float)res.iHeight; + auto fToPosX = info.Overscan.left + info.guiInsets.left; + auto fToPosY = info.Overscan.top + info.guiInsets.top; + auto fToWidth = info.Overscan.right - info.guiInsets.right - fToPosX; + auto fToHeight = info.Overscan.bottom - info.guiInsets.bottom - fToPosY; + + float fZoom = (100 + CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_LOOKANDFEEL_SKINZOOM)) * 0.01f; + + fZoom -= 1.0f; + fToPosX -= fToWidth * fZoom * 0.5f; + fToWidth *= fZoom + 1.0f; + + // adjust for aspect ratio as zoom is given in the vertical direction and we don't + // do aspect ratio corrections in the gui code + fZoom = fZoom / info.fPixelRatio; + fToPosY -= fToHeight * fZoom * 0.5f; + fToHeight *= fZoom + 1.0f; + + scaleX = fFromWidth / fToWidth; + scaleY = fFromHeight / fToHeight; + if (matrix) + { + TransformMatrix guiScaler = TransformMatrix::CreateScaler(fToWidth / fFromWidth, fToHeight / fFromHeight, fToHeight / fFromHeight); + TransformMatrix guiOffset = TransformMatrix::CreateTranslation(fToPosX, fToPosY); + *matrix = guiOffset * guiScaler; + } + } + else + { + scaleX = scaleY = 1.0f; + if (matrix) + matrix->Reset(); + } +} + +void CGraphicContext::SetScalingResolution(const RESOLUTION_INFO &res, bool needsScaling) +{ + m_windowResolution = res; + if (needsScaling && m_Resolution != RES_INVALID) + GetGUIScaling(res, m_guiTransform.scaleX, m_guiTransform.scaleY, &m_guiTransform.matrix); + else + { + m_guiTransform.Reset(); + } + + // reset our origin and camera + while (!m_origins.empty()) + m_origins.pop(); + m_origins.push(CPoint(0, 0)); + while (!m_cameras.empty()) + m_cameras.pop(); + m_cameras.push(CPoint(0.5f*m_iScreenWidth, 0.5f*m_iScreenHeight)); + while (!m_stereoFactors.empty()) + m_stereoFactors.pop(); + m_stereoFactors.push(0.0f); + + // and reset the final transform + m_finalTransform = m_guiTransform; +} + +void CGraphicContext::SetRenderingResolution(const RESOLUTION_INFO &res, bool needsScaling) +{ + std::unique_lock<CCriticalSection> lock(*this); + + SetScalingResolution(res, needsScaling); + UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top()); +} + +void CGraphicContext::SetStereoView(RENDER_STEREO_VIEW view) +{ + m_stereoView = view; + + while(!m_viewStack.empty()) + m_viewStack.pop(); + + CRect viewport(0.0f, 0.0f, (float)m_iScreenWidth, (float)m_iScreenHeight); + + m_viewStack.push(viewport); + + viewport = StereoCorrection(viewport); + CServiceBroker::GetRenderSystem()->SetStereoMode(m_stereoMode, m_stereoView); + CServiceBroker::GetRenderSystem()->SetViewPort(viewport); + CServiceBroker::GetRenderSystem()->SetScissors(viewport); +} + +void CGraphicContext::InvertFinalCoords(float &x, float &y) const +{ + m_finalTransform.matrix.InverseTransformPosition(x, y); +} + +float CGraphicContext::ScaleFinalXCoord(float x, float y) const +{ + return m_finalTransform.matrix.TransformXCoord(x, y, 0); +} + +float CGraphicContext::ScaleFinalYCoord(float x, float y) const +{ + return m_finalTransform.matrix.TransformYCoord(x, y, 0); +} + +float CGraphicContext::ScaleFinalZCoord(float x, float y) const +{ + return m_finalTransform.matrix.TransformZCoord(x, y, 0); +} + +void CGraphicContext::ScaleFinalCoords(float &x, float &y, float &z) const +{ + m_finalTransform.matrix.TransformPosition(x, y, z); +} + +float CGraphicContext::GetScalingPixelRatio() const +{ + // assume the resolutions are different - we want to return the aspect ratio of the video resolution + // but only once it's been corrected for the skin -> screen coordinates scaling + return GetResInfo().fPixelRatio * (m_finalTransform.scaleY / m_finalTransform.scaleX); +} + +void CGraphicContext::SetCameraPosition(const CPoint &camera) +{ + // offset the camera from our current location (this is in XML coordinates) and scale it up to + // the screen resolution + CPoint cam(camera); + if (!m_origins.empty()) + cam += m_origins.top(); + + cam.x *= (float)m_iScreenWidth / m_windowResolution.iWidth; + cam.y *= (float)m_iScreenHeight / m_windowResolution.iHeight; + + m_cameras.push(cam); + UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top()); +} + +void CGraphicContext::RestoreCameraPosition() +{ // remove the top camera from the stack + assert(m_cameras.size()); + m_cameras.pop(); + UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top()); +} + +void CGraphicContext::SetStereoFactor(float factor) +{ + m_stereoFactors.push(factor); + UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top()); +} + +void CGraphicContext::RestoreStereoFactor() +{ // remove the top factor from the stack + assert(m_stereoFactors.size()); + m_stereoFactors.pop(); + UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top()); +} + +CRect CGraphicContext::GenerateAABB(const CRect &rect) const +{ +// ------------------------ +// |(x1, y1) (x2, y2)| +// | | +// |(x3, y3) (x4, y4)| +// ------------------------ + + float x1 = rect.x1, x2 = rect.x2, x3 = rect.x1, x4 = rect.x2; + float y1 = rect.y1, y2 = rect.y1, y3 = rect.y2, y4 = rect.y2; + + float z = 0.0f; + ScaleFinalCoords(x1, y1, z); + CServiceBroker::GetRenderSystem()->Project(x1, y1, z); + + z = 0.0f; + ScaleFinalCoords(x2, y2, z); + CServiceBroker::GetRenderSystem()->Project(x2, y2, z); + + z = 0.0f; + ScaleFinalCoords(x3, y3, z); + CServiceBroker::GetRenderSystem()->Project(x3, y3, z); + + z = 0.0f; + ScaleFinalCoords(x4, y4, z); + CServiceBroker::GetRenderSystem()->Project(x4, y4, z); + + return CRect( std::min(std::min(std::min(x1, x2), x3), x4), + std::min(std::min(std::min(y1, y2), y3), y4), + std::max(std::max(std::max(x1, x2), x3), x4), + std::max(std::max(std::max(y1, y2), y3), y4)); +} + +// NOTE: This routine is currently called (twice) every time there is a <camera> +// tag in the skin. It actually only has to be called before we render +// something, so another option is to just save the camera coordinates +// and then have a routine called before every draw that checks whether +// the camera has changed, and if so, changes it. Similarly, it could set +// the world transform at that point as well (or even combine world + view +// to cut down on one setting) +void CGraphicContext::UpdateCameraPosition(const CPoint &camera, const float &factor) +{ + float stereoFactor = 0.f; + if ( m_stereoMode != RENDER_STEREO_MODE_OFF + && m_stereoMode != RENDER_STEREO_MODE_MONO + && m_stereoView != RENDER_STEREO_VIEW_OFF) + { + RESOLUTION_INFO res = GetResInfo(); + RESOLUTION_INFO desktop = GetResInfo(RES_DESKTOP); + float scaleRes = (static_cast<float>(res.iWidth) / static_cast<float>(desktop.iWidth)); + float scaleX = static_cast<float>(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_LOOKANDFEEL_STEREOSTRENGTH)) * scaleRes; + stereoFactor = factor * (m_stereoView == RENDER_STEREO_VIEW_LEFT ? scaleX : -scaleX); + } + CServiceBroker::GetRenderSystem()->SetCameraPosition(camera, m_iScreenWidth, m_iScreenHeight, stereoFactor); +} + +bool CGraphicContext::RectIsAngled(float x1, float y1, float x2, float y2) const +{ // need only test 3 points, as they must be co-planer + if (m_finalTransform.matrix.TransformZCoord(x1, y1, 0)) return true; + if (m_finalTransform.matrix.TransformZCoord(x2, y2, 0)) return true; + if (m_finalTransform.matrix.TransformZCoord(x1, y2, 0)) return true; + return false; +} + +const TransformMatrix &CGraphicContext::GetGUIMatrix() const +{ + return m_finalTransform.matrix; +} + +float CGraphicContext::GetGUIScaleX() const +{ + return m_finalTransform.scaleX; +} + +float CGraphicContext::GetGUIScaleY() const +{ + return m_finalTransform.scaleY; +} + +UTILS::COLOR::Color CGraphicContext::MergeAlpha(UTILS::COLOR::Color color) const +{ + UTILS::COLOR::Color alpha = m_finalTransform.matrix.TransformAlpha((color >> 24) & 0xff); + if (alpha > 255) alpha = 255; + return ((alpha << 24) & 0xff000000) | (color & 0xffffff); +} + +UTILS::COLOR::Color CGraphicContext::MergeColor(UTILS::COLOR::Color color) const +{ + return m_finalTransform.matrix.TransformColor(color); +} + +int CGraphicContext::GetWidth() const +{ + return m_iScreenWidth; +} + +int CGraphicContext::GetHeight() const +{ + return m_iScreenHeight; +} + +float CGraphicContext::GetFPS() const +{ + if (m_Resolution != RES_INVALID) + { + RESOLUTION_INFO info = GetResInfo(); + if (info.fRefreshRate > 0) + return info.fRefreshRate; + } + return 60.0f; +} + +float CGraphicContext::GetDisplayLatency() const +{ + float latency = CServiceBroker::GetWinSystem()->GetDisplayLatency(); + if (latency < 0.0f) + { + // fallback + latency = (CServiceBroker::GetWinSystem()->NoOfBuffers() + 1) / GetFPS() * 1000.0f; + } + + return latency; +} + +bool CGraphicContext::IsFullScreenRoot () const +{ + return m_bFullScreenRoot; +} + +void CGraphicContext::ToggleFullScreen() +{ + RESOLUTION uiRes; + + if (m_bFullScreenRoot) + { + uiRes = RES_WINDOW; + } + else + { + if (CDisplaySettings::GetInstance().GetCurrentResolution() > RES_DESKTOP) + uiRes = CDisplaySettings::GetInstance().GetCurrentResolution(); + else + uiRes = RES_DESKTOP; + } + + CDisplaySettings::GetInstance().SetCurrentResolution(uiRes, true); +} + +void CGraphicContext::SetMediaDir(const std::string &strMediaDir) +{ + CServiceBroker::GetGUI()->GetTextureManager().SetTexturePath(strMediaDir); + m_strMediaDir = strMediaDir; +} + +const std::string& CGraphicContext::GetMediaDir() const +{ + return m_strMediaDir; + +} + +void CGraphicContext::Flip(bool rendered, bool videoLayer) +{ + CServiceBroker::GetRenderSystem()->PresentRender(rendered, videoLayer); + + if(m_stereoMode != m_nextStereoMode) + { + m_stereoMode = m_nextStereoMode; + SetVideoResolution(GetVideoResolution(), true); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_RENDERER_RESET); + } +} + +void CGraphicContext::GetAllowedResolutions(std::vector<RESOLUTION> &res) +{ + res.clear(); + + res.push_back(RES_WINDOW); + res.push_back(RES_DESKTOP); + for (size_t r = (size_t) RES_CUSTOM; r < CDisplaySettings::GetInstance().ResolutionInfoSize(); r++) + { + res.push_back((RESOLUTION) r); + } +} + +void CGraphicContext::SetFPS(float fps) +{ + m_fFPSOverride = fps; +} diff --git a/xbmc/windowing/GraphicContext.h b/xbmc/windowing/GraphicContext.h new file mode 100644 index 0000000..9cd4f32 --- /dev/null +++ b/xbmc/windowing/GraphicContext.h @@ -0,0 +1,232 @@ +/* + * 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 + +#include "Resolution.h" +#include "rendering/RenderSystem.h" +#include "threads/CriticalSection.h" +#include "utils/ColorUtils.h" +#include "utils/Geometry.h" // for CRect/CPoint +#include "utils/TransformMatrix.h" // for the members m_guiTransform etc. + +#include <map> +#include <stack> +#include <string> +#include <vector> + +// required by clients +#include "ServiceBroker.h" +#include "WinSystem.h" + +#define D3DPRESENTFLAG_INTERLACED 1 +#define D3DPRESENTFLAG_WIDESCREEN 2 +#define D3DPRESENTFLAG_PROGRESSIVE 4 +#define D3DPRESENTFLAG_MODE3DSBS 8 +#define D3DPRESENTFLAG_MODE3DTB 16 + +/* what types are important for mode setting */ +#define D3DPRESENTFLAG_MODEMASK ( D3DPRESENTFLAG_INTERLACED \ + | D3DPRESENTFLAG_MODE3DSBS \ + | D3DPRESENTFLAG_MODE3DTB ) + +enum VIEW_TYPE { VIEW_TYPE_NONE = 0, + VIEW_TYPE_LIST, + VIEW_TYPE_ICON, + VIEW_TYPE_BIG_LIST, + VIEW_TYPE_BIG_ICON, + VIEW_TYPE_WIDE, + VIEW_TYPE_BIG_WIDE, + VIEW_TYPE_WRAP, + VIEW_TYPE_BIG_WRAP, + VIEW_TYPE_INFO, + VIEW_TYPE_BIG_INFO, + VIEW_TYPE_AUTO, + VIEW_TYPE_MAX }; + +enum AdjustRefreshRate +{ + ADJUST_REFRESHRATE_OFF = 0, + ADJUST_REFRESHRATE_ALWAYS, + ADJUST_REFRESHRATE_ON_STARTSTOP, + ADJUST_REFRESHRATE_ON_START, +}; + +class CGraphicContext : public CCriticalSection +{ +public: + CGraphicContext(void); + virtual ~CGraphicContext(); + + // methods related to windowing + float GetFPS() const; + void SetFPS(float fps); + float GetDisplayLatency() const; + bool IsFullScreenRoot() const; + void ToggleFullScreen(); + void SetFullScreenVideo(bool bOnOff); + bool IsFullScreenVideo() const; + bool IsValidResolution(RESOLUTION res); + void SetVideoResolution(RESOLUTION res, bool forceUpdate); + void ApplyModeChange(RESOLUTION res); + void ApplyWindowResize(int newWidth, int newHeight); + RESOLUTION GetVideoResolution() const; + const RESOLUTION_INFO GetResInfo() const; + const RESOLUTION_INFO GetResInfo(RESOLUTION res) const; + void SetResInfo(RESOLUTION res, const RESOLUTION_INFO& info); + + void Flip(bool rendered, bool videoLayer); + + // gfx context interface + int GetWidth() const; + int GetHeight() const; + bool SetViewPort(float fx, float fy , float fwidth, float fheight, bool intersectPrevious = false); + void RestoreViewPort(); + void SetScissors(const CRect &rect); + void ResetScissors(); + const CRect &GetScissors() const; + const CRect GetViewWindow() const; + void SetViewWindow(float left, float top, float right, float bottom); + bool IsCalibrating() const; + void SetCalibrating(bool bOnOff); + void ResetOverscan(RESOLUTION res, OVERSCAN &overscan); + void ResetOverscan(RESOLUTION_INFO &resinfo); + void ResetScreenParameters(RESOLUTION res); + void CaptureStateBlock(); + void ApplyStateBlock(); + void Clear(UTILS::COLOR::Color color = 0); + void GetAllowedResolutions(std::vector<RESOLUTION> &res); + + /* \brief Get UI scaling information from a given resolution to the screen resolution. + Takes account of overscan and UI zooming. + \param res the resolution to scale from. + \param scaleX [out] the scaling amount in the X direction. + \param scaleY [out] the scaling amount in the Y direction. + \param matrix [out] if non-NULL, a suitable transformation from res to screen resolution is set. + */ + void GetGUIScaling(const RESOLUTION_INFO &res, float &scaleX, float &scaleY, TransformMatrix *matrix = NULL); + void SetRenderingResolution(const RESOLUTION_INFO &res, bool needsScaling); ///< Sets scaling up for rendering + void SetScalingResolution(const RESOLUTION_INFO &res, bool needsScaling); ///< Sets scaling up for skin loading etc. + float GetScalingPixelRatio() const; + void InvertFinalCoords(float &x, float &y) const; + float ScaleFinalXCoord(float x, float y) const; + float ScaleFinalYCoord(float x, float y) const; + float ScaleFinalZCoord(float x, float y) const; + void ScaleFinalCoords(float &x, float &y, float &z) const; + bool RectIsAngled(float x1, float y1, float x2, float y2) const; + const TransformMatrix &GetGUIMatrix() const; + float GetGUIScaleX() const; + float GetGUIScaleY() const; + UTILS::COLOR::Color MergeAlpha(UTILS::COLOR::Color color) const; + UTILS::COLOR::Color MergeColor(UTILS::COLOR::Color color) const; + void SetOrigin(float x, float y); + void RestoreOrigin(); + void SetCameraPosition(const CPoint &camera); + void SetStereoView(RENDER_STEREO_VIEW view); + RENDER_STEREO_VIEW GetStereoView() { return m_stereoView; } + void SetStereoMode(RENDER_STEREO_MODE mode) { m_nextStereoMode = mode; } + RENDER_STEREO_MODE GetStereoMode() { return m_stereoMode; } + void RestoreCameraPosition(); + void SetStereoFactor(float factor); + void RestoreStereoFactor(); + /*! \brief Set a region in which to clip all rendering + Anything that is rendered after setting a clip region will be clipped so that no part renders + outside of the clip region. Successive calls to SetClipRegion intersect the clip region, which + means the clip region may eventually become an empty set. In this case SetClipRegion returns false + to indicate that no rendering need be performed. + + This call must be matched with a RestoreClipRegion call unless SetClipRegion returns false. + + Usage should be of the form: + + if (SetClipRegion(x, y, w, h)) + { + ... + perform rendering + ... + RestoreClipRegion(); + } + + \param x the left-most coordinate of the clip region + \param y the top-most coordinate of the clip region + \param w the width of the clip region + \param h the height of the clip region + \returns true if the region is set and the result is non-empty. Returns false if the resulting region is empty. + \sa RestoreClipRegion + */ + bool SetClipRegion(float x, float y, float w, float h); + void RestoreClipRegion(); + void ClipRect(CRect &vertex, CRect &texture, CRect *diffuse = NULL); + CRect GetClipRegion(); + void AddGUITransform(); + TransformMatrix AddTransform(const TransformMatrix &matrix); + void SetTransform(const TransformMatrix &matrix); + void SetTransform(const TransformMatrix &matrix, float scaleX, float scaleY); + void RemoveTransform(); + + /* modifies final coordinates according to stereo mode if needed */ + CRect StereoCorrection(const CRect &rect) const; + CPoint StereoCorrection(const CPoint &point) const; + + CRect GenerateAABB(const CRect &rect) const; + + //@todo move those somewhere else + const std::string& GetMediaDir() const; + void SetMediaDir(const std::string& strMediaDir); + +protected: + + void UpdateCameraPosition(const CPoint &camera, const float &factor); + void SetVideoResolutionInternal(RESOLUTION res, bool forceUpdate); + void ApplyVideoResolution(RESOLUTION res); + void UpdateInternalStateWithResolution(RESOLUTION res); + + int m_iScreenHeight = 576; + int m_iScreenWidth = 720; + std::string m_strMediaDir; + CRect m_videoRect; + bool m_bFullScreenRoot = true; + bool m_bFullScreenVideo = false; + bool m_bCalibrating = false; + RESOLUTION m_Resolution = RES_INVALID; + float m_fFPSOverride = 0.0f; + + RESOLUTION_INFO m_windowResolution; + std::stack<CPoint> m_cameras; + std::stack<CPoint> m_origins; + std::stack<CRect> m_clipRegions; + std::stack<float> m_stereoFactors; + std::stack<CRect> m_viewStack; + CRect m_scissors; + + class UITransform + { + public: + UITransform() : matrix() {} + UITransform(const TransformMatrix& m, const float sX = 1.0f, const float sY = 1.0f) + : matrix(m), scaleX(sX), scaleY(sY) + { + } + void Reset() + { + matrix.Reset(); + scaleX = scaleY = 1.0f; + } + + TransformMatrix matrix; + float scaleX = 1.0f; + float scaleY = 1.0f; + }; + + UITransform m_guiTransform; + UITransform m_finalTransform; + std::stack<UITransform> m_transforms; + RENDER_STEREO_VIEW m_stereoView = RENDER_STEREO_VIEW_OFF; + RENDER_STEREO_MODE m_stereoMode = RENDER_STEREO_MODE_OFF; + RENDER_STEREO_MODE m_nextStereoMode = RENDER_STEREO_MODE_OFF; +}; diff --git a/xbmc/windowing/OSScreenSaver.cpp b/xbmc/windowing/OSScreenSaver.cpp new file mode 100644 index 0000000..3651314 --- /dev/null +++ b/xbmc/windowing/OSScreenSaver.cpp @@ -0,0 +1,98 @@ +/* + * 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 "OSScreenSaver.h" + +#include "utils/log.h" + +using namespace KODI::WINDOWING; + +COSScreenSaverManager::COSScreenSaverManager(std::unique_ptr<IOSScreenSaver> impl) +: m_impl{std::move(impl)} +{ +} + +COSScreenSaverInhibitor COSScreenSaverManager::CreateInhibitor() +{ + COSScreenSaverInhibitor inhibitor{this}; + if (m_inhibitionCount++ == 0) + { + // Inhibit if this was first inhibitor + CLog::Log(LOGDEBUG, "Inhibiting OS screen saver"); + m_impl->Inhibit(); + } + return inhibitor; +} + +bool COSScreenSaverManager::IsInhibited() +{ + return (m_inhibitionCount > 0); +} + +void COSScreenSaverManager::RemoveInhibitor() +{ + if (--m_inhibitionCount == 0) + { + CLog::Log(LOGDEBUG, "Uninhibiting OS screen saver"); + // Uninhibit if this was last inhibitor + m_impl->Uninhibit(); + } +} + +COSScreenSaverInhibitor::COSScreenSaverInhibitor() noexcept +: m_active{false}, m_manager{} +{ +} + +COSScreenSaverInhibitor::COSScreenSaverInhibitor(COSScreenSaverManager* manager) +: m_active{true}, m_manager{manager} +{ +} + +COSScreenSaverInhibitor::COSScreenSaverInhibitor(COSScreenSaverInhibitor&& other) noexcept +: m_active{false}, m_manager{} +{ + *this = std::move(other); +} + +COSScreenSaverInhibitor& COSScreenSaverInhibitor::operator=(COSScreenSaverInhibitor&& other) noexcept +{ + Release(); + m_active = other.m_active; + m_manager = other.m_manager; + other.m_active = false; + other.m_manager = nullptr; + return *this; +} + +bool COSScreenSaverInhibitor::IsActive() const +{ + return m_active; +} + +COSScreenSaverInhibitor::operator bool() const +{ + return IsActive(); +} + +void COSScreenSaverInhibitor::Release() +{ + if (m_active) + { + m_manager->RemoveInhibitor(); + m_active = false; + } +} + +COSScreenSaverInhibitor::~COSScreenSaverInhibitor() noexcept +{ + Release(); +} + + + diff --git a/xbmc/windowing/OSScreenSaver.h b/xbmc/windowing/OSScreenSaver.h new file mode 100644 index 0000000..367d6db --- /dev/null +++ b/xbmc/windowing/OSScreenSaver.h @@ -0,0 +1,118 @@ +/* + * 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 <utility> + +namespace KODI +{ +namespace WINDOWING +{ + +class COSScreenSaverManager; + +/** + * Inhibit the OS screen saver as long as this object is alive + * + * Destroy or call \ref Release to stop this inhibitor from being active. + * The OS screen saver may still be inhibited as long as other inhibitors are + * active though. + * + * \note Make sure to release or destroy the inhibitor before the \ref + * COSScreenSaverManager is destroyed + */ +class COSScreenSaverInhibitor +{ +public: + COSScreenSaverInhibitor() noexcept; + COSScreenSaverInhibitor(COSScreenSaverInhibitor&& other) noexcept; + COSScreenSaverInhibitor& operator=(COSScreenSaverInhibitor&& other) noexcept; + ~COSScreenSaverInhibitor() noexcept; + void Release(); + bool IsActive() const; + operator bool() const; + +private: + friend class COSScreenSaverManager; + explicit COSScreenSaverInhibitor(COSScreenSaverManager* manager); + bool m_active; + COSScreenSaverManager* m_manager; + + COSScreenSaverInhibitor(COSScreenSaverInhibitor const& other) = delete; + COSScreenSaverInhibitor& operator=(COSScreenSaverInhibitor const& other) = delete; +}; + +/** + * Interface for OS screen saver control implementations + */ +class IOSScreenSaver +{ +public: + virtual ~IOSScreenSaver() = default; + /** + * Do not allow the OS screen saver to become active + * + * Calling this function multiple times without calling \ref Unhibit + * MUST NOT produce any side-effects. + */ + virtual void Inhibit() = 0; + /** + * Allow the OS screen saver to become active again + * + * Calling this function multiple times or at all without calling \ref Inhibit + * MUST NOT produce any side-effects. + */ + virtual void Uninhibit() = 0; +}; + +/** + * Dummy implementation of IOSScreenSaver + */ +class CDummyOSScreenSaver : public IOSScreenSaver +{ +public: + void Inhibit() override {} + void Uninhibit() override {} +}; + +/** + * Manage the OS screen saver + * + * This class keeps track of a number of \ref COSScreenSaverInhibitor instances + * and keeps the OS screen saver inhibited as long as at least one of them + * exists and is active. + */ +class COSScreenSaverManager +{ +public: + /** + * Create manager with backing OS-specific implementation + */ + explicit COSScreenSaverManager(std::unique_ptr<IOSScreenSaver> impl); + /** + * Create inhibitor that prevents the OS screen saver from becoming active as + * long as it is alive + */ + COSScreenSaverInhibitor CreateInhibitor(); + /** + * Check whether the OS screen saver is currently inhibited + */ + bool IsInhibited(); + +private: + friend class COSScreenSaverInhibitor; + void RemoveInhibitor(); + + unsigned int m_inhibitionCount{0u}; + std::unique_ptr<IOSScreenSaver> m_impl; +}; + +} +} diff --git a/xbmc/windowing/Resolution.cpp b/xbmc/windowing/Resolution.cpp new file mode 100644 index 0000000..8d9af06 --- /dev/null +++ b/xbmc/windowing/Resolution.cpp @@ -0,0 +1,474 @@ +/* + * 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 "Resolution.h" + +#include "GraphicContext.h" +#include "ServiceBroker.h" +#include "settings/AdvancedSettings.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/MathUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <cstdlib> +#include <limits> + +namespace +{ + +const char* SETTING_VIDEOSCREEN_WHITELIST_PULLDOWN{"videoscreen.whitelistpulldown"}; +const char* SETTING_VIDEOSCREEN_WHITELIST_DOUBLEREFRESHRATE{ + "videoscreen.whitelistdoublerefreshrate"}; + +} // namespace + +EdgeInsets::EdgeInsets(float l, float t, float r, float b) : left(l), top(t), right(r), bottom(b) +{ +} + +RESOLUTION_INFO::RESOLUTION_INFO(int width, int height, float aspect, const std::string &mode) : + strMode(mode) +{ + iWidth = width; + iHeight = height; + iBlanking = 0; + iScreenWidth = width; + iScreenHeight = height; + fPixelRatio = aspect ? ((float)width)/height / aspect : 1.0f; + bFullScreen = true; + fRefreshRate = 0; + dwFlags = iSubtitles = 0; +} + +RESOLUTION_INFO::RESOLUTION_INFO(const RESOLUTION_INFO& res) + : Overscan(res.Overscan), + guiInsets(res.guiInsets), + strMode(res.strMode), + strOutput(res.strOutput), + strId(res.strId) +{ + bFullScreen = res.bFullScreen; + iWidth = res.iWidth; iHeight = res.iHeight; + iScreenWidth = res.iScreenWidth; iScreenHeight = res.iScreenHeight; + iSubtitles = res.iSubtitles; dwFlags = res.dwFlags; + fPixelRatio = res.fPixelRatio; fRefreshRate = res.fRefreshRate; + iBlanking = res.iBlanking; +} + +float RESOLUTION_INFO::DisplayRatio() const +{ + return iWidth * fPixelRatio / iHeight; +} + +RESOLUTION CResolutionUtils::ChooseBestResolution(float fps, int width, int height, bool is3D) +{ + RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); + float weight = 0.0f; + + if (!FindResolutionFromOverride(fps, width, is3D, res, weight, false)) //find a refreshrate from overrides + { + if (!FindResolutionFromOverride(fps, width, is3D, res, weight, true)) //if that fails find it from a fallback + { + FindResolutionFromWhitelist(fps, width, height, is3D, res); //find a refreshrate from whitelist + } + } + + CLog::Log(LOGINFO, "Display resolution ADJUST : {} ({}) (weight: {:.3f})", + CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(res).strMode, res, weight); + return res; +} + +void CResolutionUtils::FindResolutionFromWhitelist(float fps, int width, int height, bool is3D, RESOLUTION &resolution) +{ + RESOLUTION_INFO curr = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(resolution); + CLog::Log(LOGINFO, + "[WHITELIST] Searching the whitelist for: width: {}, height: {}, fps: {:0.3f}, 3D: {}", + width, height, fps, is3D ? "true" : "false"); + + std::vector<CVariant> indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(CSettings::SETTING_VIDEOSCREEN_WHITELIST); + + bool noWhiteList = indexList.empty(); + + if (noWhiteList) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Using the default whitelist because the user whitelist is empty"); + std::vector<RESOLUTION> candidates; + RESOLUTION_INFO info; + std::string resString; + CServiceBroker::GetWinSystem()->GetGfxContext().GetAllowedResolutions(candidates); + for (const auto& c : candidates) + { + info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(c); + if (info.iScreenHeight >= curr.iScreenHeight && info.iScreenWidth >= curr.iScreenWidth && + (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK)) + { + // do not add half refreshrates (25, 29.97 by default) as kodi cannot cope with + // them on playback start. Especially interlaced content is not properly detected + // and this causes ugly double switching. + // This won't allow 25p / 30p playback on native refreshrate by default + if ((info.fRefreshRate > 30) || (MathUtils::FloatEquals(info.fRefreshRate, 24.0f, 0.1f))) + { + resString = CDisplaySettings::GetInstance().GetStringFromRes(c); + indexList.emplace_back(resString); + } + } + } + } + + CLog::Log(LOGDEBUG, "[WHITELIST] Searching for an exact resolution with an exact refresh rate"); + + unsigned int penalty = std::numeric_limits<unsigned int>::max(); + bool found = false; + + for (const auto& mode : indexList) + { + auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i); + + // allow resolutions that are exact and have the correct refresh rate + // allow macroblock alignment / padding errors (e.g. 1080 mod16 == 8) + if (((height == info.iScreenHeight && width <= info.iScreenWidth + 8) || + (width == info.iScreenWidth && height <= info.iScreenHeight + 8)) && + (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK) && + MathUtils::FloatEquals(info.fRefreshRate, fps, 0.01f)) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Matched an exact resolution with an exact refresh rate {} ({})", + info.strMode, i); + unsigned int pen = abs(info.iScreenHeight - height) + abs(info.iScreenWidth - width); + if (pen < penalty) + { + resolution = i; + found = true; + penalty = pen; + } + } + } + + if (!found) + CLog::Log(LOGDEBUG, "[WHITELIST] No match for an exact resolution with an exact refresh rate"); + + if (noWhiteList || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + SETTING_VIDEOSCREEN_WHITELIST_DOUBLEREFRESHRATE)) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Searching for an exact resolution with double the refresh rate"); + + for (const auto& mode : indexList) + { + auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i); + + // allow resolutions that are exact and have double the refresh rate + // allow macroblock alignment / padding errors (e.g. 1080 mod16 == 8) + if (((height == info.iScreenHeight && width <= info.iScreenWidth + 8) || + (width == info.iScreenWidth && height <= info.iScreenHeight + 8)) && + (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK) && + MathUtils::FloatEquals(info.fRefreshRate, fps * 2, 0.01f)) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Matched an exact resolution with double the refresh rate {} ({})", + info.strMode, i); + unsigned int pen = abs(info.iScreenHeight - height) + abs(info.iScreenWidth - width); + if (pen < penalty) + { + resolution = i; + found = true; + penalty = pen; + } + } + } + if (found) + return; + + CLog::Log(LOGDEBUG, + "[WHITELIST] No match for an exact resolution with double the refresh rate"); + } + else if (found) + return; + + if (noWhiteList || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + SETTING_VIDEOSCREEN_WHITELIST_PULLDOWN)) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Searching for an exact resolution with a 3:2 pulldown refresh rate"); + + for (const auto& mode : indexList) + { + auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i); + + // allow resolutions that are exact and have 2.5 times the refresh rate + // allow macroblock alignment / padding errors (e.g. 1080 mod16 == 8) + if (((height == info.iScreenHeight && width <= info.iScreenWidth + 8) || + (width == info.iScreenWidth && height <= info.iScreenHeight + 8)) && + (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK) && + MathUtils::FloatEquals(info.fRefreshRate, fps * 2.5f, 0.01f)) + { + CLog::Log( + LOGDEBUG, + "[WHITELIST] Matched an exact resolution with a 3:2 pulldown refresh rate {} ({})", + info.strMode, i); + unsigned int pen = abs(info.iScreenHeight - height) + abs(info.iScreenWidth - width); + if (pen < penalty) + { + resolution = i; + found = true; + penalty = pen; + } + } + } + if (found) + return; + + CLog::Log(LOGDEBUG, "[WHITELIST] No match for a resolution with a 3:2 pulldown refresh rate"); + } + + + CLog::Log(LOGDEBUG, "[WHITELIST] Searching for a desktop resolution with an exact refresh rate"); + + const RESOLUTION_INFO desktop_info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(CDisplaySettings::GetInstance().GetCurrentResolution()); + + for (const auto& mode : indexList) + { + auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i); + + // allow resolutions that are desktop resolution but have the correct refresh rate + if (info.iScreenWidth == desktop_info.iScreenWidth && + (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (desktop_info.dwFlags & D3DPRESENTFLAG_MODEMASK) && + MathUtils::FloatEquals(info.fRefreshRate, fps, 0.01f)) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Matched a desktop resolution with an exact refresh rate {} ({})", + info.strMode, i); + resolution = i; + return; + } + } + + CLog::Log(LOGDEBUG, "[WHITELIST] No match for a desktop resolution with an exact refresh rate"); + + if (noWhiteList || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + SETTING_VIDEOSCREEN_WHITELIST_DOUBLEREFRESHRATE)) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Searching for a desktop resolution with double the refresh rate"); + + for (const auto& mode : indexList) + { + auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i); + + // allow resolutions that are desktop resolution but have double the refresh rate + if (info.iScreenWidth == desktop_info.iScreenWidth && + (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == + (desktop_info.dwFlags & D3DPRESENTFLAG_MODEMASK) && + MathUtils::FloatEquals(info.fRefreshRate, fps * 2, 0.01f)) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Matched a desktop resolution with double the refresh rate {} ({})", + info.strMode, i); + resolution = i; + return; + } + } + + CLog::Log(LOGDEBUG, + "[WHITELIST] No match for a desktop resolution with double the refresh rate"); + } + + if (noWhiteList || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + SETTING_VIDEOSCREEN_WHITELIST_PULLDOWN)) + { + CLog::Log(LOGDEBUG, + "[WHITELIST] Searching for a desktop resolution with a 3:2 pulldown refresh rate"); + + for (const auto& mode : indexList) + { + auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i); + + // allow resolutions that are desktop resolution but have 2.5 times the refresh rate + if (info.iScreenWidth == desktop_info.iScreenWidth && + (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == + (desktop_info.dwFlags & D3DPRESENTFLAG_MODEMASK) && + MathUtils::FloatEquals(info.fRefreshRate, fps * 2.5f, 0.01f)) + { + CLog::Log( + LOGDEBUG, + "[WHITELIST] Matched a desktop resolution with a 3:2 pulldown refresh rate {} ({})", + info.strMode, i); + resolution = i; + return; + } + } + + CLog::Log(LOGDEBUG, + "[WHITELIST] No match for a desktop resolution with a 3:2 pulldown refresh rate"); + } + + CLog::Log(LOGDEBUG, "[WHITELIST] No resolution matched"); +} + +bool CResolutionUtils::FindResolutionFromOverride(float fps, int width, bool is3D, RESOLUTION &resolution, float& weight, bool fallback) +{ + RESOLUTION_INFO curr = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(resolution); + + //try to find a refreshrate from the override + for (int i = 0; i < (int)CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAdjustRefreshOverrides.size(); i++) + { + RefreshOverride& override = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAdjustRefreshOverrides[i]; + + if (override.fallback != fallback) + continue; + + //if we're checking for overrides, check if the fps matches + if (!fallback && (fps < override.fpsmin || fps > override.fpsmax)) + continue; + + for (size_t j = (int)RES_DESKTOP; j < CDisplaySettings::GetInstance().ResolutionInfoSize(); j++) + { + RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo((RESOLUTION)j); + + if (info.iScreenWidth == curr.iScreenWidth && + info.iScreenHeight == curr.iScreenHeight && + (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK)) + { + if (info.fRefreshRate <= override.refreshmax && + info.fRefreshRate >= override.refreshmin) + { + resolution = (RESOLUTION)j; + + if (fallback) + { + CLog::Log( + LOGDEBUG, + "Found Resolution {} ({}) from fallback (refreshmin:{:.3f} refreshmax:{:.3f})", + info.strMode, resolution, override.refreshmin, override.refreshmax); + } + else + { + CLog::Log(LOGDEBUG, + "Found Resolution {} ({}) from override of fps {:.3f} (fpsmin:{:.3f} " + "fpsmax:{:.3f} refreshmin:{:.3f} refreshmax:{:.3f})", + info.strMode, resolution, fps, override.fpsmin, override.fpsmax, + override.refreshmin, override.refreshmax); + } + + weight = RefreshWeight(info.fRefreshRate, fps); + + return true; //fps and refresh match with this override, use this resolution + } + } + } + } + + return false; //no override found +} + +//distance of refresh to the closest multiple of fps (multiple is 1 or higher), as a multiplier of fps +float CResolutionUtils::RefreshWeight(float refresh, float fps) +{ + float div = refresh / fps; + int round = MathUtils::round_int(static_cast<double>(div)); + + float weight = 0.0f; + + if (round < 1) + weight = (fps - refresh) / fps; + else + weight = fabs(div / round - 1.0f); + + // punish higher refreshrates and prefer better matching + // e.g. 30 fps content at 60 hz is better than + // 30 fps at 120 hz - as we sometimes don't know if + // the content is interlaced at the start, only + // punish when refreshrate > 60 hz to not have to switch + // twice for 30i content + if (refresh > 60 && round > 1) + weight += round / 10000.0f; + + return weight; +} + +bool CResolutionUtils::HasWhitelist() +{ + std::vector<CVariant> indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(CSettings::SETTING_VIDEOSCREEN_WHITELIST); + return !indexList.empty(); +} + +void CResolutionUtils::PrintWhitelist() +{ + std::string modeStr; + auto indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList( + CSettings::SETTING_VIDEOSCREEN_WHITELIST); + if (!indexList.empty()) + { + for (const auto& mode : indexList) + { + auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i); + modeStr.append("\n" + info.strMode); + } + + CLog::Log(LOGDEBUG, "[WHITELIST] whitelisted modes:{}", modeStr); + } +} + +void CResolutionUtils::GetMaxAllowedResolution(unsigned int& width, unsigned int& height) +{ + if (!CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot()) + return; + + std::vector<RESOLUTION_INFO> resList; + + auto indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList( + CSettings::SETTING_VIDEOSCREEN_WHITELIST); + + unsigned int maxWidth{0}; + unsigned int maxHeight{0}; + + if (!indexList.empty()) + { + for (const auto& mode : indexList) + { + RESOLUTION res = CDisplaySettings::GetInstance().GetResFromString(mode.asString()); + RESOLUTION_INFO resInfo{CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(res)}; + if (static_cast<unsigned int>(resInfo.iScreenWidth) > maxWidth && + static_cast<unsigned int>(resInfo.iScreenHeight) > maxHeight) + { + maxWidth = static_cast<unsigned int>(resInfo.iScreenWidth); + maxHeight = static_cast<unsigned int>(resInfo.iScreenHeight); + } + } + } + else + { + std::vector<RESOLUTION> resList; + CServiceBroker::GetWinSystem()->GetGfxContext().GetAllowedResolutions(resList); + + for (const auto& res : resList) + { + RESOLUTION_INFO resInfo{CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(res)}; + if (static_cast<unsigned int>(resInfo.iScreenWidth) > maxWidth && + static_cast<unsigned int>(resInfo.iScreenHeight) > maxHeight) + { + maxWidth = static_cast<unsigned int>(resInfo.iScreenWidth); + maxHeight = static_cast<unsigned int>(resInfo.iScreenHeight); + } + } + } + + width = maxWidth; + height = maxHeight; +} diff --git a/xbmc/windowing/Resolution.h b/xbmc/windowing/Resolution.h new file mode 100644 index 0000000..768d459 --- /dev/null +++ b/xbmc/windowing/Resolution.h @@ -0,0 +1,107 @@ +/* + * 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 + +#include <stdint.h> +#include <string> + +typedef int DisplayMode; +#define DM_WINDOWED -1 +#define DM_FULLSCREEN 0 + +enum RESOLUTION +{ + RES_INVALID = -1, + RES_WINDOW = 15, + RES_DESKTOP = 16, // Desktop resolution + RES_CUSTOM = 16 + 1, // First additional resolution +}; + +struct OVERSCAN +{ + int left; + int top; + int right; + int bottom; +public: + OVERSCAN() + { + left = top = right = bottom = 0; + } + OVERSCAN(const OVERSCAN& os) + { + left = os.left; top = os.top; + right = os.right; bottom = os.bottom; + } + OVERSCAN& operator=(const OVERSCAN&) = default; + + bool operator==(const OVERSCAN& other) + { + return left == other.left && right == other.right && top == other.top && bottom == other.bottom; + } + bool operator!=(const OVERSCAN& other) + { + return left != other.left || right != other.right || top != other.top || bottom != other.bottom; + } +}; + +struct EdgeInsets +{ + float left = 0.0f; + float top = 0.0f; + float right = 0.0f; + float bottom = 0.0f; + + EdgeInsets() = default; + EdgeInsets(float l, float t, float r, float b); +}; + +struct RESOLUTION_INFO +{ + OVERSCAN Overscan; + EdgeInsets guiInsets; + bool bFullScreen; + int iWidth; + int iHeight; + int iBlanking; /**< number of pixels of padding between stereoscopic frames */ + int iScreenWidth; + int iScreenHeight; + int iSubtitles; + uint32_t dwFlags; + float fPixelRatio; + float fRefreshRate; + std::string strMode; + std::string strOutput; + std::string strId; +public: + RESOLUTION_INFO(int width = 1280, int height = 720, float aspect = 0, const std::string &mode = ""); + float DisplayRatio() const; + RESOLUTION_INFO(const RESOLUTION_INFO& res); + RESOLUTION_INFO& operator=(const RESOLUTION_INFO&) = default; +}; + +class CResolutionUtils +{ +public: + static RESOLUTION ChooseBestResolution(float fps, int width, int height, bool is3D); + static bool HasWhitelist(); + static void PrintWhitelist(); + + /*! + * \brief Get the max allowed resolution, if fullscreen + * \param width [OUT] Max width resolution + * \param height [OUT] Max height resolution + */ + static void GetMaxAllowedResolution(unsigned int& width, unsigned int& height); + +protected: + static void FindResolutionFromWhitelist(float fps, int width, int height, bool is3D, RESOLUTION &resolution); + static bool FindResolutionFromOverride(float fps, int width, bool is3D, RESOLUTION &resolution, float& weight, bool fallback); + static float RefreshWeight(float refresh, float fps); +}; diff --git a/xbmc/windowing/VideoSync.h b/xbmc/windowing/VideoSync.h new file mode 100644 index 0000000..7e79339 --- /dev/null +++ b/xbmc/windowing/VideoSync.h @@ -0,0 +1,31 @@ +/* + * 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 + +#include "threads/Event.h" + +class CVideoReferenceClock; +typedef void (*PUPDATECLOCK)(int NrVBlanks, uint64_t time, void *clock); + +class CVideoSync +{ +public: + explicit CVideoSync(void* clock) { m_refClock = clock; } + virtual ~CVideoSync() = default; + virtual bool Setup(PUPDATECLOCK func) = 0; + virtual void Run(CEvent& stop) = 0; + virtual void Cleanup() = 0; + virtual float GetFps() = 0; + virtual void RefreshChanged() {} + +protected: + PUPDATECLOCK UpdateClock; + float m_fps; + void *m_refClock; +}; diff --git a/xbmc/windowing/WinEvents.h b/xbmc/windowing/WinEvents.h new file mode 100644 index 0000000..8f58c6c --- /dev/null +++ b/xbmc/windowing/WinEvents.h @@ -0,0 +1,19 @@ +/* + * 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 + +#include "XBMC_events.h" + +class IWinEvents +{ +public: + virtual ~IWinEvents() = default; + virtual bool MessagePump() = 0; +}; + diff --git a/xbmc/windowing/WinSystem.cpp b/xbmc/windowing/WinSystem.cpp new file mode 100644 index 0000000..cef6940 --- /dev/null +++ b/xbmc/windowing/WinSystem.cpp @@ -0,0 +1,278 @@ +/* + * 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 "WinSystem.h" + +#include "ServiceBroker.h" +#include "guilib/DispResource.h" +#include "powermanagement/DPMSSupport.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "utils/StringUtils.h" +#include "windowing/GraphicContext.h" + +#include <mutex> +#if HAS_GLES +#include "guilib/GUIFontTTFGL.h" +#endif + +const char* CWinSystemBase::SETTING_WINSYSTEM_IS_HDR_DISPLAY = "winsystem.ishdrdisplay"; + +CWinSystemBase::CWinSystemBase() +{ + m_gfxContext.reset(new CGraphicContext()); +} + +CWinSystemBase::~CWinSystemBase() = default; + +bool CWinSystemBase::InitWindowSystem() +{ + UpdateResolutions(); + CDisplaySettings::GetInstance().ApplyCalibrations(); + + CResolutionUtils::PrintWhitelist(); + + return true; +} + +bool CWinSystemBase::DestroyWindowSystem() +{ + m_screenSaverManager.reset(); + return false; +} + +void CWinSystemBase::UpdateDesktopResolution(RESOLUTION_INFO& newRes, const std::string &output, int width, int height, float refreshRate, uint32_t dwFlags) +{ + newRes.Overscan.left = 0; + newRes.Overscan.top = 0; + newRes.Overscan.right = width; + newRes.Overscan.bottom = height; + newRes.bFullScreen = true; + newRes.iSubtitles = height; + newRes.dwFlags = dwFlags; + newRes.fRefreshRate = refreshRate; + newRes.fPixelRatio = 1.0f; + newRes.iWidth = width; + newRes.iHeight = height; + newRes.iScreenWidth = width; + newRes.iScreenHeight = height; + newRes.strMode = StringUtils::Format("{}: {}x{}", output, width, height); + if (refreshRate > 1) + newRes.strMode += StringUtils::Format(" @ {:.2f}Hz", refreshRate); + if (dwFlags & D3DPRESENTFLAG_INTERLACED) + newRes.strMode += "i"; + if (dwFlags & D3DPRESENTFLAG_MODE3DTB) + newRes.strMode += "tab"; + if (dwFlags & D3DPRESENTFLAG_MODE3DSBS) + newRes.strMode += "sbs"; + newRes.strOutput = output; +} + +void CWinSystemBase::UpdateResolutions() +{ + // add the window res - defaults are fine. + RESOLUTION_INFO& window = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + window.bFullScreen = false; + if (window.iWidth == 0) + window.iWidth = 720; + if (window.iHeight == 0) + window.iHeight = 480; + window.iScreenWidth = window.iWidth; + window.iScreenHeight = window.iHeight; + if (window.iSubtitles == 0) + window.iSubtitles = window.iHeight; + window.fPixelRatio = 1.0f; + window.strMode = "Windowed"; +} + +void CWinSystemBase::SetWindowResolution(int width, int height) +{ + RESOLUTION_INFO& window = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + window.iWidth = width; + window.iHeight = height; + window.iScreenWidth = width; + window.iScreenHeight = height; + window.iSubtitles = window.iHeight; + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(window); +} + +static void AddResolution(std::vector<RESOLUTION_WHR> &resolutions, unsigned int addindex, float bestRefreshrate) +{ + RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(addindex); + int width = resInfo.iScreenWidth; + int height = resInfo.iScreenHeight; + int flags = resInfo.dwFlags & D3DPRESENTFLAG_MODEMASK; + float refreshrate = resInfo.fRefreshRate; + + // don't touch RES_DESKTOP + for (unsigned int idx = 1; idx < resolutions.size(); idx++) + if ( resolutions[idx].width == width + && resolutions[idx].height == height + &&(resolutions[idx].flags & D3DPRESENTFLAG_MODEMASK) == flags) + { + // check if the refresh rate of this resolution is better suited than + // the refresh rate of the resolution with the same width/height/interlaced + // property and if so replace it + if (bestRefreshrate > 0.0f && refreshrate == bestRefreshrate) + resolutions[idx].ResInfo_Index = addindex; + + // no need to add the resolution again + return; + } + + RESOLUTION_WHR res = {width, height, flags, (int)addindex}; + resolutions.push_back(res); +} + +static bool resSortPredicate(RESOLUTION_WHR i, RESOLUTION_WHR j) +{ + // note: this comparison must obey "strict weak ordering" + // a "!=" on the flags comparison resulted in memory corruption + return ( i.width < j.width + || (i.width == j.width && i.height < j.height) + || (i.width == j.width && i.height == j.height && i.flags < j.flags) ); +} + +std::vector<RESOLUTION_WHR> CWinSystemBase::ScreenResolutions(float refreshrate) +{ + std::vector<RESOLUTION_WHR> resolutions; + + for (unsigned int idx = RES_CUSTOM; idx < CDisplaySettings::GetInstance().ResolutionInfoSize(); idx++) + { + RESOLUTION_INFO info = CDisplaySettings::GetInstance().GetResolutionInfo(idx); + AddResolution(resolutions, idx, refreshrate); + } + + // Can't assume a sort order + sort(resolutions.begin(), resolutions.end(), resSortPredicate); + + return resolutions; +} + +static void AddRefreshRate(std::vector<REFRESHRATE> &refreshrates, unsigned int addindex) +{ + float RefreshRate = CDisplaySettings::GetInstance().GetResolutionInfo(addindex).fRefreshRate; + + for (unsigned int idx = 0; idx < refreshrates.size(); idx++) + if ( refreshrates[idx].RefreshRate == RefreshRate) + return; // already taken care of. + + REFRESHRATE rr = {RefreshRate, (int)addindex}; + refreshrates.push_back(rr); +} + +static bool rrSortPredicate(REFRESHRATE i, REFRESHRATE j) +{ + return (i.RefreshRate < j.RefreshRate); +} + +std::vector<REFRESHRATE> CWinSystemBase::RefreshRates(int width, int height, uint32_t dwFlags) +{ + std::vector<REFRESHRATE> refreshrates; + + for (unsigned int idx = RES_DESKTOP; idx < CDisplaySettings::GetInstance().ResolutionInfoSize(); idx++) + { + if (CDisplaySettings::GetInstance().GetResolutionInfo(idx).iScreenWidth == width && + CDisplaySettings::GetInstance().GetResolutionInfo(idx).iScreenHeight == height && + (CDisplaySettings::GetInstance().GetResolutionInfo(idx).dwFlags & D3DPRESENTFLAG_MODEMASK) == (dwFlags & D3DPRESENTFLAG_MODEMASK)) + AddRefreshRate(refreshrates, idx); + } + + // Can't assume a sort order + sort(refreshrates.begin(), refreshrates.end(), rrSortPredicate); + + return refreshrates; +} + +REFRESHRATE CWinSystemBase::DefaultRefreshRate(std::vector<REFRESHRATE> rates) +{ + REFRESHRATE bestmatch = rates[0]; + float bestfitness = -1.0f; + float targetfps = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).fRefreshRate; + + for (unsigned i = 0; i < rates.size(); i++) + { + float fitness = fabs(targetfps - rates[i].RefreshRate); + + if (bestfitness <0 || fitness < bestfitness) + { + bestfitness = fitness; + bestmatch = rates[i]; + if (bestfitness == 0.0f) // perfect match + break; + } + } + return bestmatch; +} + +bool CWinSystemBase::UseLimitedColor() +{ + return false; +} + +std::string CWinSystemBase::GetClipboardText(void) +{ + return ""; +} + +int CWinSystemBase::NoOfBuffers(void) +{ + int buffers = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOSCREEN_NOOFBUFFERS); + return buffers; +} + +KODI::WINDOWING::COSScreenSaverManager* CWinSystemBase::GetOSScreenSaver() +{ + if (!m_screenSaverManager) + { + auto impl = GetOSScreenSaverImpl(); + if (impl) + { + m_screenSaverManager.reset(new KODI::WINDOWING::COSScreenSaverManager(std::move(impl))); + } + } + + return m_screenSaverManager.get(); +} + +void CWinSystemBase::RegisterRenderLoop(IRenderLoop *client) +{ + std::unique_lock<CCriticalSection> lock(m_renderLoopSection); + m_renderLoopClients.push_back(client); +} + +void CWinSystemBase::UnregisterRenderLoop(IRenderLoop *client) +{ + std::unique_lock<CCriticalSection> lock(m_renderLoopSection); + auto i = find(m_renderLoopClients.begin(), m_renderLoopClients.end(), client); + if (i != m_renderLoopClients.end()) + m_renderLoopClients.erase(i); +} + +void CWinSystemBase::DriveRenderLoop() +{ + MessagePump(); + + { + std::unique_lock<CCriticalSection> lock(m_renderLoopSection); + for (auto i = m_renderLoopClients.begin(); i != m_renderLoopClients.end(); ++i) + (*i)->FrameMove(); + } +} + +CGraphicContext& CWinSystemBase::GetGfxContext() +{ + return *m_gfxContext; +} + +std::shared_ptr<CDPMSSupport> CWinSystemBase::GetDPMSManager() +{ + return m_dpms; +} diff --git a/xbmc/windowing/WinSystem.h b/xbmc/windowing/WinSystem.h new file mode 100644 index 0000000..cac94b0 --- /dev/null +++ b/xbmc/windowing/WinSystem.h @@ -0,0 +1,203 @@ +/* + * 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 + +#include "HDRStatus.h" +#include "OSScreenSaver.h" +#include "Resolution.h" +#include "VideoSync.h" +#include "WinEvents.h" +#include "cores/VideoPlayer/VideoRenderers/DebugInfo.h" +#include "guilib/DispResource.h" +#include "utils/HDRCapabilities.h" + +#include <memory> +#include <vector> + +struct RESOLUTION_WHR +{ + int width; + int height; + int flags; //< only D3DPRESENTFLAG_MODEMASK flags + int ResInfo_Index; +}; + +struct REFRESHRATE +{ + float RefreshRate; + int ResInfo_Index; +}; + +class CDPMSSupport; +class CGraphicContext; +class CRenderSystemBase; +class IRenderLoop; + +struct VideoPicture; + +class CWinSystemBase +{ +public: + CWinSystemBase(); + virtual ~CWinSystemBase(); + + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Access render system interface + virtual CRenderSystemBase *GetRenderSystem() { return nullptr; } + + virtual const std::string GetName() { return "platform default"; } + + // windowing interfaces + virtual bool InitWindowSystem(); + virtual bool DestroyWindowSystem(); + virtual bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) = 0; + virtual bool DestroyWindow(){ return false; } + virtual bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) = 0; + virtual bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) = 0; + virtual bool DisplayHardwareScalingEnabled() { return false; } + virtual void UpdateDisplayHardwareScaling(const RESOLUTION_INFO& resInfo) { } + virtual bool MoveWindow(int topLeft, int topRight){return false;} + virtual void FinishModeChange(RESOLUTION res){} + virtual void FinishWindowResize(int newWidth, int newHeight) {ResizeWindow(newWidth, newHeight, -1, -1);} + virtual bool CenterWindow(){return false;} + virtual bool IsCreated(){ return m_bWindowCreated; } + virtual void NotifyAppFocusChange(bool bGaining) {} + virtual void NotifyAppActiveChange(bool bActivated) {} + virtual void ShowOSMouse(bool show) {} + virtual bool HasCursor(){ return true; } + //some platforms have api for gesture inertial scrolling - default to false and use the InertialScrollingHandler + virtual bool HasInertialGestures(){ return false; } + //does the output expect limited color range (ie 16-235) + virtual bool UseLimitedColor(); + //the number of presentation buffers + virtual int NoOfBuffers(); + /** + * Get average display latency + * + * The latency should be measured as the time between finishing the rendering + * of a frame, i.e. calling PresentRender, and the rendered content becoming + * visible on the screen. + * + * \return average display latency in seconds, or negative value if unknown + */ + virtual float GetDisplayLatency() { return -1.0f; } + /** + * Get time that should be subtracted from the display latency for this frame + * in milliseconds + * + * Contrary to \ref GetDisplayLatency, this value is calculated ad-hoc + * for the frame currently being rendered and not a value that is calculated/ + * averaged from past frames and their presentation times + */ + virtual float GetFrameLatencyAdjustment() { return 0.0; } + + virtual bool Minimize() { return false; } + virtual bool Restore() { return false; } + virtual bool Hide() { return false; } + virtual bool Show(bool raise = true) { return false; } + + // videosync + virtual std::unique_ptr<CVideoSync> GetVideoSync(void *clock) { return nullptr; } + + // notifications + virtual void OnMove(int x, int y) {} + + // OS System screensaver + /** + * Get OS screen saver inhibit implementation if available + * + * \return OS screen saver implementation that can be used with this windowing system + * or nullptr if unsupported. + * Lifetime of the returned object will usually end with \ref DestroyWindowSystem, so + * do not use any more after calling that. + */ + KODI::WINDOWING::COSScreenSaverManager* GetOSScreenSaver(); + + // resolution interfaces + unsigned int GetWidth() { return m_nWidth; } + unsigned int GetHeight() { return m_nHeight; } + virtual bool CanDoWindowed() { return true; } + bool IsFullScreen() { return m_bFullScreen; } + virtual void UpdateResolutions(); + void SetWindowResolution(int width, int height); + std::vector<RESOLUTION_WHR> ScreenResolutions(float refreshrate); + std::vector<REFRESHRATE> RefreshRates(int width, int height, uint32_t dwFlags); + REFRESHRATE DefaultRefreshRate(std::vector<REFRESHRATE> rates); + virtual bool HasCalibration(const RESOLUTION_INFO& resInfo) { return true; } + + // text input interface + virtual std::string GetClipboardText(void); + + // Display event callback + virtual void Register(IDispResource *resource) = 0; + virtual void Unregister(IDispResource *resource) = 0; + + // render loop + void RegisterRenderLoop(IRenderLoop *client); + void UnregisterRenderLoop(IRenderLoop *client); + void DriveRenderLoop(); + + // winsystem events + virtual bool MessagePump() { return false; } + + // Access render system interface + CGraphicContext& GetGfxContext(); + + /** + * Get OS specific hardware context + * + * \return OS specific context or nullptr if OS not have + * + * \note This function is currently only related to Windows with DirectX, + * all other OS where use GL returns nullptr. + * Returned Windows class pointer is ID3D11DeviceContext1. + */ + virtual void* GetHWContext() { return nullptr; } + + std::shared_ptr<CDPMSSupport> GetDPMSManager(); + + /** + * @brief Set the HDR metadata. Passing nullptr as the parameter should + * disable HDR. + * + */ + virtual bool SetHDR(const VideoPicture* videoPicture) { return false; } + virtual bool IsHDRDisplay() { return false; } + virtual HDR_STATUS ToggleHDR() { return HDR_STATUS::HDR_UNSUPPORTED; } + virtual HDR_STATUS GetOSHDRStatus() { return HDR_STATUS::HDR_UNSUPPORTED; } + virtual CHDRCapabilities GetDisplayHDRCapabilities() const { return {}; } + + static const char* SETTING_WINSYSTEM_IS_HDR_DISPLAY; + + // Gets debug info from video renderer + virtual DEBUG_INFO_RENDER GetDebugInfo() { return {}; } + + virtual std::vector<std::string> GetConnectedOutputs() { return {}; } + +protected: + void UpdateDesktopResolution(RESOLUTION_INFO& newRes, const std::string &output, int width, int height, float refreshRate, uint32_t dwFlags); + virtual std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() { return nullptr; } + + int m_nWidth = 0; + int m_nHeight = 0; + int m_nTop = 0; + int m_nLeft = 0; + bool m_bWindowCreated = false; + bool m_bFullScreen = false; + bool m_bBlankOtherDisplay = false; + float m_fRefreshRate = 0.0f; + std::unique_ptr<KODI::WINDOWING::COSScreenSaverManager> m_screenSaverManager; + CCriticalSection m_renderLoopSection; + std::vector<IRenderLoop*> m_renderLoopClients; + + std::unique_ptr<IWinEvents> m_winEvents; + std::unique_ptr<CGraphicContext> m_gfxContext; + std::shared_ptr<CDPMSSupport> m_dpms; +}; diff --git a/xbmc/windowing/WindowSystemFactory.cpp b/xbmc/windowing/WindowSystemFactory.cpp new file mode 100644 index 0000000..edb6098 --- /dev/null +++ b/xbmc/windowing/WindowSystemFactory.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2005-2020 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 "WindowSystemFactory.h" + +#include <algorithm> + +using namespace KODI::WINDOWING; + +std::list<std::pair<std::string, std::function<std::unique_ptr<CWinSystemBase>()>>> + CWindowSystemFactory::m_windowSystems; + +std::list<std::string> CWindowSystemFactory::GetWindowSystems() +{ + std::list<std::string> available; + for (const auto& windowSystem : m_windowSystems) + available.emplace_back(windowSystem.first); + + return available; +} + +std::unique_ptr<CWinSystemBase> CWindowSystemFactory::CreateWindowSystem(const std::string& name) +{ + auto windowSystem = + std::find_if(m_windowSystems.begin(), m_windowSystems.end(), + [&name](auto& windowSystem) { return windowSystem.first == name; }); + if (windowSystem != m_windowSystems.end()) + return windowSystem->second(); + + return nullptr; +} + +void CWindowSystemFactory::RegisterWindowSystem( + const std::function<std::unique_ptr<CWinSystemBase>()>& createFunction, const std::string& name) +{ + m_windowSystems.emplace_back(std::make_pair(name, createFunction)); +} diff --git a/xbmc/windowing/WindowSystemFactory.h b/xbmc/windowing/WindowSystemFactory.h new file mode 100644 index 0000000..20e051d --- /dev/null +++ b/xbmc/windowing/WindowSystemFactory.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2005-2020 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 "WinSystem.h" + +#include <functional> +#include <list> +#include <map> +#include <memory> + +namespace KODI +{ +namespace WINDOWING +{ + +class CWindowSystemFactory +{ +public: + static std::unique_ptr<CWinSystemBase> CreateWindowSystem(const std::string& name); + static std::list<std::string> GetWindowSystems(); + static void RegisterWindowSystem( + const std::function<std::unique_ptr<CWinSystemBase>()>& createFunction, + const std::string& name = "default"); + +private: + static std::list<std::pair<std::string, std::function<std::unique_ptr<CWinSystemBase>()>>> + m_windowSystems; +}; + +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/X11/CMakeLists.txt b/xbmc/windowing/X11/CMakeLists.txt new file mode 100644 index 0000000..91e13d6 --- /dev/null +++ b/xbmc/windowing/X11/CMakeLists.txt @@ -0,0 +1,37 @@ +set(SOURCES GLContextEGL.cpp + GLContext.cpp + OptionalsReg.cpp + OSScreenSaverX11.cpp + WinEventsX11.cpp + WinSystemX11.cpp + XRandR.cpp + X11DPMSSupport.cpp) + +set(HEADERS GLContext.h + GLContextEGL.h + OptionalsReg.h + OSScreenSaverX11.h + WinEventsX11.h + WinSystemX11.h + XRandR.h + X11DPMSSupport.h) + +if(GLX_FOUND) + list(APPEND SOURCES GLContextGLX.cpp + VideoSyncGLX.cpp) + list(APPEND HEADERS GLContextGLX.h + VideoSyncGLX.h) +endif() + +if(OPENGL_FOUND) + list(APPEND SOURCES WinSystemX11GLContext.cpp) + list(APPEND HEADERS WinSystemX11GLContext.h) + list(APPEND SOURCES VideoSyncOML.cpp) + list(APPEND HEADERS VideoSyncOML.h) +endif() +if(OPENGLES_FOUND) + list(APPEND SOURCES WinSystemX11GLESContext.cpp) + list(APPEND HEADERS WinSystemX11GLESContext.h) +endif() + +core_add_library(windowing_X11) diff --git a/xbmc/windowing/X11/GLContext.cpp b/xbmc/windowing/X11/GLContext.cpp new file mode 100644 index 0000000..3dac508 --- /dev/null +++ b/xbmc/windowing/X11/GLContext.cpp @@ -0,0 +1,20 @@ +/* + * 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 "GLContext.h" + +bool CGLContext::IsExtSupported(const char* extension) const +{ + std::string name; + + name = " "; + name += extension; + name += " "; + + return m_extensions.find(name) != std::string::npos; +} diff --git a/xbmc/windowing/X11/GLContext.h b/xbmc/windowing/X11/GLContext.h new file mode 100644 index 0000000..95e22ec --- /dev/null +++ b/xbmc/windowing/X11/GLContext.h @@ -0,0 +1,42 @@ +/* + * 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 + +#include <cstdint> +#include <string> + +#include <X11/Xlib.h> + +class CGLContext +{ +public: + explicit CGLContext(Display *dpy) + { + m_dpy = dpy; + } + virtual ~CGLContext() = default; + virtual bool Refresh(bool force, int screen, Window glWindow, bool &newContext) = 0; + virtual bool CreatePB() { return false; } + virtual void Destroy() = 0; + virtual void Detach() = 0; + virtual void SetVSync(bool enable) = 0; + virtual void SwapBuffers() = 0; + virtual void QueryExtensions() = 0; + virtual uint64_t GetVblankTiming(uint64_t& msc, uint64_t& interval) { return 0; } + bool IsExtSupported(const char* extension) const; + + std::string ExtPrefix() { return m_extPrefix; } + std::string m_extPrefix; + std::string m_extensions; + + Display *m_dpy; + +protected: + bool m_omlSync = true; +}; diff --git a/xbmc/windowing/X11/GLContextEGL.cpp b/xbmc/windowing/X11/GLContextEGL.cpp new file mode 100644 index 0000000..66de374 --- /dev/null +++ b/xbmc/windowing/X11/GLContextEGL.cpp @@ -0,0 +1,542 @@ +/* + * 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. + */ + +// always define GL_GLEXT_PROTOTYPES before include gl headers +#if !defined(GL_GLEXT_PROTOTYPES) + #define GL_GLEXT_PROTOTYPES +#endif + +#include "GLContextEGL.h" + +#include "ServiceBroker.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/log.h" + +#include <clocale> +#include <mutex> + +#include <EGL/eglext.h> +#include <unistd.h> + +#include "PlatformDefs.h" +#include "system_gl.h" + +#define EGL_NO_CONFIG (EGLConfig)0 + +CGLContextEGL::CGLContextEGL(Display* dpy, EGLint renderingApi) + : CGLContext(dpy), m_renderingApi(renderingApi) +{ + m_extPrefix = "EGL_"; + m_eglConfig = EGL_NO_CONFIG; + + m_eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); + + const auto settings = CServiceBroker::GetSettingsComponent(); + if (settings) + { + m_omlSync = settings->GetAdvancedSettings()->m_omlSync; + } +} + +CGLContextEGL::~CGLContextEGL() +{ + Destroy(); +} + +bool CGLContextEGL::Refresh(bool force, int screen, Window glWindow, bool &newContext) +{ + m_sync.cont = 0; + + // refresh context + if (m_eglContext && !force) + { + if (m_eglSurface == EGL_NO_SURFACE) + { + m_eglSurface = eglCreateWindowSurface(m_eglDisplay, m_eglConfig, glWindow, NULL); + if (m_eglSurface == EGL_NO_SURFACE) + { + CLog::Log(LOGERROR, "failed to create EGL window surface {}", eglGetError()); + return false; + } + } + + CLog::Log(LOGDEBUG, "CWinSystemX11::RefreshEGLContext: refreshing context"); + eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext); + return true; + } + + Destroy(); + newContext = true; + + if (m_eglGetPlatformDisplayEXT) + { + EGLint attribs[] = + { + EGL_PLATFORM_X11_SCREEN_EXT, screen, + EGL_NONE + }; + m_eglDisplay = m_eglGetPlatformDisplayEXT(EGL_PLATFORM_X11_EXT,(EGLNativeDisplayType)m_dpy, + attribs); + } + else + m_eglDisplay = eglGetDisplay((EGLNativeDisplayType)m_dpy); + + if (m_eglDisplay == EGL_NO_DISPLAY) + { + CLog::Log(LOGERROR, "failed to get egl display"); + return false; + } + if (!eglInitialize(m_eglDisplay, NULL, NULL)) + { + CLog::Log(LOGERROR, "failed to initialize egl"); + Destroy(); + return false; + } + if (!eglBindAPI(m_renderingApi)) + { + CLog::Log(LOGERROR, "failed to bind rendering API"); + Destroy(); + return false; + } + + // create context + + XVisualInfo vMask; + XVisualInfo *vInfo = nullptr; + int availableVisuals = 0; + vMask.screen = screen; + XWindowAttributes winAttr; + + if (!XGetWindowAttributes(m_dpy, glWindow, &winAttr)) + { + CLog::Log(LOGWARNING, "Failed to get window attributes"); + Destroy(); + return false; + } + + vMask.visualid = XVisualIDFromVisual(winAttr.visual); + vInfo = XGetVisualInfo(m_dpy, VisualScreenMask | VisualIDMask, &vMask, &availableVisuals); + if (!vInfo) + { + CLog::Log(LOGERROR, "Failed to get VisualInfo of visual 0x{:x}", (unsigned)vMask.visualid); + Destroy(); + return false; + } + + unsigned int visualid = static_cast<unsigned int>(vInfo->visualid); + m_eglConfig = GetEGLConfig(m_eglDisplay, vInfo); + XFree(vInfo); + + if (m_eglConfig == EGL_NO_CONFIG) + { + CLog::Log(LOGERROR, "failed to get suitable eglconfig for visual 0x{:x}", visualid); + Destroy(); + return false; + } + + CLog::Log(LOGINFO, "Using visual 0x{:x}", visualid); + + m_eglSurface = eglCreateWindowSurface(m_eglDisplay, m_eglConfig, glWindow, NULL); + if (m_eglSurface == EGL_NO_SURFACE) + { + CLog::Log(LOGERROR, "failed to create EGL window surface {}", eglGetError()); + Destroy(); + return false; + } + + EGLint contextAttributes[] = + { + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR, + EGL_NONE + }; + m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, EGL_NO_CONTEXT, contextAttributes); + if (m_eglContext == EGL_NO_CONTEXT) + { + EGLint contextAttributes[] = + { + EGL_CONTEXT_MAJOR_VERSION_KHR, 2, + EGL_NONE + }; + m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, EGL_NO_CONTEXT, contextAttributes); + + if (m_eglContext == EGL_NO_CONTEXT) + { + CLog::Log(LOGERROR, "failed to create EGL context"); + Destroy(); + return false; + } + + CLog::Log(LOGWARNING, "Failed to get an OpenGL context supporting core profile 3.2, " + "using legacy mode with reduced feature set"); + } + + if (!eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)) + { + CLog::Log(LOGERROR, "Failed to make context current {} {} {}", fmt::ptr(m_eglDisplay), + fmt::ptr(m_eglSurface), fmt::ptr(m_eglContext)); + Destroy(); + return false; + } + + m_eglGetSyncValuesCHROMIUM = (PFNEGLGETSYNCVALUESCHROMIUMPROC)eglGetProcAddress("eglGetSyncValuesCHROMIUM"); + + m_usePB = false; + return true; +} + +bool CGLContextEGL::CreatePB() +{ + const EGLint configAttribs[] = + { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_DEPTH_SIZE, 8, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_NONE + }; + + const EGLint pbufferAttribs[] = + { + EGL_WIDTH, 9, + EGL_HEIGHT, 9, + EGL_NONE, + }; + + Destroy(); + + if (m_eglGetPlatformDisplayEXT) + { + m_eglDisplay = m_eglGetPlatformDisplayEXT(EGL_PLATFORM_X11_EXT,(EGLNativeDisplayType)m_dpy, + NULL); + } + else + m_eglDisplay = eglGetDisplay((EGLNativeDisplayType)m_dpy); + + if (m_eglDisplay == EGL_NO_DISPLAY) + { + CLog::Log(LOGERROR, "failed to get egl display"); + return false; + } + if (!eglInitialize(m_eglDisplay, NULL, NULL)) + { + CLog::Log(LOGERROR, "failed to initialize egl"); + Destroy(); + return false; + } + if (!eglBindAPI(m_renderingApi)) + { + CLog::Log(LOGERROR, "failed to bind rendering API"); + Destroy(); + return false; + } + + EGLint numConfigs; + + eglChooseConfig(m_eglDisplay, configAttribs, &m_eglConfig, 1, &numConfigs); + m_eglSurface = eglCreatePbufferSurface(m_eglDisplay, m_eglConfig, pbufferAttribs); + if (m_eglSurface == EGL_NO_SURFACE) + { + CLog::Log(LOGERROR, "failed to create EGL window surface {}", eglGetError()); + Destroy(); + return false; + } + + EGLint contextAttributes[] = + { + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR, + EGL_NONE + }; + m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, EGL_NO_CONTEXT, contextAttributes); + if (m_eglContext == EGL_NO_CONTEXT) + { + EGLint contextAttributes[] = + { + EGL_CONTEXT_MAJOR_VERSION_KHR, 2, + EGL_NONE + }; + m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, EGL_NO_CONTEXT, contextAttributes); + + if (m_eglContext == EGL_NO_CONTEXT) + { + CLog::Log(LOGERROR, "failed to create EGL context"); + Destroy(); + return false; + } + } + + if (!eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)) + { + CLog::Log(LOGERROR, "Failed to make context current {} {} {}", fmt::ptr(m_eglDisplay), + fmt::ptr(m_eglSurface), fmt::ptr(m_eglContext)); + Destroy(); + return false; + } + + m_usePB = true; + return true; +} + +void CGLContextEGL::Destroy() +{ + if (m_eglContext) + { + glFinish(); + eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroyContext(m_eglDisplay, m_eglContext); + m_eglContext = EGL_NO_CONTEXT; + } + + if (m_eglSurface) + { + eglDestroySurface(m_eglDisplay, m_eglSurface); + m_eglSurface = EGL_NO_SURFACE; + } + + if (m_eglDisplay) + { + eglTerminate(m_eglDisplay); + m_eglDisplay = EGL_NO_DISPLAY; + } + + m_eglConfig = EGL_NO_CONFIG; +} + +void CGLContextEGL::Detach() +{ + if (m_eglContext) + { + glFinish(); + eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + if (m_eglSurface) + { + eglDestroySurface(m_eglDisplay, m_eglSurface); + m_eglSurface = EGL_NO_SURFACE; + } +} + +bool CGLContextEGL::SuitableCheck(EGLDisplay eglDisplay, EGLConfig config) +{ + if (config == EGL_NO_CONFIG) + return false; + + EGLint value; + if (!eglGetConfigAttrib(eglDisplay, config, EGL_RED_SIZE, &value) || value < 8) + return false; + if (!eglGetConfigAttrib(eglDisplay, config, EGL_GREEN_SIZE, &value) || value < 8) + return false; + if (!eglGetConfigAttrib(eglDisplay, config, EGL_BLUE_SIZE, &value) || value < 8) + return false; + if (!eglGetConfigAttrib(eglDisplay, config, EGL_DEPTH_SIZE, &value) || value < 24) + return false; + + return true; +} + +EGLConfig CGLContextEGL::GetEGLConfig(EGLDisplay eglDisplay, XVisualInfo *vInfo) +{ + EGLint numConfigs; + + if (!eglGetConfigs(eglDisplay, nullptr, 0, &numConfigs)) + { + CLog::Log(LOGERROR, "Failed to query number of egl configs"); + return EGL_NO_CONFIG; + } + if (numConfigs == 0) + { + CLog::Log(LOGERROR, "No suitable egl configs found"); + return EGL_NO_CONFIG; + } + + EGLConfig *eglConfigs; + eglConfigs = (EGLConfig*)malloc(numConfigs * sizeof(EGLConfig)); + if (!eglConfigs) + { + CLog::Log(LOGERROR, "eglConfigs malloc failed"); + return EGL_NO_CONFIG; + } + EGLConfig eglConfig = EGL_NO_CONFIG; + if (!eglGetConfigs(eglDisplay, eglConfigs, numConfigs, &numConfigs)) + { + CLog::Log(LOGERROR, "Failed to query egl configs"); + goto Exit; + } + for (EGLint i = 0; i < numConfigs; ++i) + { + if (!SuitableCheck(eglDisplay, eglConfigs[i])) + continue; + + EGLint value; + if (!eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_NATIVE_VISUAL_ID, &value)) + { + CLog::Log(LOGERROR, "Failed to query EGL_NATIVE_VISUAL_ID for egl config."); + break; + } + if (value == (EGLint)vInfo->visualid) + { + eglConfig = eglConfigs[i]; + break; + } + } + +Exit: + free(eglConfigs); + return eglConfig; +} + +void CGLContextEGL::SetVSync(bool enable) +{ + eglSwapInterval(m_eglDisplay, enable ? 1 : 0); +} + +void CGLContextEGL::SwapBuffers() +{ + if ((m_eglDisplay == EGL_NO_DISPLAY) || (m_eglSurface == EGL_NO_SURFACE)) + return; + + if (m_usePB) + { + eglSwapBuffers(m_eglDisplay, m_eglSurface); + usleep(20 * 1000); + return; + } + + uint64_t ust1, ust2; + uint64_t msc1, msc2; + uint64_t sbc1, sbc2; + struct timespec nowTs; + uint64_t now; + uint64_t cont = m_sync.cont; + uint64_t interval = m_sync.interval; + + if (m_eglGetSyncValuesCHROMIUM) + { + m_eglGetSyncValuesCHROMIUM(m_eglDisplay, m_eglSurface, &ust1, &msc1, &sbc1); + } + + eglSwapBuffers(m_eglDisplay, m_eglSurface); + + if (!m_eglGetSyncValuesCHROMIUM) + return; + + clock_gettime(CLOCK_MONOTONIC, &nowTs); + now = static_cast<uint64_t>(nowTs.tv_sec) * 1000000000ULL + nowTs.tv_nsec; + + m_eglGetSyncValuesCHROMIUM(m_eglDisplay, m_eglSurface, &ust2, &msc2, &sbc2); + + if ((msc1 - m_sync.msc1) > 2) + { + cont = 0; + } + + // we want to block in SwapBuffers + // if a vertical retrace occurs 5 times in a row outside + // of this function, we take action + if (m_sync.cont < 5) + { + if ((msc1 - m_sync.msc1) == 2) + { + cont = 0; + } + else if ((msc1 - m_sync.msc1) == 1) + { + interval = (ust1 - m_sync.ust1) / (msc1 - m_sync.msc1); + cont++; + } + } + else if (m_sync.cont == 5 && m_omlSync) + { + CLog::Log(LOGDEBUG, "CGLContextEGL::SwapBuffers: sync check blocking"); + + if (msc2 == msc1) + { + // if no vertical retrace has occurred in eglSwapBuffers, + // sleep until next vertical retrace + uint64_t lastIncrement = (now / 1000 - ust2); + if (lastIncrement > m_sync.interval) + { + lastIncrement = m_sync.interval; + CLog::Log(LOGWARNING, "CGLContextEGL::SwapBuffers: last msc time greater than interval"); + } + uint64_t sleeptime = m_sync.interval - lastIncrement; + usleep(sleeptime); + cont++; + msc2++; + CLog::Log(LOGDEBUG, "CGLContextEGL::SwapBuffers: sync sleep: {}", sleeptime); + } + } + else if ((m_sync.cont > 5) && (msc2 == m_sync.msc2)) + { + // sleep until next vertical retrace + // this avoids blocking outside of this function + uint64_t lastIncrement = (now / 1000 - ust2); + if (lastIncrement > m_sync.interval) + { + lastIncrement = m_sync.interval; + CLog::Log(LOGWARNING, "CGLContextEGL::SwapBuffers: last msc time greater than interval (1)"); + } + uint64_t sleeptime = m_sync.interval - lastIncrement; + usleep(sleeptime); + msc2++; + } + { + std::unique_lock<CCriticalSection> lock(m_syncLock); + m_sync.ust1 = ust1; + m_sync.ust2 = ust2; + m_sync.msc1 = msc1; + m_sync.msc2 = msc2; + m_sync.interval = interval; + m_sync.cont = cont; + } +} + +uint64_t CGLContextEGL::GetVblankTiming(uint64_t &msc, uint64_t &interval) +{ + struct timespec nowTs; + uint64_t now; + clock_gettime(CLOCK_MONOTONIC, &nowTs); + now = static_cast<uint64_t>(nowTs.tv_sec) * 1000000000ULL + nowTs.tv_nsec; + now /= 1000; + + std::unique_lock<CCriticalSection> lock(m_syncLock); + msc = m_sync.msc2; + + interval = (m_sync.cont >= 5) ? m_sync.interval : m_sync.ust2 - m_sync.ust1; + if (interval == 0) + return 0; + + if (now < m_sync.ust2) + { + return 0; + } + + uint64_t ret = now - m_sync.ust2; + while (ret > interval) + { + ret -= interval; + msc++; + } + + return ret; +} + +void CGLContextEGL::QueryExtensions() +{ + std::string extensions = eglQueryString(m_eglDisplay, EGL_EXTENSIONS); + m_extensions = std::string(" ") + extensions + " "; + + CLog::Log(LOGDEBUG, "EGL_EXTENSIONS:{}", m_extensions); +} diff --git a/xbmc/windowing/X11/GLContextEGL.h b/xbmc/windowing/X11/GLContextEGL.h new file mode 100644 index 0000000..441787b --- /dev/null +++ b/xbmc/windowing/X11/GLContextEGL.h @@ -0,0 +1,63 @@ +/* + * 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 + +#include "GLContext.h" +#include "system_egl.h" +#include "threads/CriticalSection.h" + +#include <cstdint> + +#include <EGL/eglext.h> +#ifdef HAVE_EGLEXTANGLE +#include <EGL/eglext_angle.h> +#else +#include <EGL/eglextchromium.h> +#endif +#include <X11/Xutil.h> + +class CGLContextEGL : public CGLContext +{ +public: + explicit CGLContextEGL(Display *dpy, EGLint renderingApi); + ~CGLContextEGL() override; + bool Refresh(bool force, int screen, Window glWindow, bool &newContext) override; + bool CreatePB() override; + void Destroy() override; + void Detach() override; + void SetVSync(bool enable) override; + void SwapBuffers() override; + void QueryExtensions() override; + uint64_t GetVblankTiming(uint64_t &msc, uint64_t &interval) override; + + EGLint m_renderingApi; + EGLDisplay m_eglDisplay = EGL_NO_DISPLAY; + EGLSurface m_eglSurface = EGL_NO_SURFACE; + EGLContext m_eglContext = EGL_NO_CONTEXT; + EGLConfig m_eglConfig; +protected: + bool SuitableCheck(EGLDisplay eglDisplay, EGLConfig config); + EGLConfig GetEGLConfig(EGLDisplay eglDisplay, XVisualInfo *vInfo); + PFNEGLGETSYNCVALUESCHROMIUMPROC m_eglGetSyncValuesCHROMIUM = nullptr; + PFNEGLGETPLATFORMDISPLAYEXTPROC m_eglGetPlatformDisplayEXT = nullptr; + + struct Sync + { + uint64_t cont = 0; + uint64_t ust1 = 0; + uint64_t ust2 = 0; + uint64_t msc1 = 0; + uint64_t msc2 = 0; + uint64_t interval = 0; + } m_sync; + + CCriticalSection m_syncLock; + + bool m_usePB = false; +}; diff --git a/xbmc/windowing/X11/GLContextGLX.cpp b/xbmc/windowing/X11/GLContextGLX.cpp new file mode 100644 index 0000000..3c31c22 --- /dev/null +++ b/xbmc/windowing/X11/GLContextGLX.cpp @@ -0,0 +1,293 @@ +/* + * 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 "GLContextGLX.h" + +#include "utils/log.h" + +#include <GL/glx.h> + +#include "system_gl.h" + +using namespace KODI::WINDOWING::X11; + +CGLContextGLX::CGLContextGLX(Display *dpy) : CGLContext(dpy) +{ + m_extPrefix = "GLX_"; + m_vsyncMode = 0; +} + +bool CGLContextGLX::Refresh(bool force, int screen, Window glWindow, bool &newContext) +{ + bool retVal = false; + m_glxWindow = glWindow; + m_nScreen = screen; + + // refresh context + if (m_glxContext && !force) + { + CLog::Log(LOGDEBUG, "CWinSystemX11::RefreshGlxContext: refreshing context"); + glXMakeCurrent(m_dpy, None, NULL); + glXMakeCurrent(m_dpy, glWindow, m_glxContext); + return true; + } + + // create context + + XVisualInfo vMask; + XVisualInfo *visuals; + XVisualInfo *vInfo = NULL; + int availableVisuals = 0; + vMask.screen = screen; + XWindowAttributes winAttr; + + /* Assume a depth of 24 in case the below calls to XGetWindowAttributes() + or XGetVisualInfo() fail. That shouldn't happen unless something is + fatally wrong, but lets prepare for everything. */ + vMask.depth = 24; + + if (XGetWindowAttributes(m_dpy, glWindow, &winAttr)) + { + vMask.visualid = XVisualIDFromVisual(winAttr.visual); + vInfo = XGetVisualInfo(m_dpy, VisualScreenMask | VisualIDMask, &vMask, &availableVisuals); + if (!vInfo) + CLog::Log(LOGWARNING, "Failed to get VisualInfo of visual 0x{:x}", (unsigned)vMask.visualid); + else if(!IsSuitableVisual(vInfo)) + { + CLog::Log(LOGWARNING, + "Visual 0x{:x} of the window is not suitable, looking for another one...", + (unsigned)vInfo->visualid); + vMask.depth = vInfo->depth; + XFree(vInfo); + vInfo = NULL; + } + } + else + CLog::Log(LOGWARNING, "Failed to get window attributes"); + + /* As per glXMakeCurrent documentation, we have to use the same visual as + m_glWindow. Since that was not suitable for use, we try to use another + one with the same depth and hope that the used implementation is less + strict than the documentation. */ + if (!vInfo) + { + visuals = XGetVisualInfo(m_dpy, VisualScreenMask | VisualDepthMask, &vMask, &availableVisuals); + for (int i = 0; i < availableVisuals; i++) + { + if (IsSuitableVisual(&visuals[i])) + { + vMask.visualid = visuals[i].visualid; + vInfo = XGetVisualInfo(m_dpy, VisualScreenMask | VisualIDMask, &vMask, &availableVisuals); + break; + } + } + XFree(visuals); + } + + if (vInfo) + { + CLog::Log(LOGINFO, "Using visual 0x{:x}", (unsigned)vInfo->visualid); + if (m_glxContext) + { + glXMakeCurrent(m_dpy, None, NULL); + glXDestroyContext(m_dpy, m_glxContext); + XSync(m_dpy, False); + } + + if ((m_glxContext = glXCreateContext(m_dpy, vInfo, NULL, True))) + { + // make this context current + glXMakeCurrent(m_dpy, glWindow, m_glxContext); + retVal = true; + newContext = true; + } + else + CLog::Log(LOGERROR, "GLX Error: Could not create context"); + + XFree(vInfo); + } + else + { + CLog::Log(LOGERROR, "GLX Error: vInfo is NULL!"); + } + + return retVal; +} + +void CGLContextGLX::Destroy() +{ + glXMakeCurrent(m_dpy, None, NULL); + glXDestroyContext(m_dpy, m_glxContext); + m_glxContext = 0; +} + +void CGLContextGLX::Detach() +{ + glXMakeCurrent(m_dpy, None, NULL); +} + +bool CGLContextGLX::IsSuitableVisual(XVisualInfo *vInfo) +{ + int value; + + if (glXGetConfig(m_dpy, vInfo, GLX_RGBA, &value) || !value) + return false; + if (glXGetConfig(m_dpy, vInfo, GLX_DOUBLEBUFFER, &value) || !value) + return false; + if (glXGetConfig(m_dpy, vInfo, GLX_RED_SIZE, &value) || value < 8) + return false; + if (glXGetConfig(m_dpy, vInfo, GLX_GREEN_SIZE, &value) || value < 8) + return false; + if (glXGetConfig(m_dpy, vInfo, GLX_BLUE_SIZE, &value) || value < 8) + return false; + if (glXGetConfig(m_dpy, vInfo, GLX_DEPTH_SIZE, &value) || value < 24) + return false; + + return true; +} + +void CGLContextGLX::SetVSync(bool enable) +{ + // turn of current setting first + if(m_glXSwapIntervalEXT) + m_glXSwapIntervalEXT(m_dpy, m_glxWindow, 0); + else if(m_glXSwapIntervalMESA) + m_glXSwapIntervalMESA(0); + + m_iVSyncErrors = 0; + + if(!enable) + return; + + if (m_glXSwapIntervalEXT) + { + m_glXSwapIntervalEXT(m_dpy, m_glxWindow, 1); + m_vsyncMode = 6; + } + if (m_glXSwapIntervalMESA) + { + if(m_glXSwapIntervalMESA(1) == 0) + m_vsyncMode = 2; + else + CLog::Log(LOGWARNING, "{} - glXSwapIntervalMESA failed", __FUNCTION__); + } + if (m_glXWaitVideoSyncSGI && m_glXGetVideoSyncSGI && !m_vsyncMode) + { + unsigned int count; + if(m_glXGetVideoSyncSGI(&count) == 0) + m_vsyncMode = 3; + else + CLog::Log(LOGWARNING, "{} - glXGetVideoSyncSGI failed, glcontext probably not direct", + __FUNCTION__); + } +} + +void CGLContextGLX::SwapBuffers() +{ + if (m_vsyncMode == 3) + { + glFinish(); + unsigned int before = 0, after = 0; + if (m_glXGetVideoSyncSGI(&before) != 0) + CLog::Log(LOGERROR, "{} - glXGetVideoSyncSGI - Failed to get current retrace count", + __FUNCTION__); + + glXSwapBuffers(m_dpy, m_glxWindow); + glFinish(); + + if(m_glXGetVideoSyncSGI(&after) != 0) + CLog::Log(LOGERROR, "{} - glXGetVideoSyncSGI - Failed to get current retrace count", + __FUNCTION__); + + if (after == before) + m_iVSyncErrors = 1; + else + m_iVSyncErrors--; + + if (m_iVSyncErrors > 0) + { + CLog::Log(LOGINFO, "GL: retrace count didn't change after buffer swap, switching to vsync mode 4"); + m_iVSyncErrors = 0; + m_vsyncMode = 4; + } + + if (m_iVSyncErrors < -200) + { + CLog::Log( + LOGINFO, + "GL: retrace count change for {} consecutive buffer swap, switching to vsync mode 2", + -m_iVSyncErrors); + m_iVSyncErrors = 0; + m_vsyncMode = 2; + } + } + else if (m_vsyncMode == 4) + { + glFinish(); + unsigned int before = 0, swap = 0, after = 0; + if (m_glXGetVideoSyncSGI(&before) != 0) + CLog::Log(LOGERROR, "{} - glXGetVideoSyncSGI - Failed to get current retrace count", + __FUNCTION__); + + if(m_glXWaitVideoSyncSGI(2, (before+1)%2, &swap) != 0) + CLog::Log(LOGERROR, "{} - glXWaitVideoSyncSGI - Returned error", __FUNCTION__); + + glXSwapBuffers(m_dpy, m_glxWindow); + glFinish(); + + if (m_glXGetVideoSyncSGI(&after) != 0) + CLog::Log(LOGERROR, "{} - glXGetVideoSyncSGI - Failed to get current retrace count", + __FUNCTION__); + + if (after == before) + CLog::Log(LOGERROR, "{} - glXWaitVideoSyncSGI - Woke up early", __FUNCTION__); + + if (after > before + 1) + m_iVSyncErrors++; + else + m_iVSyncErrors = 0; + + if (m_iVSyncErrors > 30) + { + CLog::Log(LOGINFO, "GL: retrace count seems to be changing due to the swapbuffers call, switching to vsync mode 3"); + m_vsyncMode = 3; + m_iVSyncErrors = 0; + } + } + else + glXSwapBuffers(m_dpy, m_glxWindow); +} + +void CGLContextGLX::QueryExtensions() +{ + m_extensions = " "; + m_extensions += glXQueryExtensionsString(m_dpy, m_nScreen); + m_extensions += " "; + + CLog::Log(LOGDEBUG, "GLX_EXTENSIONS:{}", m_extensions); + + if (IsExtSupported("GLX_SGI_video_sync")) + m_glXWaitVideoSyncSGI = (int (*)(int, int, unsigned int*))glXGetProcAddress((const GLubyte*)"glXWaitVideoSyncSGI"); + else + m_glXWaitVideoSyncSGI = NULL; + + if (IsExtSupported("GLX_SGI_video_sync")) + m_glXGetVideoSyncSGI = (int (*)(unsigned int*))glXGetProcAddress((const GLubyte*)"glXGetVideoSyncSGI"); + else + m_glXGetVideoSyncSGI = NULL; + + if (IsExtSupported("GLX_MESA_swap_control")) + m_glXSwapIntervalMESA = (int (*)(int))glXGetProcAddress((const GLubyte*)"glXSwapIntervalMESA"); + else + m_glXSwapIntervalMESA = NULL; + + if (IsExtSupported("GLX_EXT_swap_control")) + m_glXSwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC)glXGetProcAddress((const GLubyte*)"glXSwapIntervalEXT"); + else + m_glXSwapIntervalEXT = NULL; +} diff --git a/xbmc/windowing/X11/GLContextGLX.h b/xbmc/windowing/X11/GLContextGLX.h new file mode 100644 index 0000000..6fd41b3 --- /dev/null +++ b/xbmc/windowing/X11/GLContextGLX.h @@ -0,0 +1,49 @@ +/* + * 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 + +#include "GLContext.h" + +#include <GL/glx.h> + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CGLContextGLX : public CGLContext +{ +public: + explicit CGLContextGLX(Display *dpy); + bool Refresh(bool force, int screen, Window glWindow, bool &newContext) override; + void Destroy() override; + void Detach() override; + void SetVSync(bool enable) override; + void SwapBuffers() override; + void QueryExtensions() override; + GLXWindow m_glxWindow = 0; + GLXContext m_glxContext = 0; + +protected: + bool IsSuitableVisual(XVisualInfo *vInfo); + + int (*m_glXGetVideoSyncSGI)(unsigned int*); + int (*m_glXWaitVideoSyncSGI)(int, int, unsigned int*); + int (*m_glXSwapIntervalMESA)(int); + PFNGLXSWAPINTERVALEXTPROC m_glXSwapIntervalEXT; + int m_nScreen; + int m_iVSyncErrors; + int m_vsyncMode; +}; + +} +} +} diff --git a/xbmc/windowing/X11/OSScreenSaverX11.cpp b/xbmc/windowing/X11/OSScreenSaverX11.cpp new file mode 100644 index 0000000..3395e46 --- /dev/null +++ b/xbmc/windowing/X11/OSScreenSaverX11.cpp @@ -0,0 +1,36 @@ +/* + * 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 "OSScreenSaverX11.h" + +#include <cassert> + +using namespace std::chrono_literals; + +COSScreenSaverX11::COSScreenSaverX11(Display* dpy) +: m_dpy(dpy), m_screensaverResetTimer(std::bind(&COSScreenSaverX11::ResetScreenSaver, this)) +{ + assert(m_dpy); +} + +void COSScreenSaverX11::Inhibit() +{ + // disallow the screensaver by periodically calling XResetScreenSaver(), + // for some reason setting a 0 timeout with XSetScreenSaver doesn't work with gnome + m_screensaverResetTimer.Start(5000ms, true); +} + +void COSScreenSaverX11::Uninhibit() +{ + m_screensaverResetTimer.Stop(true); +} + +void COSScreenSaverX11::ResetScreenSaver() +{ + XResetScreenSaver(m_dpy); +} diff --git a/xbmc/windowing/X11/OSScreenSaverX11.h b/xbmc/windowing/X11/OSScreenSaverX11.h new file mode 100644 index 0000000..60c9b45 --- /dev/null +++ b/xbmc/windowing/X11/OSScreenSaverX11.h @@ -0,0 +1,28 @@ +/* + * 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 "threads/Timer.h" + +#include <X11/Xlib.h> + +class COSScreenSaverX11 : public KODI::WINDOWING::IOSScreenSaver +{ +public: + explicit COSScreenSaverX11(Display* dpy); + void Inhibit() override; + void Uninhibit() override; + +private: + void ResetScreenSaver(); + + Display* m_dpy; + CTimer m_screensaverResetTimer; +}; diff --git a/xbmc/windowing/X11/OptionalsReg.cpp b/xbmc/windowing/X11/OptionalsReg.cpp new file mode 100644 index 0000000..ecfd780 --- /dev/null +++ b/xbmc/windowing/X11/OptionalsReg.cpp @@ -0,0 +1,256 @@ +/* + * 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) +#include <va/va_x11.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 + +using namespace KODI::WINDOWING::X11; + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CWinSystemX11GLContext; + +class CVaapiProxy : public VAAPI::IVaapiWinSystem +{ +public: + CVaapiProxy() = default; + virtual ~CVaapiProxy() = default; + VADisplay GetVADisplay() override { return vaGetDisplay(dpy); }; + void *GetEGLDisplay() override { return eglDisplay; }; + + 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<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 = vaGetDisplay(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 = vaGetDisplay(winSystem->dpy); + CRendererVAAPIGLES::Register(winSystem, vaDpy, eglDpy, general, deepColor); +} +#endif +} +} +} + +#else +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CVaapiProxy +{ +}; + +CVaapiProxy* VaapiProxyCreate() +{ + return nullptr; +} + +void VaapiProxyDelete(CVaapiProxy *proxy) +{ +} + +void VaapiProxyConfig(CVaapiProxy *proxy, void *dpy, void *eglDpy) +{ +} + +void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor) +{ +} + +void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor) +{ +} + +void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor) +{ +} + +} +} +} +#endif + +//----------------------------------------------------------------------------- +// GLX +//----------------------------------------------------------------------------- + +#ifdef HAS_GLX +#include <GL/glx.h> +#include "VideoSyncGLX.h" +#include "GLContextGLX.h" + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +XID GLXGetWindow(void* context) +{ + return static_cast<CGLContextGLX*>(context)->m_glxWindow; +} + +void* GLXGetContext(void* context) +{ + return static_cast<CGLContextGLX*>(context)->m_glxContext; +} + +CGLContext* GLXContextCreate(Display *dpy) +{ + return new CGLContextGLX(dpy); +} + + +CVideoSync* GLXVideoSyncCreate(void* clock, CWinSystemX11GLContext& winSystem) +{ + return new CVideoSyncGLX(clock, winSystem); +} + +} +} +} +#else + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +XID GLXGetWindow(void* context) +{ + return 0; +} + +void* GLXGetContext(void* context) +{ + return nullptr; +} + +CGLContext* GLXContextCreate(Display *dpy) +{ + return nullptr; +} + +CVideoSync* GLXVideoSyncCreate(void* clock, CWinSystemX11GLContext& winSystem) +{ + return nullptr; +} + +} +} +} +#endif + +//----------------------------------------------------------------------------- +// VDPAU +//----------------------------------------------------------------------------- + +#if defined (HAVE_LIBVDPAU) +#include "cores/VideoPlayer/DVDCodecs/Video/VDPAU.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.h" + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +void VDPAURegisterRender() +{ + CRendererVDPAU::Register(); +} + +void VDPAURegister() +{ + VDPAU::CDecoder::Register(); +} + +} +} +} +#else + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +void VDPAURegisterRender() +{ + +} + +void VDPAURegister() +{ + +} + +} +} +} +#endif + diff --git a/xbmc/windowing/X11/OptionalsReg.h b/xbmc/windowing/X11/OptionalsReg.h new file mode 100644 index 0000000..2c4f15b --- /dev/null +++ b/xbmc/windowing/X11/OptionalsReg.h @@ -0,0 +1,78 @@ +/* + * 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 + +#include <X11/Xlib.h> + +//----------------------------------------------------------------------------- +// VAAPI +//----------------------------------------------------------------------------- + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +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 +} +} +} + +//----------------------------------------------------------------------------- +// GLX +//----------------------------------------------------------------------------- + +class CVideoSync; +class CGLContext; + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CWinSystemX11GLContext; + +XID GLXGetWindow(void* context); +void* GLXGetContext(void* context); +CGLContext* GLXContextCreate(Display *dpy); +CVideoSync* GLXVideoSyncCreate(void *clock, CWinSystemX11GLContext& winSystem); +} +} +} + +//----------------------------------------------------------------------------- +// VDPAU +//----------------------------------------------------------------------------- + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ +void VDPAURegisterRender(); +void VDPAURegister(); +} +} +} diff --git a/xbmc/windowing/X11/VideoSyncGLX.cpp b/xbmc/windowing/X11/VideoSyncGLX.cpp new file mode 100644 index 0000000..0e29a85 --- /dev/null +++ b/xbmc/windowing/X11/VideoSyncGLX.cpp @@ -0,0 +1,277 @@ +/* + * 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 "VideoSyncGLX.h" + +#include "utils/TimeUtils.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/X11/WinSystemX11GLContext.h" + +#include <mutex> +#include <sstream> + +#include <X11/extensions/Xrandr.h> + +using namespace KODI::WINDOWING::X11; + +using namespace std::chrono_literals; + +Display* CVideoSyncGLX::m_Dpy = NULL; + +void CVideoSyncGLX::OnLostDisplay() +{ + if (!m_displayLost) + { + m_displayLost = true; + m_lostEvent.Wait(); + } +} + +void CVideoSyncGLX::OnResetDisplay() +{ + m_displayReset = true; +} + +bool CVideoSyncGLX::Setup(PUPDATECLOCK func) +{ + std::unique_lock<CCriticalSection> lock(m_winSystem.GetGfxContext()); + + m_glXWaitVideoSyncSGI = NULL; + m_glXGetVideoSyncSGI = NULL; + m_vInfo = NULL; + m_Window = 0; + m_Context = NULL; + UpdateClock = func; + + int singleBufferAttributes[] = { + GLX_RGBA, + GLX_RED_SIZE, 0, + GLX_GREEN_SIZE, 0, + GLX_BLUE_SIZE, 0, + None + }; + + int ReturnV, SwaMask; + unsigned int GlxTest; + XSetWindowAttributes Swa; + + m_vInfo = NULL; + m_Context = NULL; + m_Window = 0; + + CLog::Log(LOGDEBUG, "CVideoReferenceClock: Setting up GLX"); + + static_cast<CWinSystemX11*>(&m_winSystem)->Register(this); + + m_displayLost = false; + m_displayReset = false; + m_lostEvent.Reset(); + + if (!m_Dpy) + { + m_Dpy = XOpenDisplay(NULL); + if (!m_Dpy) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: Unable to open display"); + return false; + } + } + + if (!glXQueryExtension(m_Dpy, NULL, NULL)) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: X server does not support GLX"); + return false; + } + + bool ExtensionFound = false; + std::istringstream Extensions(glXQueryExtensionsString(m_Dpy, m_winSystem.GetScreen())); + std::string ExtensionStr; + + while (!ExtensionFound) + { + Extensions >> ExtensionStr; + if (Extensions.fail()) + break; + + if (ExtensionStr == "GLX_SGI_video_sync") + ExtensionFound = true; + } + + if (!ExtensionFound) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: X server does not support GLX_SGI_video_sync"); + return false; + } + + m_vInfo = glXChooseVisual(m_Dpy, m_winSystem.GetScreen(), singleBufferAttributes); + if (!m_vInfo) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXChooseVisual returned NULL"); + return false; + } + + Swa.border_pixel = 0; + Swa.event_mask = StructureNotifyMask; + Swa.colormap = XCreateColormap(m_Dpy, m_winSystem.GetWindow(), m_vInfo->visual, AllocNone ); + SwaMask = CWBorderPixel | CWColormap | CWEventMask; + + m_Window = XCreateWindow(m_Dpy, m_winSystem.GetWindow(), 0, 0, 256, 256, 0, + m_vInfo->depth, InputOutput, m_vInfo->visual, SwaMask, &Swa); + + m_Context = glXCreateContext(m_Dpy, m_vInfo, NULL, True); + if (!m_Context) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXCreateContext returned NULL"); + return false; + } + + ReturnV = glXMakeCurrent(m_Dpy, m_Window, m_Context); + if (ReturnV != True) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXMakeCurrent returned {}", ReturnV); + return false; + } + + m_glXWaitVideoSyncSGI = (int (*)(int, int, unsigned int*))glXGetProcAddress((const GLubyte*)"glXWaitVideoSyncSGI"); + if (!m_glXWaitVideoSyncSGI) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXWaitVideoSyncSGI not found"); + return false; + } + + ReturnV = m_glXWaitVideoSyncSGI(2, 0, &GlxTest); + if (ReturnV) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXWaitVideoSyncSGI returned {}", ReturnV); + return false; + } + + m_glXGetVideoSyncSGI = (int (*)(unsigned int*))glXGetProcAddress((const GLubyte*)"glXGetVideoSyncSGI"); + if (!m_glXGetVideoSyncSGI) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXGetVideoSyncSGI not found"); + return false; + } + + ReturnV = m_glXGetVideoSyncSGI(&GlxTest); + if (ReturnV) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXGetVideoSyncSGI returned {}", ReturnV); + return false; + } + + return true; +} + +void CVideoSyncGLX::Run(CEvent& stopEvent) +{ + unsigned int PrevVblankCount; + unsigned int VblankCount; + int ReturnV; + bool IsReset = false; + int64_t Now; + + //get the current vblank counter + m_glXGetVideoSyncSGI(&VblankCount); + PrevVblankCount = VblankCount; + + while(!stopEvent.Signaled() && !m_displayLost && !m_displayReset) + { + //wait for the next vblank + ReturnV = m_glXWaitVideoSyncSGI(2, (VblankCount + 1) % 2, &VblankCount); + m_glXGetVideoSyncSGI(&VblankCount); //the vblank count returned by glXWaitVideoSyncSGI is not always correct + Now = CurrentHostCounter(); //get the timestamp of this vblank + + if(ReturnV) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXWaitVideoSyncSGI returned {}", ReturnV); + return; + } + + if (VblankCount > PrevVblankCount) + { + UpdateClock((int)(VblankCount - PrevVblankCount), Now, m_refClock); + IsReset = false; + } + else + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: Vblank counter has reset"); + + //only try reattaching once + if (IsReset) + return; + + //because of a bug in the nvidia driver, glXWaitVideoSyncSGI breaks when the vblank counter resets + CLog::Log(LOGDEBUG, "CVideoReferenceClock: Detaching glX context"); + ReturnV = glXMakeCurrent(m_Dpy, None, NULL); + if (ReturnV != True) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXMakeCurrent returned {}", ReturnV); + return; + } + + //sleep here so we don't busy spin when this constantly happens, for example when the display went to sleep + KODI::TIME::Sleep(1s); + + CLog::Log(LOGDEBUG, "CVideoReferenceClock: Attaching glX context"); + ReturnV = glXMakeCurrent(m_Dpy, m_Window, m_Context); + if (ReturnV != True) + { + CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXMakeCurrent returned {}", ReturnV); + return; + } + + m_glXGetVideoSyncSGI(&VblankCount); + + IsReset = true; + } + PrevVblankCount = VblankCount; + } + m_lostEvent.Set(); + while(!stopEvent.Signaled() && m_displayLost && !m_displayReset) + { + KODI::TIME::Sleep(10ms); + } +} + +void CVideoSyncGLX::Cleanup() +{ + CLog::Log(LOGDEBUG, "CVideoReferenceClock: Cleaning up GLX"); + + { + std::unique_lock<CCriticalSection> lock(m_winSystem.GetGfxContext()); + + if (m_vInfo) + { + XFree(m_vInfo); + m_vInfo = NULL; + } + if (m_Context) + { + glXMakeCurrent(m_Dpy, None, NULL); + glXDestroyContext(m_Dpy, m_Context); + m_Context = NULL; + } + if (m_Window) + { + XDestroyWindow(m_Dpy, m_Window); + m_Window = 0; + } + } + + m_lostEvent.Set(); + m_winSystem.Unregister(this); +} + +float CVideoSyncGLX::GetFps() +{ + m_fps = m_winSystem.GetGfxContext().GetFPS(); + return m_fps; +} diff --git a/xbmc/windowing/X11/VideoSyncGLX.h b/xbmc/windowing/X11/VideoSyncGLX.h new file mode 100644 index 0000000..06d348e --- /dev/null +++ b/xbmc/windowing/X11/VideoSyncGLX.h @@ -0,0 +1,62 @@ +/* + * 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 + +#include "guilib/DispResource.h" +#include "threads/Event.h" +#include "windowing/VideoSync.h" + +#include <GL/glx.h> +#include <X11/X.h> +#include <X11/Xlib.h> + +#include "system_gl.h" + + + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CWinSystemX11GLContext; + +class CVideoSyncGLX : public CVideoSync, IDispResource +{ +public: + explicit CVideoSyncGLX(void* clock, CWinSystemX11GLContext& winSystem) + : CVideoSync(clock), m_winSystem(winSystem) + { + } + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stopEvent) override; + void Cleanup() override; + float GetFps() override; + void OnLostDisplay() override; + void OnResetDisplay() override; + +private: + int (*m_glXWaitVideoSyncSGI) (int, int, unsigned int*); + int (*m_glXGetVideoSyncSGI) (unsigned int*); + + static Display* m_Dpy; + CWinSystemX11GLContext &m_winSystem; + XVisualInfo *m_vInfo; + Window m_Window; + GLXContext m_Context; + volatile bool m_displayLost; + volatile bool m_displayReset; + CEvent m_lostEvent; +}; + +} +} +} diff --git a/xbmc/windowing/X11/VideoSyncOML.cpp b/xbmc/windowing/X11/VideoSyncOML.cpp new file mode 100644 index 0000000..2dc91fe --- /dev/null +++ b/xbmc/windowing/X11/VideoSyncOML.cpp @@ -0,0 +1,83 @@ +/* + * 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 "VideoSyncOML.h" + +#include "utils/TimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/X11/WinSystemX11GLContext.h" + +#include <unistd.h> + +using namespace KODI::WINDOWING::X11; + +bool CVideoSyncOML::Setup(PUPDATECLOCK func) +{ + CLog::Log(LOGDEBUG, "CVideoSyncOML::{} - setting up OML", __FUNCTION__); + + UpdateClock = func; + + m_abort = false; + + static_cast<CWinSystemX11*>(&m_winSystem)->Register(this); + + return true; +} + +void CVideoSyncOML::Run(CEvent& stopEvent) +{ + uint64_t interval, timeSinceVblank, msc; + + timeSinceVblank = m_winSystem.GetVblankTiming(msc, interval); + + while (!stopEvent.Signaled() && !m_abort) + { + if (interval == 0) + { + usleep(10000); + } + else + { + usleep(interval - timeSinceVblank + 1000); + } + uint64_t newMsc; + timeSinceVblank = m_winSystem.GetVblankTiming(newMsc, interval); + + if (newMsc == msc) + { + newMsc++; + } + else if (newMsc < msc) + { + timeSinceVblank = interval; + continue; + } + + uint64_t now = CurrentHostCounter(); + UpdateClock(newMsc - msc, now, m_refClock); + msc = newMsc; + } +} + +void CVideoSyncOML::Cleanup() +{ + m_winSystem.Unregister(this); +} + +void CVideoSyncOML::OnResetDisplay() +{ + m_abort = true; +} + +float CVideoSyncOML::GetFps() +{ + m_fps = m_winSystem.GetGfxContext().GetFPS(); + return m_fps; +} + diff --git a/xbmc/windowing/X11/VideoSyncOML.h b/xbmc/windowing/X11/VideoSyncOML.h new file mode 100644 index 0000000..a04bd1d --- /dev/null +++ b/xbmc/windowing/X11/VideoSyncOML.h @@ -0,0 +1,46 @@ +/* + * 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 + +#include "guilib/DispResource.h" +#include "windowing/VideoSync.h" + +#include <atomic> + + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CWinSystemX11GLContext; + +class CVideoSyncOML : public CVideoSync, IDispResource +{ +public: + explicit CVideoSyncOML(void* clock, CWinSystemX11GLContext& winSystem) + : CVideoSync(clock), m_winSystem(winSystem) + { + } + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stopEvent) override; + void Cleanup() override; + float GetFps() override; + void OnResetDisplay() override; + +private: + std::atomic_bool m_abort; + CWinSystemX11GLContext &m_winSystem; +}; + +} +} +} diff --git a/xbmc/windowing/X11/WinEventsX11.cpp b/xbmc/windowing/X11/WinEventsX11.cpp new file mode 100644 index 0000000..faffd99 --- /dev/null +++ b/xbmc/windowing/X11/WinEventsX11.cpp @@ -0,0 +1,673 @@ +/* + * 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 "WinEventsX11.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/Application.h" +#include "cores/AudioEngine/Interfaces/AE.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "input/InputManager.h" +#include "input/mouse/MouseStat.h" +#include "messaging/ApplicationMessenger.h" +#include "utils/CharsetConverter.h" +#include "utils/log.h" +#include "windowing/WinEvents.h" +#include "windowing/X11/WinSystemX11.h" + +#include <stdexcept> + +#include <X11/XF86keysym.h> +#include <X11/Xlib.h> +#include <X11/extensions/Xrandr.h> +#include <X11/keysymdef.h> + +using namespace KODI::WINDOWING::X11; + +static uint32_t SymMappingsX11[][2] = +{ + {XK_BackSpace, XBMCK_BACKSPACE} +, {XK_Tab, XBMCK_TAB} +, {XK_Clear, XBMCK_CLEAR} +, {XK_Return, XBMCK_RETURN} +, {XK_Pause, XBMCK_PAUSE} +, {XK_Escape, XBMCK_ESCAPE} +, {XK_Delete, XBMCK_DELETE} +// multi-media keys +, {XF86XK_Back, XBMCK_BROWSER_BACK} +, {XF86XK_Forward, XBMCK_BROWSER_FORWARD} +, {XF86XK_Refresh, XBMCK_BROWSER_REFRESH} +, {XF86XK_Stop, XBMCK_BROWSER_STOP} +, {XF86XK_Search, XBMCK_BROWSER_SEARCH} +, {XF86XK_Favorites, XBMCK_BROWSER_FAVORITES} +, {XF86XK_HomePage, XBMCK_BROWSER_HOME} +, {XF86XK_AudioMute, XBMCK_VOLUME_MUTE} +, {XF86XK_AudioLowerVolume, XBMCK_VOLUME_DOWN} +, {XF86XK_AudioRaiseVolume, XBMCK_VOLUME_UP} +, {XF86XK_AudioNext, XBMCK_MEDIA_NEXT_TRACK} +, {XF86XK_AudioPrev, XBMCK_MEDIA_PREV_TRACK} +, {XF86XK_AudioStop, XBMCK_MEDIA_STOP} +, {XF86XK_AudioPause, XBMCK_MEDIA_PLAY_PAUSE} +, {XF86XK_Mail, XBMCK_LAUNCH_MAIL} +, {XF86XK_Select, XBMCK_LAUNCH_MEDIA_SELECT} +, {XF86XK_Launch0, XBMCK_LAUNCH_APP1} +, {XF86XK_Launch1, XBMCK_LAUNCH_APP2} +, {XF86XK_WWW, XBMCK_LAUNCH_FILE_BROWSER} +, {XF86XK_AudioMedia, XBMCK_LAUNCH_MEDIA_CENTER } + // Numeric keypad +, {XK_KP_0, XBMCK_KP0} +, {XK_KP_1, XBMCK_KP1} +, {XK_KP_2, XBMCK_KP2} +, {XK_KP_3, XBMCK_KP3} +, {XK_KP_4, XBMCK_KP4} +, {XK_KP_5, XBMCK_KP5} +, {XK_KP_6, XBMCK_KP6} +, {XK_KP_7, XBMCK_KP7} +, {XK_KP_8, XBMCK_KP8} +, {XK_KP_9, XBMCK_KP9} +, {XK_KP_Separator, XBMCK_KP_PERIOD} +, {XK_KP_Divide, XBMCK_KP_DIVIDE} +, {XK_KP_Multiply, XBMCK_KP_MULTIPLY} +, {XK_KP_Subtract, XBMCK_KP_MINUS} +, {XK_KP_Add, XBMCK_KP_PLUS} +, {XK_KP_Enter, XBMCK_KP_ENTER} +, {XK_KP_Equal, XBMCK_KP_EQUALS} + // Arrows + Home/End pad +, {XK_Up, XBMCK_UP} +, {XK_Down, XBMCK_DOWN} +, {XK_Right, XBMCK_RIGHT} +, {XK_Left, XBMCK_LEFT} +, {XK_Insert, XBMCK_INSERT} +, {XK_Home, XBMCK_HOME} +, {XK_End, XBMCK_END} +, {XK_Page_Up, XBMCK_PAGEUP} +, {XK_Page_Down, XBMCK_PAGEDOWN} + // Function keys +, {XK_F1, XBMCK_F1} +, {XK_F2, XBMCK_F2} +, {XK_F3, XBMCK_F3} +, {XK_F4, XBMCK_F4} +, {XK_F5, XBMCK_F5} +, {XK_F6, XBMCK_F6} +, {XK_F7, XBMCK_F7} +, {XK_F8, XBMCK_F8} +, {XK_F9, XBMCK_F9} +, {XK_F10, XBMCK_F10} +, {XK_F11, XBMCK_F11} +, {XK_F12, XBMCK_F12} +, {XK_F13, XBMCK_F13} +, {XK_F14, XBMCK_F14} +, {XK_F15, XBMCK_F15} + // Key state modifier keys +, {XK_Num_Lock, XBMCK_NUMLOCK} +, {XK_Caps_Lock, XBMCK_CAPSLOCK} +, {XK_Scroll_Lock, XBMCK_SCROLLOCK} +, {XK_Shift_R, XBMCK_RSHIFT} +, {XK_Shift_L, XBMCK_LSHIFT} +, {XK_Control_R, XBMCK_RCTRL} +, {XK_Control_L, XBMCK_LCTRL} +, {XK_Alt_R, XBMCK_RALT} +, {XK_Alt_L, XBMCK_LALT} +, {XK_Meta_R, XBMCK_RMETA} +, {XK_Meta_L, XBMCK_LMETA} +, {XK_Super_L, XBMCK_LSUPER} +, {XK_Super_R, XBMCK_RSUPER} +, {XK_Mode_switch, XBMCK_MODE} +, {XK_Multi_key, XBMCK_COMPOSE} + // Miscellaneous function keys +, {XK_Help, XBMCK_HELP} +, {XK_Print, XBMCK_PRINT} +//, {0, XBMCK_SYSREQ} +, {XK_Break, XBMCK_BREAK} +, {XK_Menu, XBMCK_MENU} +, {XF86XK_PowerOff, XBMCK_POWER} +, {XF86XK_Sleep, XBMCK_SLEEP} +, {XK_EcuSign, XBMCK_EURO} +, {XK_Undo, XBMCK_UNDO} + /* Media keys */ +, {XF86XK_Eject, XBMCK_EJECT} +, {XF86XK_Stop, XBMCK_STOP} +, {XF86XK_AudioRecord, XBMCK_RECORD} +, {XF86XK_AudioRewind, XBMCK_REWIND} +, {XF86XK_Phone, XBMCK_PHONE} +, {XF86XK_AudioPlay, XBMCK_PLAY} +, {XF86XK_AudioRandomPlay, XBMCK_SHUFFLE} +, {XF86XK_AudioForward, XBMCK_FASTFORWARD} +}; + +CWinEventsX11::CWinEventsX11(CWinSystemX11& winSystem) : m_winSystem(winSystem) +{ +} + +CWinEventsX11::~CWinEventsX11() +{ + Quit(); +} + +bool CWinEventsX11::Init(Display *dpy, Window win) +{ + if (m_display) + return true; + + m_display = dpy; + m_window = win; + m_keybuf_len = 32*sizeof(char); + m_keybuf = (char*)malloc(m_keybuf_len); + m_keymodState = 0; + m_wmDeleteMessage = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + m_structureChanged = false; + m_xrrEventPending = false; + + // open input method + char *old_locale = NULL, *old_modifiers = NULL; + char res_name[8]; + const char *p; + + // set resource name to xbmc, not used + strcpy(res_name, "xbmc"); + + // save current locale, this should be "C" + p = setlocale(LC_ALL, NULL); + if (p) + { + old_locale = (char*)malloc(strlen(p) +1); + strcpy(old_locale, p); + } + p = XSetLocaleModifiers(NULL); + if (p) + { + old_modifiers = (char*)malloc(strlen(p) +1); + strcpy(old_modifiers, p); + } + + // set users preferences and open input method + p = setlocale(LC_ALL, ""); + XSetLocaleModifiers(""); + m_xim = XOpenIM(m_display, NULL, res_name, res_name); + + // restore old locale + if (old_locale) + { + setlocale(LC_ALL, old_locale); + free(old_locale); + } + if (old_modifiers) + { + XSetLocaleModifiers(old_modifiers); + free(old_modifiers); + } + + m_xic = NULL; + if (m_xim) + { + m_xic = XCreateIC(m_xim, + XNClientWindow, m_window, + XNFocusWindow, m_window, + XNInputStyle, XIMPreeditNothing | XIMStatusNothing, + XNResourceName, res_name, + XNResourceClass, res_name, + nullptr); + } + + if (!m_xic) + CLog::Log(LOGWARNING,"CWinEventsX11::Init - no input method found"); + + // build Keysym lookup table + for (const auto& symMapping : SymMappingsX11) + { + m_symLookupTable[symMapping[0]] = symMapping[1]; + } + + // register for xrandr events + int iReturn; + XRRQueryExtension(m_display, &m_RREventBase, &iReturn); + int numScreens = XScreenCount(m_display); + for (int i = 0; i < numScreens; i++) + { + XRRSelectInput(m_display, RootWindow(m_display, i), RRScreenChangeNotifyMask | RRCrtcChangeNotifyMask | RROutputChangeNotifyMask | RROutputPropertyNotifyMask); + } + + return true; +} + +void CWinEventsX11::Quit() +{ + free(m_keybuf); + m_keybuf = nullptr; + + if (m_xic) + { + XUnsetICFocus(m_xic); + XDestroyIC(m_xic); + m_xic = nullptr; + } + + if (m_xim) + { + XCloseIM(m_xim); + m_xim = nullptr; + } + + m_symLookupTable.clear(); + + m_display = nullptr; +} + +bool CWinEventsX11::HasStructureChanged() +{ + if (!m_display) + return false; + + bool ret = m_structureChanged; + m_structureChanged = false; + return ret; +} + +void CWinEventsX11::SetXRRFailSafeTimer(std::chrono::milliseconds duration) +{ + if (!m_display) + return; + + m_xrrFailSafeTimer.Set(duration); + m_xrrEventPending = true; +} + +bool CWinEventsX11::MessagePump() +{ + if (!m_display) + return false; + + bool ret = false; + XEvent xevent; + unsigned long serial = 0; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + + while (m_display && XPending(m_display)) + { + memset(&xevent, 0, sizeof (XEvent)); + XNextEvent(m_display, &xevent); + + if (m_display && (xevent.type == m_RREventBase + RRScreenChangeNotify)) + { + if (xevent.xgeneric.serial == serial) + continue; + + if (m_xrrEventPending) + { + m_winSystem.NotifyXRREvent(); + m_xrrEventPending = false; + serial = xevent.xgeneric.serial; + } + + continue; + } + else if (m_display && (xevent.type == m_RREventBase + RRNotify)) + { + if (xevent.xgeneric.serial == serial) + continue; + + XRRNotifyEvent* rrEvent = reinterpret_cast<XRRNotifyEvent*>(&xevent); + if (rrEvent->subtype == RRNotify_OutputChange) + { + XRROutputChangeNotifyEvent* changeEvent = reinterpret_cast<XRROutputChangeNotifyEvent*>(&xevent); + if (changeEvent->connection == RR_Connected || + changeEvent->connection == RR_Disconnected) + { + m_winSystem.NotifyXRREvent(); + CServiceBroker::GetActiveAE()->DeviceChange(); + serial = xevent.xgeneric.serial; + } + } + + continue; + } + + if (XFilterEvent(&xevent, m_window)) + continue; + + switch (xevent.type) + { + case MapNotify: + { + if (appPort) + appPort->SetRenderGUI(true); + break; + } + + case UnmapNotify: + { + if (appPort) + appPort->SetRenderGUI(false); + break; + } + + case FocusIn: + { + if (m_xic) + XSetICFocus(m_xic); + g_application.m_AppFocused = true; + m_keymodState = 0; + if (serial == xevent.xfocus.serial) + break; + m_winSystem.NotifyAppFocusChange(g_application.m_AppFocused); + break; + } + + case FocusOut: + { + if (m_xic) + XUnsetICFocus(m_xic); + g_application.m_AppFocused = false; + m_winSystem.NotifyAppFocusChange(g_application.m_AppFocused); + serial = xevent.xfocus.serial; + break; + } + + case Expose: + { + CServiceBroker::GetGUI()->GetWindowManager().MarkDirty(); + break; + } + + case ConfigureNotify: + { + if (xevent.xconfigure.window != m_window) + break; + + m_structureChanged = true; + XBMC_Event newEvent = {}; + newEvent.type = XBMC_VIDEORESIZE; + newEvent.resize.w = xevent.xconfigure.width; + newEvent.resize.h = xevent.xconfigure.height; + if (appPort) + ret |= appPort->OnEvent(newEvent); + CServiceBroker::GetGUI()->GetWindowManager().MarkDirty(); + break; + } + + case ClientMessage: + { + if ((unsigned int)xevent.xclient.data.l[0] == m_wmDeleteMessage) + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + break; + } + + case KeyPress: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_KEYDOWN; + KeySym xkeysym; + + // fallback if we have no IM + if (!m_xic) + { + static XComposeStatus state; + char keybuf[32]; + XLookupString(&xevent.xkey, NULL, 0, &xkeysym, NULL); + newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym); + newEvent.key.keysym.scancode = xevent.xkey.keycode; + if (XLookupString(&xevent.xkey, keybuf, sizeof(keybuf), NULL, &state)) + { + newEvent.key.keysym.unicode = keybuf[0]; + } + ret |= ProcessKey(newEvent); + break; + } + + Status status; + int len; + len = Xutf8LookupString(m_xic, &xevent.xkey, + m_keybuf, m_keybuf_len, + &xkeysym, &status); + if (status == XBufferOverflow) + { + m_keybuf_len = len; + m_keybuf = (char*)realloc(m_keybuf, m_keybuf_len); + if (m_keybuf == nullptr) + throw std::runtime_error("Failed to realloc memory, insufficient memory available"); + len = Xutf8LookupString(m_xic, &xevent.xkey, + m_keybuf, m_keybuf_len, + &xkeysym, &status); + } + switch (status) + { + case XLookupNone: + break; + case XLookupChars: + case XLookupBoth: + { + std::string data(m_keybuf, len); + std::wstring keys; + g_charsetConverter.utf8ToW(data, keys, false); + + if (keys.length() == 0) + { + break; + } + + for (unsigned int i = 0; i < keys.length() - 1; i++) + { + newEvent.key.keysym.sym = XBMCK_UNKNOWN; + newEvent.key.keysym.unicode = keys[i]; + ret |= ProcessKey(newEvent); + } + if (keys.length() > 0) + { + newEvent.key.keysym.scancode = xevent.xkey.keycode; + XLookupString(&xevent.xkey, NULL, 0, &xkeysym, NULL); + newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym); + newEvent.key.keysym.unicode = keys[keys.length() - 1]; + + ret |= ProcessKey(newEvent); + } + break; + } + + case XLookupKeySym: + { + newEvent.key.keysym.scancode = xevent.xkey.keycode; + newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym); + ret |= ProcessKey(newEvent); + break; + } + + }// switch status + break; + } //KeyPress + + case KeyRelease: + { + // if we have a queued press directly after, this is a repeat + if (XEventsQueued(m_display, QueuedAfterReading)) + { + XEvent next_event; + XPeekEvent(m_display, &next_event); + if (next_event.type == KeyPress && + next_event.xkey.window == xevent.xkey.window && + next_event.xkey.keycode == xevent.xkey.keycode && + (next_event.xkey.time - xevent.xkey.time < 2)) + continue; + } + + XBMC_Event newEvent = {}; + KeySym xkeysym; + newEvent.type = XBMC_KEYUP; + xkeysym = XLookupKeysym(&xevent.xkey, 0); + newEvent.key.keysym.scancode = xevent.xkey.keycode; + newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym); + ret |= ProcessKey(newEvent); + break; + } + + case EnterNotify: + { + break; + } + + // lose mouse coverage + case LeaveNotify: + { + CServiceBroker::GetInputManager().SetMouseActive(false); + break; + } + + case MotionNotify: + { + if (xevent.xmotion.window != m_window) + break; + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEMOTION; + newEvent.motion.x = (int16_t)xevent.xmotion.x; + newEvent.motion.y = (int16_t)xevent.xmotion.y; + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case ButtonPress: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = (unsigned char)xevent.xbutton.button; + newEvent.button.x = (int16_t)xevent.xbutton.x; + newEvent.button.y = (int16_t)xevent.xbutton.y; + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case ButtonRelease: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = (unsigned char)xevent.xbutton.button; + newEvent.button.x = (int16_t)xevent.xbutton.x; + newEvent.button.y = (int16_t)xevent.xbutton.y; + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + default: + { + break; + } + }// switch event.type + }// while + + if (m_display && m_xrrEventPending && m_xrrFailSafeTimer.IsTimePast()) + { + CLog::Log(LOGERROR,"CWinEventsX11::MessagePump - missed XRR Events"); + m_winSystem.NotifyXRREvent(); + m_xrrEventPending = false; + } + + return ret; +} + +bool CWinEventsX11::ProcessKey(XBMC_Event &event) +{ + if (event.type == XBMC_KEYDOWN) + { + // check key modifiers + switch(event.key.keysym.sym) + { + case XBMCK_LSHIFT: + m_keymodState |= XBMCKMOD_LSHIFT; + break; + case XBMCK_RSHIFT: + m_keymodState |= XBMCKMOD_RSHIFT; + break; + case XBMCK_LCTRL: + m_keymodState |= XBMCKMOD_LCTRL; + break; + case XBMCK_RCTRL: + m_keymodState |= XBMCKMOD_RCTRL; + break; + case XBMCK_LALT: + m_keymodState |= XBMCKMOD_LALT; + break; + case XBMCK_RALT: + m_keymodState |= XBMCKMOD_RCTRL; + break; + case XBMCK_LMETA: + m_keymodState |= XBMCKMOD_LMETA; + break; + case XBMCK_RMETA: + m_keymodState |= XBMCKMOD_RMETA; + break; + case XBMCK_MODE: + m_keymodState |= XBMCKMOD_MODE; + break; + default: + break; + } + event.key.keysym.mod = (XBMCMod)m_keymodState; + } + else if (event.type == XBMC_KEYUP) + { + switch(event.key.keysym.sym) + { + case XBMCK_LSHIFT: + m_keymodState &= ~XBMCKMOD_LSHIFT; + break; + case XBMCK_RSHIFT: + m_keymodState &= ~XBMCKMOD_RSHIFT; + break; + case XBMCK_LCTRL: + m_keymodState &= ~XBMCKMOD_LCTRL; + break; + case XBMCK_RCTRL: + m_keymodState &= ~XBMCKMOD_RCTRL; + break; + case XBMCK_LALT: + m_keymodState &= ~XBMCKMOD_LALT; + break; + case XBMCK_RALT: + m_keymodState &= ~XBMCKMOD_RCTRL; + break; + case XBMCK_LMETA: + m_keymodState &= ~XBMCKMOD_LMETA; + break; + case XBMCK_RMETA: + m_keymodState &= ~XBMCKMOD_RMETA; + break; + case XBMCK_MODE: + m_keymodState &= ~XBMCKMOD_MODE; + break; + default: + break; + } + event.key.keysym.mod = (XBMCMod)m_keymodState; + } + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(event); + return true; +} + +XBMCKey CWinEventsX11::LookupXbmcKeySym(KeySym keysym) +{ + // try direct mapping first + std::map<uint32_t, uint32_t>::iterator it; + it = m_symLookupTable.find(keysym); + if (it != m_symLookupTable.end()) + { + return (XBMCKey)(it->second); + } + + // try ascii mappings + if (keysym>>8 == 0x00) + return (XBMCKey)tolower(keysym & 0xFF); + + return (XBMCKey)keysym; +} diff --git a/xbmc/windowing/X11/WinEventsX11.h b/xbmc/windowing/X11/WinEventsX11.h new file mode 100644 index 0000000..b2111da --- /dev/null +++ b/xbmc/windowing/X11/WinEventsX11.h @@ -0,0 +1,60 @@ +/* + * 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 + +#include "threads/SystemClock.h" +#include "windowing/WinEvents.h" +#include "windowing/X11/WinSystemX11.h" + +#include <clocale> +#include <map> + +#include <X11/Xlib.h> + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CWinEventsX11 : public IWinEvents +{ +public: + CWinEventsX11(CWinSystemX11& winSystem); + ~CWinEventsX11() override; + bool MessagePump() override; + bool Init(Display *dpy, Window win); + void Quit(); + bool HasStructureChanged(); + void PendingResize(int width, int height); + void SetXRRFailSafeTimer(std::chrono::milliseconds duration); + +protected: + XBMCKey LookupXbmcKeySym(KeySym keysym); + bool ProcessKey(XBMC_Event &event); + Display *m_display = nullptr; + Window m_window = 0; + Atom m_wmDeleteMessage; + char *m_keybuf = nullptr; + size_t m_keybuf_len = 0; + XIM m_xim = nullptr; + XIC m_xic = nullptr; + std::map<uint32_t,uint32_t> m_symLookupTable; + int m_keymodState; + bool m_structureChanged; + int m_RREventBase; + XbmcThreads::EndTime<> m_xrrFailSafeTimer; + bool m_xrrEventPending; + CWinSystemX11& m_winSystem; +}; + +} +} +} diff --git a/xbmc/windowing/X11/WinSystemX11.cpp b/xbmc/windowing/X11/WinSystemX11.cpp new file mode 100644 index 0000000..43a8ebb --- /dev/null +++ b/xbmc/windowing/X11/WinSystemX11.cpp @@ -0,0 +1,1081 @@ +/* + * 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 "WinSystemX11.h" + +#include "CompileInfo.h" +#include "OSScreenSaverX11.h" +#include "ServiceBroker.h" +#include "WinEventsX11.h" +#include "XRandR.h" +#include "guilib/DispResource.h" +#include "guilib/Texture.h" +#include "input/InputManager.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "utils/StringUtils.h" +#include "utils/TimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +#include <mutex> +#include <string> +#include <vector> + +#include <X11/Xatom.h> +#include <X11/extensions/Xrandr.h> + +using namespace KODI::WINDOWING::X11; + +using namespace std::chrono_literals; + +#define EGL_NO_CONFIG (EGLConfig)0 + +CWinSystemX11::CWinSystemX11() : CWinSystemBase() +{ + m_dpy = NULL; + m_bWasFullScreenBeforeMinimize = false; + m_minimized = false; + m_bIgnoreNextFocusMessage = false; + m_bIsInternalXrr = false; + m_delayDispReset = false; + + XSetErrorHandler(XErrorHandler); + + m_winEventsX11 = new CWinEventsX11(*this); + m_winEvents.reset(m_winEventsX11); +} + +CWinSystemX11::~CWinSystemX11() = default; + +bool CWinSystemX11::InitWindowSystem() +{ + const char* env = getenv("DISPLAY"); + if (!env) + { + CLog::Log(LOGDEBUG, "CWinSystemX11::{} - DISPLAY env not set", __FUNCTION__); + return false; + } + + if ((m_dpy = XOpenDisplay(NULL))) + { + bool ret = CWinSystemBase::InitWindowSystem(); + + CServiceBroker::GetSettingsComponent() + ->GetSettings() + ->GetSetting(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE) + ->SetVisible(true); + + return ret; + } + else + CLog::Log(LOGERROR, "X11 Error: No Display found"); + + return false; +} + +bool CWinSystemX11::DestroyWindowSystem() +{ + //restore desktop resolution on exit + if (m_bFullScreen) + { + XOutput out; + XMode mode; + out.name = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strOutput; + mode.w = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iWidth; + mode.h = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iHeight; + mode.hz = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).fRefreshRate; + mode.id = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strId; + g_xrandr.SetMode(out, mode); + } + + return true; +} + +bool CWinSystemX11::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + if(!SetFullScreen(fullScreen, res, false)) + return false; + + m_bWindowCreated = true; + return true; +} + +bool CWinSystemX11::DestroyWindow() +{ + if (!m_mainWindow) + return true; + + if (m_invisibleCursor) + { + XUndefineCursor(m_dpy, m_mainWindow); + XFreeCursor(m_dpy, m_invisibleCursor); + m_invisibleCursor = 0; + } + + m_winEventsX11->Quit(); + + XUnmapWindow(m_dpy, m_mainWindow); + XDestroyWindow(m_dpy, m_glWindow); + XDestroyWindow(m_dpy, m_mainWindow); + m_glWindow = 0; + m_mainWindow = 0; + + if (m_icon) + XFreePixmap(m_dpy, m_icon); + + return true; +} + +bool CWinSystemX11::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + m_userOutput = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR); + XOutput *out = NULL; + if (m_userOutput.compare("Default") != 0) + { + out = g_xrandr.GetOutput(m_userOutput); + if (out) + { + XMode mode = g_xrandr.GetCurrentMode(m_userOutput); + if (!mode.isCurrent) + { + out = NULL; + } + } + } + if (!out) + { + std::vector<XOutput> outputs = g_xrandr.GetModes(); + if (!outputs.empty()) + { + m_userOutput = outputs[0].name; + } + } + + if (!SetWindow(newWidth, newHeight, false, m_userOutput)) + { + return false; + } + + m_nWidth = newWidth; + m_nHeight = newHeight; + m_bFullScreen = false; + m_currentOutput = m_userOutput; + + return true; +} + +void CWinSystemX11::FinishWindowResize(int newWidth, int newHeight) +{ + m_userOutput = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR); + XOutput *out = NULL; + if (m_userOutput.compare("Default") != 0) + { + out = g_xrandr.GetOutput(m_userOutput); + if (out) + { + XMode mode = g_xrandr.GetCurrentMode(m_userOutput); + if (!mode.isCurrent) + { + out = NULL; + } + } + } + if (!out) + { + std::vector<XOutput> outputs = g_xrandr.GetModes(); + if (!outputs.empty()) + { + m_userOutput = outputs[0].name; + } + } + + XResizeWindow(m_dpy, m_glWindow, newWidth, newHeight); + UpdateCrtc(); + + if (m_userOutput.compare(m_currentOutput) != 0) + { + SetWindow(newWidth, newHeight, false, m_userOutput); + } + + m_nWidth = newWidth; + m_nHeight = newHeight; +} + +bool CWinSystemX11::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + XOutput out; + XMode mode; + + if (fullScreen) + { + out.name = res.strOutput; + mode.w = res.iWidth; + mode.h = res.iHeight; + mode.hz = res.fRefreshRate; + mode.id = res.strId; + } + else + { + out.name = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strOutput; + mode.w = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iWidth; + mode.h = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iHeight; + mode.hz = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).fRefreshRate; + mode.id = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strId; + } + + XMode currmode = g_xrandr.GetCurrentMode(out.name); + if (!currmode.name.empty()) + { + // flip h/w when rotated + if (m_bIsRotated) + { + int w = mode.w; + mode.w = mode.h; + mode.h = w; + } + + // only call xrandr if mode changes + if (m_mainWindow) + { + if (currmode.w != mode.w || currmode.h != mode.h || + currmode.hz != mode.hz || currmode.id != mode.id) + { + CLog::Log(LOGINFO, "CWinSystemX11::SetFullScreen - calling xrandr"); + + // remember last position of mouse + Window root_return, child_return; + int root_x_return, root_y_return; + int win_x_return, win_y_return; + unsigned int mask_return; + bool isInWin = XQueryPointer(m_dpy, m_mainWindow, &root_return, &child_return, + &root_x_return, &root_y_return, + &win_x_return, &win_y_return, + &mask_return); + + if (isInWin) + { + m_MouseX = win_x_return; + m_MouseY = win_y_return; + } + else + { + m_MouseX = -1; + m_MouseY = -1; + } + + OnLostDevice(); + m_bIsInternalXrr = true; + g_xrandr.SetMode(out, mode); + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + if (delay > 0ms) + { + m_delayDispReset = true; + m_dispResetTimer.Set(delay); + } + return true; + } + } + } + + if (!SetWindow(res.iWidth, res.iHeight, fullScreen, m_userOutput)) + return false; + + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + m_currentOutput = m_userOutput; + + return true; +} + +void CWinSystemX11::UpdateResolutions() +{ + CWinSystemBase::UpdateResolutions(); + + int numScreens = XScreenCount(m_dpy); + g_xrandr.SetNumScreens(numScreens); + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + bool switchOnOff = settings->GetBool(CSettings::SETTING_VIDEOSCREEN_BLANKDISPLAYS); + m_userOutput = settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR); + if (m_userOutput.compare("Default") == 0) + switchOnOff = false; + + if(g_xrandr.Query(true, !switchOnOff)) + { + XOutput *out = NULL; + if (m_userOutput.compare("Default") != 0) + { + out = g_xrandr.GetOutput(m_userOutput); + if (out) + { + XMode mode = g_xrandr.GetCurrentMode(m_userOutput); + if (!mode.isCurrent && !switchOnOff) + { + out = NULL; + } + } + } + if (!out) + { + m_userOutput = g_xrandr.GetModes()[0].name; + out = g_xrandr.GetOutput(m_userOutput); + } + + if (switchOnOff) + { + // switch on output + g_xrandr.TurnOnOutput(m_userOutput); + + // switch off other outputs + std::vector<XOutput> outputs = g_xrandr.GetModes(); + for (size_t i=0; i<outputs.size(); i++) + { + if (StringUtils::EqualsNoCase(outputs[i].name, m_userOutput)) + continue; + g_xrandr.TurnOffOutput(outputs[i].name); + } + } + + XMode mode = g_xrandr.GetCurrentMode(m_userOutput); + if (mode.id.empty()) + mode = g_xrandr.GetPreferredMode(m_userOutput); + m_bIsRotated = out->isRotated; + if (!m_bIsRotated) + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), out->name, mode.w, mode.h, mode.hz, 0); + else + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), out->name, mode.h, mode.w, mode.hz, 0); + CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strId = mode.id; + } + else + { + m_userOutput = "No Output"; + m_screen = DefaultScreen(m_dpy); + int w = DisplayWidth(m_dpy, m_screen); + int h = DisplayHeight(m_dpy, m_screen); + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), m_userOutput, w, h, 0.0, 0); + } + + // erase previous stored modes + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + CLog::Log(LOGINFO, "Available videomodes (xrandr):"); + + XOutput *out = g_xrandr.GetOutput(m_userOutput); + if (out != NULL) + { + CLog::Log(LOGINFO, "Output '{}' has {} modes", out->name, out->modes.size()); + + for (auto mode : out->modes) + { + CLog::Log(LOGINFO, "ID:{} Name:{} Refresh:{:f} Width:{} Height:{}", mode.id, mode.name, + mode.hz, mode.w, mode.h); + RESOLUTION_INFO res; + res.dwFlags = 0; + + if (mode.IsInterlaced()) + res.dwFlags |= D3DPRESENTFLAG_INTERLACED; + + if (!m_bIsRotated) + { + res.iWidth = mode.w; + res.iHeight = mode.h; + res.iScreenWidth = mode.w; + res.iScreenHeight = mode.h; + } + else + { + res.iWidth = mode.h; + res.iHeight = mode.w; + res.iScreenWidth = mode.h; + res.iScreenHeight = mode.w; + } + + if (mode.h > 0 && mode.w > 0 && out->hmm > 0 && out->wmm > 0) + res.fPixelRatio = ((float)out->wmm/(float)mode.w) / (((float)out->hmm/(float)mode.h)); + else + res.fPixelRatio = 1.0f; + + CLog::Log(LOGINFO, "Pixel Ratio: {:f}", res.fPixelRatio); + + res.strMode = StringUtils::Format("{}: {} @ {:.2f}Hz", out->name, mode.name, mode.hz); + res.strOutput = out->name; + res.strId = mode.id; + res.iSubtitles = mode.h; + res.fRefreshRate = mode.hz; + res.bFullScreen = true; + + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + } + } + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +bool CWinSystemX11::HasCalibration(const RESOLUTION_INFO &resInfo) +{ + XOutput *out = g_xrandr.GetOutput(m_currentOutput); + + // keep calibrations done on a not connected output + if (!StringUtils::EqualsNoCase(out->name, resInfo.strOutput)) + return true; + + // keep calibrations not updated with resolution data + if (resInfo.iWidth == 0) + return true; + + float fPixRatio; + if (resInfo.iHeight>0 && resInfo.iWidth>0 && out->hmm>0 && out->wmm>0) + fPixRatio = ((float)out->wmm/(float)resInfo.iWidth) / (((float)out->hmm/(float)resInfo.iHeight)); + else + fPixRatio = 1.0f; + + if (resInfo.Overscan.left != 0) + return true; + if (resInfo.Overscan.top != 0) + return true; + if (resInfo.Overscan.right != resInfo.iWidth) + return true; + if (resInfo.Overscan.bottom != resInfo.iHeight) + return true; + if (resInfo.fPixelRatio != fPixRatio) + return true; + if (resInfo.iSubtitles != resInfo.iHeight) + return true; + + return false; +} + +bool CWinSystemX11::UseLimitedColor() +{ + return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE); +} + +std::vector<std::string> CWinSystemX11::GetConnectedOutputs() +{ + std::vector<std::string> outputs; + std::vector<XOutput> outs; + g_xrandr.Query(true); + outs = g_xrandr.GetModes(); + outputs.emplace_back("Default"); + for(unsigned int i=0; i<outs.size(); ++i) + { + outputs.emplace_back(outs[i].name); + } + + return outputs; +} + +bool CWinSystemX11::IsCurrentOutput(const std::string& output) +{ + return (StringUtils::EqualsNoCase(output, "Default")) || (m_currentOutput.compare(output.c_str()) == 0); +} + +void CWinSystemX11::ShowOSMouse(bool show) +{ + if (show) + XUndefineCursor(m_dpy,m_mainWindow); + else if (m_invisibleCursor) + XDefineCursor(m_dpy,m_mainWindow, m_invisibleCursor); +} + +std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> CWinSystemX11::GetOSScreenSaverImpl() +{ + std::unique_ptr<IOSScreenSaver> ret; + if (m_dpy) + { + ret.reset(new COSScreenSaverX11(m_dpy)); + } + return ret; +} + +void CWinSystemX11::NotifyAppActiveChange(bool bActivated) +{ + if (bActivated && m_bWasFullScreenBeforeMinimize && !m_bFullScreen) + { + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + + m_bWasFullScreenBeforeMinimize = false; + } + m_minimized = !bActivated; +} + +void CWinSystemX11::NotifyAppFocusChange(bool bGaining) +{ + if (bGaining && m_bWasFullScreenBeforeMinimize && !m_bIgnoreNextFocusMessage && + !m_bFullScreen) + { + m_bWasFullScreenBeforeMinimize = false; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + m_minimized = false; + } + if (!bGaining) + m_bIgnoreNextFocusMessage = false; +} + +bool CWinSystemX11::Minimize() +{ + m_bWasFullScreenBeforeMinimize = m_bFullScreen; + if (m_bWasFullScreenBeforeMinimize) + { + m_bIgnoreNextFocusMessage = true; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + } + + XIconifyWindow(m_dpy, m_mainWindow, m_screen); + + m_minimized = true; + return true; +} +bool CWinSystemX11::Restore() +{ + return false; +} +bool CWinSystemX11::Hide() +{ + XUnmapWindow(m_dpy, m_mainWindow); + XFlush(m_dpy); + return true; +} +bool CWinSystemX11::Show(bool raise) +{ + XMapWindow(m_dpy, m_mainWindow); + XFlush(m_dpy); + m_minimized = false; + return true; +} + +void CWinSystemX11::NotifyXRREvent() +{ + CLog::Log(LOGDEBUG, "{} - notify display reset event", __FUNCTION__); + + std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext()); + + if (!g_xrandr.Query(true)) + { + CLog::Log(LOGERROR, "WinSystemX11::RefreshWindow - failed to query xrandr"); + return; + } + + // if external event update resolutions + if (!m_bIsInternalXrr) + { + UpdateResolutions(); + } + + RecreateWindow(); +} + +void CWinSystemX11::RecreateWindow() +{ + m_windowDirty = true; + + std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext()); + + XOutput *out = g_xrandr.GetOutput(m_userOutput); + XMode mode = g_xrandr.GetCurrentMode(m_userOutput); + + if (out) + CLog::Log(LOGDEBUG, "{} - current output: {}, mode: {}, refresh: {:.3f}", __FUNCTION__, + out->name, mode.id, mode.hz); + else + CLog::Log(LOGWARNING, "{} - output name not set", __FUNCTION__); + + RESOLUTION_INFO res; + unsigned int i; + bool found(false); + for (i = RES_DESKTOP; i < CDisplaySettings::GetInstance().ResolutionInfoSize(); ++i) + { + res = CDisplaySettings::GetInstance().GetResolutionInfo(i); + if (StringUtils::EqualsNoCase(CDisplaySettings::GetInstance().GetResolutionInfo(i).strId, mode.id)) + { + found = true; + break; + } + } + + if (!found) + { + CLog::Log(LOGERROR, "CWinSystemX11::RecreateWindow - could not find resolution"); + i = RES_DESKTOP; + } + + if (CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot()) + CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution((RESOLUTION)i, true); + else + CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(RES_WINDOW, true); +} + +void CWinSystemX11::OnLostDevice() +{ + CLog::Log(LOGDEBUG, "{} - notify display change event", __FUNCTION__); + + { + std::unique_lock<CCriticalSection> lock(m_resourceSection); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnLostDisplay(); + } + + m_winEventsX11->SetXRRFailSafeTimer(3s); +} + +void CWinSystemX11::Register(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemX11::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +int CWinSystemX11::XErrorHandler(Display* dpy, XErrorEvent* error) +{ + char buf[1024]; + XGetErrorText(error->display, error->error_code, buf, sizeof(buf)); + CLog::Log(LOGERROR, + "CWinSystemX11::XErrorHandler: {}, type:{}, serial:{}, error_code:{}, request_code:{} " + "minor_code:{}", + buf, error->type, error->serial, (int)error->error_code, (int)error->request_code, + (int)error->minor_code); + + return 0; +} + +bool CWinSystemX11::SetWindow(int width, int height, bool fullscreen, const std::string &output, int *winstate) +{ + bool changeWindow = false; + bool changeSize = false; + float mouseX = 0.5; + float mouseY = 0.5; + + if (!m_mainWindow) + { + CServiceBroker::GetInputManager().SetMouseActive(false); + } + + if (m_mainWindow && ((m_bFullScreen != fullscreen) || m_currentOutput.compare(output) != 0 || m_windowDirty)) + { + // set mouse to last known position + // we can't trust values after an xrr event + if (m_bIsInternalXrr && m_MouseX >= 0 && m_MouseY >= 0) + { + mouseX = (float)m_MouseX/m_nWidth; + mouseY = (float)m_MouseY/m_nHeight; + } + else if (!m_windowDirty) + { + Window root_return, child_return; + int root_x_return, root_y_return; + int win_x_return, win_y_return; + unsigned int mask_return; + bool isInWin = XQueryPointer(m_dpy, m_mainWindow, &root_return, &child_return, + &root_x_return, &root_y_return, + &win_x_return, &win_y_return, + &mask_return); + + if (isInWin) + { + mouseX = (float)win_x_return/m_nWidth; + mouseY = (float)win_y_return/m_nHeight; + } + } + + CServiceBroker::GetInputManager().SetMouseActive(false); + OnLostDevice(); + DestroyWindow(); + m_windowDirty = true; + } + + // create main window + if (!m_mainWindow) + { + Colormap cmap; + XSetWindowAttributes swa; + XVisualInfo *vi; + int x0 = 0; + int y0 = 0; + + XOutput *out = g_xrandr.GetOutput(output); + if (!out) + out = g_xrandr.GetOutput(m_currentOutput); + if (out) + { + m_screen = out->screen; + x0 = out->x; + y0 = out->y; + } + + vi = GetVisual(); + if (!vi) + { + CLog::Log(LOGERROR, "Failed to find matching visual"); + return false; + } + + cmap = XCreateColormap(m_dpy, RootWindow(m_dpy, vi->screen), vi->visual, AllocNone); + + bool hasWM = HasWindowManager(); + + int def_vis = (vi->visual == DefaultVisual(m_dpy, vi->screen)); + swa.override_redirect = hasWM ? False : True; + swa.border_pixel = fullscreen ? 0 : 5; + swa.background_pixel = def_vis ? BlackPixel(m_dpy, vi->screen) : 0; + swa.colormap = cmap; + swa.event_mask = FocusChangeMask | KeyPressMask | KeyReleaseMask | + ButtonPressMask | ButtonReleaseMask | PointerMotionMask | + PropertyChangeMask | StructureNotifyMask | KeymapStateMask | + EnterWindowMask | LeaveWindowMask | ExposureMask; + unsigned long mask = CWBackPixel | CWBorderPixel | CWColormap | CWOverrideRedirect | CWEventMask; + + m_mainWindow = XCreateWindow(m_dpy, RootWindow(m_dpy, vi->screen), + x0, y0, width, height, 0, vi->depth, + InputOutput, vi->visual, + mask, &swa); + + swa.override_redirect = False; + swa.border_pixel = 0; + swa.event_mask = ExposureMask; + mask = CWBackPixel | CWBorderPixel | CWColormap | CWOverrideRedirect | CWColormap | CWEventMask; + + m_glWindow = XCreateWindow(m_dpy, m_mainWindow, + 0, 0, width, height, 0, vi->depth, + InputOutput, vi->visual, + mask, &swa); + + if (fullscreen && hasWM) + { + Atom fs = XInternAtom(m_dpy, "_NET_WM_STATE_FULLSCREEN", True); + XChangeProperty(m_dpy, m_mainWindow, XInternAtom(m_dpy, "_NET_WM_STATE", True), XA_ATOM, 32, PropModeReplace, (unsigned char *) &fs, 1); + // disable desktop compositing for KDE, when Kodi is in full-screen mode + int one = 1; + Atom composite = XInternAtom(m_dpy, "_KDE_NET_WM_BLOCK_COMPOSITING", True); + if (composite != None) + { + XChangeProperty(m_dpy, m_mainWindow, XInternAtom(m_dpy, "_KDE_NET_WM_BLOCK_COMPOSITING", True), XA_CARDINAL, 32, + PropModeReplace, (unsigned char*) &one, 1); + } + composite = XInternAtom(m_dpy, "_NET_WM_BYPASS_COMPOSITOR", True); + if (composite != None) + { + // standard way for Gnome 3 + XChangeProperty(m_dpy, m_mainWindow, XInternAtom(m_dpy, "_NET_WM_BYPASS_COMPOSITOR", True), XA_CARDINAL, 32, + PropModeReplace, (unsigned char*) &one, 1); + } + } + + // define invisible cursor + Pixmap bitmapNoData; + XColor black; + static char noData[] = { 0,0,0,0,0,0,0,0 }; + black.red = black.green = black.blue = 0; + + bitmapNoData = XCreateBitmapFromData(m_dpy, m_mainWindow, noData, 8, 8); + m_invisibleCursor = XCreatePixmapCursor(m_dpy, bitmapNoData, bitmapNoData, + &black, &black, 0, 0); + XFreePixmap(m_dpy, bitmapNoData); + XDefineCursor(m_dpy,m_mainWindow, m_invisibleCursor); + XFree(vi); + + //init X11 events + m_winEventsX11->Init(m_dpy, m_mainWindow); + + changeWindow = true; + changeSize = true; + } + + if (!m_winEventsX11->HasStructureChanged() && ((width != m_nWidth) || (height != m_nHeight))) + { + changeSize = true; + } + + if (changeSize || changeWindow) + { + XResizeWindow(m_dpy, m_mainWindow, width, height); + } + + if ((width != m_nWidth) || (height != m_nHeight) || changeWindow) + { + XResizeWindow(m_dpy, m_glWindow, width, height); + } + + if (changeWindow) + { + m_icon = None; + { + CreateIconPixmap(); + XWMHints *wm_hints; + XClassHint *class_hints; + XTextProperty windowName, iconName; + + std::string titleString = CCompileInfo::GetAppName(); + const std::string& classString = titleString; + char *title = const_cast<char*>(titleString.c_str()); + + XStringListToTextProperty(&title, 1, &windowName); + XStringListToTextProperty(&title, 1, &iconName); + + wm_hints = XAllocWMHints(); + wm_hints->initial_state = NormalState; + wm_hints->icon_pixmap = m_icon; + wm_hints->flags = StateHint | IconPixmapHint; + + class_hints = XAllocClassHint(); + class_hints->res_class = const_cast<char*>(classString.c_str()); + class_hints->res_name = const_cast<char*>(classString.c_str()); + + XSetWMProperties(m_dpy, m_mainWindow, &windowName, &iconName, + NULL, 0, NULL, wm_hints, + class_hints); + XFree(class_hints); + XFree(wm_hints); + XFree(iconName.value); + XFree(windowName.value); + + // register interest in the delete window message + Atom wmDeleteMessage = XInternAtom(m_dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(m_dpy, m_mainWindow, &wmDeleteMessage, 1); + } + + // placement of window may follow mouse + XWarpPointer(m_dpy, None, m_mainWindow, 0, 0, 0, 0, mouseX*width, mouseY*height); + + XMapRaised(m_dpy, m_glWindow); + XMapRaised(m_dpy, m_mainWindow); + + // discard events generated by creating the window, i.e. xrr events + XSync(m_dpy, True); + + if (winstate) + *winstate = 1; + } + + UpdateCrtc(); + + return true; +} + +bool CWinSystemX11::CreateIconPixmap() +{ + int depth; + XImage *img = NULL; + Visual *vis; + XWindowAttributes wndattribs; + XVisualInfo visInfo; + double rRatio; + double gRatio; + double bRatio; + int outIndex = 0; + unsigned int i,j; + unsigned char *buf; + uint32_t *newBuf = 0; + size_t numNewBufBytes; + + // Get visual Info + XGetWindowAttributes(m_dpy, m_glWindow, &wndattribs); + visInfo.visualid = wndattribs.visual->visualid; + int nvisuals = 0; + XVisualInfo* visuals = XGetVisualInfo(m_dpy, VisualIDMask, &visInfo, &nvisuals); + if (nvisuals != 1) + { + CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - could not find visual"); + return false; + } + visInfo = visuals[0]; + XFree(visuals); + + depth = visInfo.depth; + vis = visInfo.visual; + + if (depth < 15) + { + CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - no suitable depth"); + return false; + } + + rRatio = vis->red_mask / 255.0; + gRatio = vis->green_mask / 255.0; + bRatio = vis->blue_mask / 255.0; + + std::unique_ptr<CTexture> iconTexture = + CTexture::LoadFromFile("special://xbmc/media/icon256x256.png"); + + if (!iconTexture) + return false; + + buf = iconTexture->GetPixels(); + + if (depth>=24) + numNewBufBytes = (4 * (iconTexture->GetWidth() * iconTexture->GetHeight())); + else + numNewBufBytes = (2 * (iconTexture->GetWidth() * iconTexture->GetHeight())); + + newBuf = (uint32_t*)malloc(numNewBufBytes); + if (!newBuf) + { + CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - malloc failed"); + return false; + } + + for (i=0; i<iconTexture->GetHeight();++i) + { + for (j=0; j<iconTexture->GetWidth();++j) + { + unsigned int pos = i*iconTexture->GetPitch()+j*4; + unsigned int r, g, b; + r = (buf[pos+2] * rRatio); + g = (buf[pos+1] * gRatio); + b = (buf[pos+0] * bRatio); + r &= vis->red_mask; + g &= vis->green_mask; + b &= vis->blue_mask; + newBuf[outIndex] = r | g | b; + ++outIndex; + } + } + img = XCreateImage(m_dpy, vis, depth,ZPixmap, 0, (char *)newBuf, + iconTexture->GetWidth(), iconTexture->GetHeight(), + (depth>=24)?32:16, 0); + if (!img) + { + CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - could not create image"); + free(newBuf); + return false; + } + if (!XInitImage(img)) + { + CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - init image failed"); + XDestroyImage(img); + return false; + } + + // set byte order + union + { + char c[sizeof(short)]; + short s; + } order; + order.s = 1; + if ((1 == order.c[0])) + { + img->byte_order = LSBFirst; + } + else + { + img->byte_order = MSBFirst; + } + + // create icon pixmap from image + m_icon = XCreatePixmap(m_dpy, m_glWindow, img->width, img->height, depth); + GC gc = XCreateGC(m_dpy, m_glWindow, 0, NULL); + XPutImage(m_dpy, m_icon, gc, img, 0, 0, 0, 0, img->width, img->height); + XFreeGC(m_dpy, gc); + XDestroyImage(img); // this also frees newBuf + + return true; +} + +bool CWinSystemX11::HasWindowManager() +{ + Window wm_check; + unsigned char *data; + int status, real_format; + Atom real_type, prop; + unsigned long items_read, items_left; + + prop = XInternAtom(m_dpy, "_NET_SUPPORTING_WM_CHECK", True); + if (prop == None) + return false; + status = XGetWindowProperty(m_dpy, DefaultRootWindow(m_dpy), prop, + 0L, 1L, False, XA_WINDOW, &real_type, &real_format, + &items_read, &items_left, &data); + if(status != Success || ! items_read) + { + if(status == Success) + XFree(data); + return false; + } + + wm_check = ((Window*)data)[0]; + XFree(data); + + status = XGetWindowProperty(m_dpy, wm_check, prop, + 0L, 1L, False, XA_WINDOW, &real_type, &real_format, + &items_read, &items_left, &data); + + if(status != Success || !items_read) + { + if(status == Success) + XFree(data); + return false; + } + + if(wm_check != ((Window*)data)[0]) + { + XFree(data); + return false; + } + + XFree(data); + + prop = XInternAtom(m_dpy, "_NET_WM_NAME", True); + if (prop == None) + { + CLog::Log(LOGDEBUG,"Window Manager Name: "); + return true; + } + + status = XGetWindowProperty(m_dpy, wm_check, prop, + 0L, (~0L), False, AnyPropertyType, &real_type, &real_format, + &items_read, &items_left, &data); + + if(status == Success && items_read) + { + const char* s; + + s = reinterpret_cast<const char*>(data); + CLog::Log(LOGDEBUG, "Window Manager Name: {}", s); + } + else + CLog::Log(LOGDEBUG,"Window Manager Name: "); + + if(status == Success) + XFree(data); + + return true; +} + +void CWinSystemX11::UpdateCrtc() +{ + XWindowAttributes winattr; + int posx, posy; + float fps = 0.0f; + Window child; + XGetWindowAttributes(m_dpy, m_mainWindow, &winattr); + XTranslateCoordinates(m_dpy, m_mainWindow, RootWindow(m_dpy, m_screen), winattr.x, winattr.y, + &posx, &posy, &child); + + m_crtc = g_xrandr.GetCrtc(posx+winattr.width/2, posy+winattr.height/2, fps); + CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(fps); +} + +bool CWinSystemX11::MessagePump() +{ + return m_winEvents->MessagePump(); +} diff --git a/xbmc/windowing/X11/WinSystemX11.h b/xbmc/windowing/X11/WinSystemX11.h new file mode 100644 index 0000000..f65538d --- /dev/null +++ b/xbmc/windowing/X11/WinSystemX11.h @@ -0,0 +1,117 @@ +/* + * 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 + +#include "settings/lib/ISettingCallback.h" +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "utils/Stopwatch.h" +#include "windowing/WinSystem.h" + +#include <string> +#include <vector> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> + +class IDispResource; + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CWinEventsX11; + +class CWinSystemX11 : public CWinSystemBase +{ +public: + CWinSystemX11(); + ~CWinSystemX11() override; + + const std::string GetName() override { return "x11"; } + + // CWinSystemBase + 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; + void FinishWindowResize(int newWidth, int newHeight) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + void UpdateResolutions() override; + void ShowOSMouse(bool show) override; + + void NotifyAppActiveChange(bool bActivated) override; + void NotifyAppFocusChange(bool bGaining) override; + + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + void Register(IDispResource *resource) override; + void Unregister(IDispResource *resource) override; + bool HasCalibration(const RESOLUTION_INFO &resInfo) override; + bool UseLimitedColor() override; + + std::vector<std::string> GetConnectedOutputs() override; + + // Local to WinSystemX11 only + Display* GetDisplay() { return m_dpy; } + int GetScreen() { return m_screen; } + void NotifyXRREvent(); + bool IsCurrentOutput(const std::string& output); + void RecreateWindow(); + int GetCrtc() { return m_crtc; } + + // winevents override + bool MessagePump() override; + +protected: + std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override; + + virtual bool SetWindow(int width, int height, bool fullscreen, const std::string &output, int *winstate = NULL) = 0; + virtual XVisualInfo* GetVisual() = 0; + + void OnLostDevice(); + + Window m_glWindow = 0, m_mainWindow = 0; + int m_screen = 0; + Display *m_dpy; + Cursor m_invisibleCursor = 0; + Pixmap m_icon; + bool m_bIsRotated; + bool m_bWasFullScreenBeforeMinimize; + bool m_minimized; + bool m_bIgnoreNextFocusMessage; + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + bool m_delayDispReset; + XbmcThreads::EndTime<> m_dispResetTimer; + std::string m_currentOutput; + std::string m_userOutput; + bool m_windowDirty; + bool m_bIsInternalXrr; + int m_MouseX, m_MouseY; + int m_crtc; + CWinEventsX11 *m_winEventsX11; + +private: + bool IsSuitableVisual(XVisualInfo *vInfo); + static int XErrorHandler(Display* dpy, XErrorEvent* error); + bool CreateIconPixmap(); + bool HasWindowManager(); + void UpdateCrtc(); +}; + +} +} +} diff --git a/xbmc/windowing/X11/WinSystemX11GLContext.cpp b/xbmc/windowing/X11/WinSystemX11GLContext.cpp new file mode 100644 index 0000000..ad27aca --- /dev/null +++ b/xbmc/windowing/X11/WinSystemX11GLContext.cpp @@ -0,0 +1,358 @@ +/* + * 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 "WinSystemX11GLContext.h" + +#include "GLContextEGL.h" +#include "OptionalsReg.h" +#include "VideoSyncOML.h" +#include "X11DPMSSupport.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationSkinHandling.h" +#include "cores/RetroPlayer/process/X11/RPProcessInfoX11.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/Process/X11/ProcessInfoX11.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "guilib/DispResource.h" +#include "rendering/gl/ScreenshotSurfaceGL.h" +#include "windowing/GraphicContext.h" +#include "windowing/WindowSystemFactory.h" + +#include <mutex> +#include <vector> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> + +using namespace KODI; +using namespace KODI::WINDOWING::X11; + + +void CWinSystemX11GLContext::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "x11"); +} + +std::unique_ptr<CWinSystemBase> CWinSystemX11GLContext::CreateWinSystem() +{ + return std::make_unique<CWinSystemX11GLContext>(); +} + +CWinSystemX11GLContext::~CWinSystemX11GLContext() +{ + delete m_pGLContext; +} + +void CWinSystemX11GLContext::PresentRenderImpl(bool rendered) +{ + if (rendered) + m_pGLContext->SwapBuffers(); + + if (m_delayDispReset && m_dispResetTimer.IsTimePast()) + { + m_delayDispReset = false; + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); + } +} + +void CWinSystemX11GLContext::SetVSyncImpl(bool enable) +{ + m_pGLContext->SetVSync(enable); +} + +bool CWinSystemX11GLContext::IsExtSupported(const char* extension) const +{ + if(strncmp(extension, m_pGLContext->ExtPrefix().c_str(), 4) != 0) + return CRenderSystemGL::IsExtSupported(extension); + + return m_pGLContext->IsExtSupported(extension); +} + +XID CWinSystemX11GLContext::GetWindow() const +{ + return GLXGetWindow(m_pGLContext); +} + +void* CWinSystemX11GLContext::GetGlxContext() const +{ + return GLXGetContext(m_pGLContext); +} + +EGLDisplay CWinSystemX11GLContext::GetEGLDisplay() const +{ + return static_cast<CGLContextEGL*>(m_pGLContext)->m_eglDisplay; +} + +EGLSurface CWinSystemX11GLContext::GetEGLSurface() const +{ + return static_cast<CGLContextEGL*>(m_pGLContext)->m_eglSurface; +} + +EGLContext CWinSystemX11GLContext::GetEGLContext() const +{ + return static_cast<CGLContextEGL*>(m_pGLContext)->m_eglContext; +} + +EGLConfig CWinSystemX11GLContext::GetEGLConfig() const +{ + return static_cast<CGLContextEGL*>(m_pGLContext)->m_eglConfig; +} + +bool CWinSystemX11GLContext::SetWindow(int width, int height, bool fullscreen, const std::string &output, int *winstate) +{ + int newwin = 0; + + CWinSystemX11::SetWindow(width, height, fullscreen, output, &newwin); + if (newwin) + { + RefreshGLContext(m_currentOutput.compare(output) != 0); + XSync(m_dpy, False); + CServiceBroker::GetWinSystem()->GetGfxContext().Clear(0); + CServiceBroker::GetWinSystem()->GetGfxContext().Flip(true, false); + ResetVSync(); + + m_windowDirty = false; + m_bIsInternalXrr = false; + + if (!m_delayDispReset) + { + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); + } + } + return true; +} + +bool CWinSystemX11GLContext::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + if(!CWinSystemX11::CreateNewWindow(name, fullScreen, res)) + return false; + + m_pGLContext->QueryExtensions(); + return true; +} + +bool CWinSystemX11GLContext::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + m_newGlContext = false; + CWinSystemX11::ResizeWindow(newWidth, newHeight, newLeft, newTop); + CRenderSystemGL::ResetRenderSystem(newWidth, newHeight); + + if (m_newGlContext) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appSkin = components.GetComponent<CApplicationSkinHandling>(); + appSkin->ReloadSkin(); + } + + return true; +} + +void CWinSystemX11GLContext::FinishWindowResize(int newWidth, int newHeight) +{ + m_newGlContext = false; + CWinSystemX11::FinishWindowResize(newWidth, newHeight); + CRenderSystemGL::ResetRenderSystem(newWidth, newHeight); + + if (m_newGlContext) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appSkin = components.GetComponent<CApplicationSkinHandling>(); + appSkin->ReloadSkin(); + } +} + +bool CWinSystemX11GLContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + m_newGlContext = false; + CWinSystemX11::SetFullScreen(fullScreen, res, blankOtherDisplays); + CRenderSystemGL::ResetRenderSystem(res.iWidth, res.iHeight); + + if (m_newGlContext) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appSkin = components.GetComponent<CApplicationSkinHandling>(); + appSkin->ReloadSkin(); + } + + return true; +} + +bool CWinSystemX11GLContext::DestroyWindowSystem() +{ + if (m_pGLContext) + m_pGLContext->Destroy(); + return CWinSystemX11::DestroyWindowSystem(); +} + +bool CWinSystemX11GLContext::DestroyWindow() +{ + if (m_pGLContext) + m_pGLContext->Detach(); + return CWinSystemX11::DestroyWindow(); +} + +XVisualInfo* CWinSystemX11GLContext::GetVisual() +{ + int count = 0; + XVisualInfo vTemplate; + XVisualInfo *visual = nullptr; + + int vMask = VisualScreenMask | VisualDepthMask | VisualClassMask; + + vTemplate.screen = m_screen; + vTemplate.depth = 24; + vTemplate.c_class = TrueColor; + + visual = XGetVisualInfo(m_dpy, vMask, &vTemplate, &count); + + if (!visual) + { + vTemplate.depth = 30; + visual = XGetVisualInfo(m_dpy, vMask, &vTemplate, &count); + } + + return visual; +} + +bool CWinSystemX11GLContext::RefreshGLContext(bool force) +{ + bool success = false; + if (m_pGLContext) + { + if (force) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appSkin = components.GetComponent<CApplicationSkinHandling>(); + appSkin->UnloadSkin(); + CRenderSystemGL::DestroyRenderSystem(); + } + success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext); + if (!success) + { + success = m_pGLContext->CreatePB(); + m_newGlContext = true; + } + if (force) + CRenderSystemGL::InitRenderSystem(); + return success; + } + + m_dpms = std::make_shared<CX11DPMSSupport>(); + VIDEOPLAYER::CProcessInfoX11::Register(); + RETRO::CRPProcessInfoX11::Register(); + RETRO::CRPProcessInfoX11::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL); + CDVDFactoryCodec::ClearHWAccels(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CLinuxRendererGL::Register(); + + CScreenshotSurfaceGL::Register(); + + std::string gpuvendor; + const char* vend = (const char*) glGetString(GL_VENDOR); + if (vend) + gpuvendor = vend; + std::transform(gpuvendor.begin(), gpuvendor.end(), gpuvendor.begin(), ::tolower); + bool isNvidia = (gpuvendor.compare(0, 6, "nvidia") == 0); + bool isIntel = (gpuvendor.compare(0, 5, "intel") == 0); + std::string gli = (getenv("KODI_GL_INTERFACE") != nullptr) ? getenv("KODI_GL_INTERFACE") : ""; + + if (gli != "GLX") + { + m_pGLContext = new CGLContextEGL(m_dpy, EGL_OPENGL_API); + success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext); + if (success) + { + if (!isNvidia) + { + m_vaapiProxy.reset(VaapiProxyCreate()); + VaapiProxyConfig(m_vaapiProxy.get(), GetDisplay(), + static_cast<CGLContextEGL*>(m_pGLContext)->m_eglDisplay); + bool general = false; + bool deepColor = false; + VAAPIRegisterRenderGL(m_vaapiProxy.get(), general, deepColor); + if (general) + { + VAAPIRegister(m_vaapiProxy.get(), deepColor); + return true; + } + if (isIntel || gli == "EGL") + return true; + } + } + else if (gli == "EGL_PB") + { + success = m_pGLContext->CreatePB(); + if (success) + return true; + } + } + + delete m_pGLContext; + + // fallback for vdpau + m_pGLContext = GLXContextCreate(m_dpy); + success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext); + if (success) + { + VDPAURegister(); + VDPAURegisterRender(); + } + return success; +} + +std::unique_ptr<CVideoSync> CWinSystemX11GLContext::GetVideoSync(void *clock) +{ + std::unique_ptr<CVideoSync> pVSync; + + if (dynamic_cast<CGLContextEGL*>(m_pGLContext)) + { + pVSync.reset(new CVideoSyncOML(clock, *this)); + } + else + { + pVSync.reset(GLXVideoSyncCreate(clock, *this)); + } + + return pVSync; +} + +float CWinSystemX11GLContext::GetFrameLatencyAdjustment() +{ + if (m_pGLContext) + { + uint64_t msc, interval; + float micros = m_pGLContext->GetVblankTiming(msc, interval); + return micros / 1000; + } + return 0; +} + +uint64_t CWinSystemX11GLContext::GetVblankTiming(uint64_t &msc, uint64_t &interval) +{ + if (m_pGLContext) + { + float micros = m_pGLContext->GetVblankTiming(msc, interval); + return micros; + } + msc = 0; + interval = 0; + return 0; +} + +void CWinSystemX11GLContext::delete_CVaapiProxy::operator()(CVaapiProxy *p) const +{ + VaapiProxyDelete(p); +} diff --git a/xbmc/windowing/X11/WinSystemX11GLContext.h b/xbmc/windowing/X11/WinSystemX11GLContext.h new file mode 100644 index 0000000..7d227b2 --- /dev/null +++ b/xbmc/windowing/X11/WinSystemX11GLContext.h @@ -0,0 +1,79 @@ +/* + * 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 + +#include "WinSystemX11.h" +#include "rendering/gl/RenderSystemGL.h" +#include "system_egl.h" + +#include <memory> + +class CGLContext; + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CVaapiProxy; + +class CWinSystemX11GLContext : public CWinSystemX11, public CRenderSystemGL +{ +public: + CWinSystemX11GLContext() = default; + ~CWinSystemX11GLContext() override; + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystem via CWinSystemX11 + CRenderSystemBase *GetRenderSystem() override { return this; } + bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override; + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + void FinishWindowResize(int newWidth, int newHeight) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + bool DestroyWindowSystem() override; + bool DestroyWindow() override; + + bool IsExtSupported(const char* extension) const override; + + // videosync + std::unique_ptr<CVideoSync> GetVideoSync(void *clock) override; + float GetFrameLatencyAdjustment() override; + uint64_t GetVblankTiming(uint64_t &msc, uint64_t &interval); + + XID GetWindow() const; + void* GetGlxContext() const; + EGLDisplay GetEGLDisplay() const; + EGLSurface GetEGLSurface() const; + EGLContext GetEGLContext() const; + EGLConfig GetEGLConfig() const; + +protected: + bool SetWindow(int width, int height, bool fullscreen, const std::string &output, int *winstate = NULL) override; + void PresentRenderImpl(bool rendered) override; + void SetVSyncImpl(bool enable) override; + bool RefreshGLContext(bool force); + XVisualInfo* GetVisual() override; + + CGLContext *m_pGLContext = nullptr; + bool m_newGlContext; + + struct delete_CVaapiProxy + { + void operator()(CVaapiProxy *p) const; + }; + std::unique_ptr<CVaapiProxy, delete_CVaapiProxy> m_vaapiProxy; +}; + +} +} +} diff --git a/xbmc/windowing/X11/WinSystemX11GLESContext.cpp b/xbmc/windowing/X11/WinSystemX11GLESContext.cpp new file mode 100644 index 0000000..e2367d3 --- /dev/null +++ b/xbmc/windowing/X11/WinSystemX11GLESContext.cpp @@ -0,0 +1,293 @@ +/* + * 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 "WinSystemX11GLESContext.h" + +#include "GLContextEGL.h" +#include "OptionalsReg.h" +#include "X11DPMSSupport.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationSkinHandling.h" +#include "cores/RetroPlayer/process/X11/RPProcessInfoX11.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/Process/X11/ProcessInfoX11.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "guilib/DispResource.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/WindowSystemFactory.h" + +#include <mutex> + +using namespace KODI; +using namespace KODI::WINDOWING::X11; + +void CWinSystemX11GLESContext::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "x11"); +} + +std::unique_ptr<CWinSystemBase> CWinSystemX11GLESContext::CreateWinSystem() +{ + return std::make_unique<CWinSystemX11GLESContext>(); +} + +CWinSystemX11GLESContext::~CWinSystemX11GLESContext() +{ + delete m_pGLContext; +} + +void CWinSystemX11GLESContext::PresentRenderImpl(bool rendered) +{ + if (rendered && m_pGLContext) + m_pGLContext->SwapBuffers(); + + if (m_delayDispReset && m_dispResetTimer.IsTimePast()) + { + m_delayDispReset = false; + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); + } +} + +void CWinSystemX11GLESContext::SetVSyncImpl(bool enable) +{ + m_pGLContext->SetVSync(enable); +} + +bool CWinSystemX11GLESContext::IsExtSupported(const char* extension) const +{ + if (strncmp(extension, m_pGLContext->ExtPrefix().c_str(), 4) != 0) + return CRenderSystemGLES::IsExtSupported(extension); + + return m_pGLContext->IsExtSupported(extension); +} + +EGLDisplay CWinSystemX11GLESContext::GetEGLDisplay() const +{ + return m_pGLContext->m_eglDisplay; +} + +EGLSurface CWinSystemX11GLESContext::GetEGLSurface() const +{ + return m_pGLContext->m_eglSurface; +} + +EGLContext CWinSystemX11GLESContext::GetEGLContext() const +{ + return m_pGLContext->m_eglContext; +} + +EGLConfig CWinSystemX11GLESContext::GetEGLConfig() const +{ + return m_pGLContext->m_eglConfig; +} + +bool CWinSystemX11GLESContext::SetWindow(int width, int height, bool fullscreen, const std::string& output, int* winstate) +{ + int newwin = 0; + + CWinSystemX11::SetWindow(width, height, fullscreen, output, &newwin); + if (newwin) + { + RefreshGLContext(m_currentOutput.compare(output) != 0); + XSync(m_dpy, false); + CServiceBroker::GetWinSystem()->GetGfxContext().Clear(0); + CServiceBroker::GetWinSystem()->GetGfxContext().Flip(true, false); + ResetVSync(); + + m_windowDirty = false; + m_bIsInternalXrr = false; + + if (!m_delayDispReset) + { + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); + } + } + return true; +} + +bool CWinSystemX11GLESContext::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + CLog::Log(LOGINFO, "CWinSystemX11GLESContext::CreateNewWindow"); + if (!CWinSystemX11::CreateNewWindow(name, fullScreen, res) || !m_pGLContext) + return false; + + m_pGLContext->QueryExtensions(); + return true; +} + +bool CWinSystemX11GLESContext::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + m_newGlContext = false; + CWinSystemX11::ResizeWindow(newWidth, newHeight, newLeft, newTop); + CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight); + + if (m_newGlContext) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appSkin = components.GetComponent<CApplicationSkinHandling>(); + appSkin->ReloadSkin(); + } + + return true; +} + +void CWinSystemX11GLESContext::FinishWindowResize(int newWidth, int newHeight) +{ + m_newGlContext = false; + CWinSystemX11::FinishWindowResize(newWidth, newHeight); + CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight); + + if (m_newGlContext) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appSkin = components.GetComponent<CApplicationSkinHandling>(); + appSkin->ReloadSkin(); + } +} + +bool CWinSystemX11GLESContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + m_newGlContext = false; + CWinSystemX11::SetFullScreen(fullScreen, res, blankOtherDisplays); + CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight); + + if (m_newGlContext) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appSkin = components.GetComponent<CApplicationSkinHandling>(); + appSkin->ReloadSkin(); + } + + return true; +} + +bool CWinSystemX11GLESContext::DestroyWindowSystem() +{ + if (m_pGLContext) + m_pGLContext->Destroy(); + return CWinSystemX11::DestroyWindowSystem(); +} + +bool CWinSystemX11GLESContext::DestroyWindow() +{ + if (m_pGLContext) + m_pGLContext->Detach(); + return CWinSystemX11::DestroyWindow(); +} + +XVisualInfo* CWinSystemX11GLESContext::GetVisual() +{ + EGLDisplay eglDisplay; + + PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = + reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(eglGetProcAddress("eglGetPlatformDisplayEXT")); + if (eglGetPlatformDisplayEXT) + { + EGLint attribs[] = + { + EGL_PLATFORM_X11_SCREEN_EXT, m_screen, + EGL_NONE + }; + eglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_X11_EXT,static_cast<EGLNativeDisplayType>(m_dpy), attribs); + } + else + eglDisplay = eglGetDisplay(static_cast<EGLNativeDisplayType>(m_dpy)); + + if (eglDisplay == EGL_NO_DISPLAY) + { + CLog::Log(LOGERROR, "failed to get egl display"); + return nullptr; + } + if (!eglInitialize(eglDisplay, nullptr, nullptr)) + { + CLog::Log(LOGERROR, "failed to initialize egl display"); + return nullptr; + } + + GLint att[] = + { + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_BUFFER_SIZE, 32, + EGL_DEPTH_SIZE, 24, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_NONE + }; + EGLint numConfigs; + EGLConfig eglConfig = 0; + if (!eglChooseConfig(eglDisplay, att, &eglConfig, 1, &numConfigs) || numConfigs == 0) + { + CLog::Log(LOGERROR, "Failed to choose a config {}", eglGetError()); + return nullptr; + } + + XVisualInfo x11_visual_info_template; + memset(&x11_visual_info_template, 0, sizeof(XVisualInfo)); + + if (!eglGetConfigAttrib(eglDisplay, eglConfig, + EGL_NATIVE_VISUAL_ID, reinterpret_cast<EGLint*>(&x11_visual_info_template.visualid))) + { + CLog::Log(LOGERROR, "Failed to query native visual id"); + return nullptr; + } + int num_visuals; + XVisualInfo* visual = + XGetVisualInfo(m_dpy, VisualIDMask, &x11_visual_info_template, &num_visuals); + return visual; +} + +bool CWinSystemX11GLESContext::RefreshGLContext(bool force) +{ + bool success = false; + if (m_pGLContext) + { + success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext); + if (!success) + { + success = m_pGLContext->CreatePB(); + m_newGlContext = true; + } + return success; + } + + m_dpms = std::make_shared<CX11DPMSSupport>(); + VIDEOPLAYER::CProcessInfoX11::Register(); + RETRO::CRPProcessInfoX11::Register(); + RETRO::CRPProcessInfoX11::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES); + CDVDFactoryCodec::ClearHWAccels(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CLinuxRendererGLES::Register(); + + std::string gli = (getenv("KODI_GL_INTERFACE") != nullptr) ? getenv("KODI_GL_INTERFACE") : ""; + + m_pGLContext = new CGLContextEGL(m_dpy, EGL_OPENGL_ES_API); + success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext); + if (!success && gli == "EGL_PB") + { + success = m_pGLContext->CreatePB(); + m_newGlContext = true; + } + + if (!success) + { + delete m_pGLContext; + m_pGLContext = nullptr; + } + return success; +} diff --git a/xbmc/windowing/X11/WinSystemX11GLESContext.h b/xbmc/windowing/X11/WinSystemX11GLESContext.h new file mode 100644 index 0000000..cdb1cc4 --- /dev/null +++ b/xbmc/windowing/X11/WinSystemX11GLESContext.h @@ -0,0 +1,62 @@ +/* + * 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 + +#include "EGL/egl.h" +#include "WinSystemX11.h" +#include "rendering/gles/RenderSystemGLES.h" + +class CGLContextEGL; + +namespace KODI +{ +namespace WINDOWING +{ +namespace X11 +{ + +class CWinSystemX11GLESContext : public CWinSystemX11, public CRenderSystemGLES +{ +public: + CWinSystemX11GLESContext() = default; + virtual ~CWinSystemX11GLESContext() override; + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystem via CWinSystemX11 + CRenderSystemBase* GetRenderSystem() override { return this; } + bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override; + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + void FinishWindowResize(int newWidth, int newHeight) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + bool DestroyWindowSystem() override; + bool DestroyWindow() override; + + bool IsExtSupported(const char* extension) const override; + + EGLDisplay GetEGLDisplay() const; + EGLSurface GetEGLSurface() const; + EGLContext GetEGLContext() const; + EGLConfig GetEGLConfig() const; + +protected: + bool SetWindow(int width, int height, bool fullscreen, const std::string& output, int* winstate = nullptr) override; + void PresentRenderImpl(bool rendered) override; + void SetVSyncImpl(bool enable) override; + bool RefreshGLContext(bool force); + XVisualInfo* GetVisual() override; + + CGLContextEGL* m_pGLContext = nullptr; + bool m_newGlContext; +}; + +} // namespace X11 +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/X11/X11DPMSSupport.cpp b/xbmc/windowing/X11/X11DPMSSupport.cpp new file mode 100644 index 0000000..7512bfa --- /dev/null +++ b/xbmc/windowing/X11/X11DPMSSupport.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2009-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 "X11DPMSSupport.h" + +#include "ServiceBroker.h" +#include "utils/log.h" +#include "windowing/X11/WinSystemX11.h" + +#include <X11/Xlib.h> +#include <X11/extensions/dpms.h> + +using namespace KODI::WINDOWING::X11; + +namespace +{ +// Mapping of PowerSavingMode to X11's mode constants. +const CARD16 X_DPMS_MODES[] = +{ + DPMSModeStandby, + DPMSModeSuspend, + DPMSModeOff +}; +} + +CX11DPMSSupport::CX11DPMSSupport() +{ + CWinSystemX11* winSystem = dynamic_cast<CWinSystemX11*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return; + + Display* dpy = winSystem->GetDisplay(); + if (!dpy) + return; + + int event_base, error_base; // we ignore these + if (!DPMSQueryExtension(dpy, &event_base, &error_base)) + { + CLog::Log(LOGINFO, "DPMS: X11 extension not present, power-saving will not be available"); + return; + } + + if (!DPMSCapable(dpy)) + { + CLog::Log(LOGINFO, "DPMS: display does not support power-saving"); + return; + } + + m_supportedModes.push_back(SUSPEND); // best compromise + m_supportedModes.push_back(OFF); // next best + m_supportedModes.push_back(STANDBY); // rather lame, < 80% power according to DPMS spec +} + +bool CX11DPMSSupport::EnablePowerSaving(PowerSavingMode mode) +{ + CWinSystemX11* winSystem = dynamic_cast<CWinSystemX11*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return false; + + Display* dpy = winSystem->GetDisplay(); + if (!dpy) + return false; + + // This is not needed on my ATI Radeon, but the docs say that DPMSForceLevel + // after a DPMSDisable (from SDL) should not normally work. + DPMSEnable(dpy); + DPMSForceLevel(dpy, X_DPMS_MODES[mode]); + // There shouldn't be any errors if we called DPMSEnable; if they do happen, + // they're asynchronous and messy to detect. + XFlush(dpy); + return true; +} + +bool CX11DPMSSupport::DisablePowerSaving() +{ + CWinSystemX11* winSystem = dynamic_cast<CWinSystemX11*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return false; + + Display* dpy = winSystem->GetDisplay(); + if (!dpy) + return false; + + DPMSForceLevel(dpy, DPMSModeOn); + DPMSDisable(dpy); + XFlush(dpy); + + winSystem->RecreateWindow(); + + return true; +} diff --git a/xbmc/windowing/X11/X11DPMSSupport.h b/xbmc/windowing/X11/X11DPMSSupport.h new file mode 100644 index 0000000..c6ce8df --- /dev/null +++ b/xbmc/windowing/X11/X11DPMSSupport.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009-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 "xbmc/powermanagement/DPMSSupport.h" + +class CX11DPMSSupport : public CDPMSSupport +{ +public: + CX11DPMSSupport(); + ~CX11DPMSSupport() override = default; + +protected: + bool EnablePowerSaving(PowerSavingMode mode) override; + bool DisablePowerSaving() override; +}; diff --git a/xbmc/windowing/X11/XRandR.cpp b/xbmc/windowing/X11/XRandR.cpp new file mode 100644 index 0000000..ab20da3 --- /dev/null +++ b/xbmc/windowing/X11/XRandR.cpp @@ -0,0 +1,518 @@ +/* + * 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 "XRandR.h" + +#include "CompileInfo.h" +#include "threads/SystemClock.h" +#include "utils/StringUtils.h" +#include "utils/XBMCTinyXML.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" + +#include <string.h> + +#include <sys/wait.h> + +#include "PlatformDefs.h" + +#if defined(TARGET_FREEBSD) +#include <sys/types.h> +#include <sys/wait.h> +#endif + +using namespace std::chrono_literals; + +CXRandR::CXRandR(bool query) +{ + m_bInit = false; + m_numScreens = 1; + if (query) + Query(); +} + +bool CXRandR::Query(bool force, bool ignoreoff) +{ + if (!force) + if (m_bInit) + return m_outputs.size() > 0; + + m_bInit = true; + + if (getenv("KODI_BIN_HOME") == NULL) + return false; + + m_outputs.clear(); + // query all screens + // we are happy if at least one screen returns results + bool success = false; + for(unsigned int screennum=0; screennum<m_numScreens; ++screennum) + { + if(Query(force, screennum, ignoreoff)) + success = true; + } + return success; +} + +bool CXRandR::Query(bool force, int screennum, bool ignoreoff) +{ + std::string cmd; + std::string appname = CCompileInfo::GetAppName(); + StringUtils::ToLower(appname); + if (getenv("KODI_BIN_HOME")) + { + cmd = getenv("KODI_BIN_HOME"); + cmd += "/" + appname + "-xrandr"; + cmd = StringUtils::Format("{} -q --screen {}", cmd, screennum); + } + + FILE* file = popen(cmd.c_str(),"r"); + if (!file) + { + CLog::Log(LOGERROR, "CXRandR::Query - unable to execute xrandr tool"); + return false; + } + + + CXBMCTinyXML xmlDoc; + if (!xmlDoc.LoadFile(file, TIXML_DEFAULT_ENCODING)) + { + CLog::Log(LOGERROR, "CXRandR::Query - unable to open xrandr xml"); + pclose(file); + return false; + } + pclose(file); + + TiXmlElement *pRootElement = xmlDoc.RootElement(); + if (atoi(pRootElement->Attribute("id")) != screennum) + { + //! @todo ERROR + return false; + } + + for (TiXmlElement* output = pRootElement->FirstChildElement("output"); output; output = output->NextSiblingElement("output")) + { + XOutput xoutput; + xoutput.name = output->Attribute("name"); + StringUtils::Trim(xoutput.name); + xoutput.isConnected = (StringUtils::CompareNoCase(output->Attribute("connected"), "true") == 0); + xoutput.screen = screennum; + xoutput.w = (output->Attribute("w") != NULL ? atoi(output->Attribute("w")) : 0); + xoutput.h = (output->Attribute("h") != NULL ? atoi(output->Attribute("h")) : 0); + xoutput.x = (output->Attribute("x") != NULL ? atoi(output->Attribute("x")) : 0); + xoutput.y = (output->Attribute("y") != NULL ? atoi(output->Attribute("y")) : 0); + xoutput.crtc = (output->Attribute("crtc") != NULL ? atoi(output->Attribute("crtc")) : 0); + xoutput.wmm = (output->Attribute("wmm") != NULL ? atoi(output->Attribute("wmm")) : 0); + xoutput.hmm = (output->Attribute("hmm") != NULL ? atoi(output->Attribute("hmm")) : 0); + if (output->Attribute("rotation") != NULL && + (StringUtils::CompareNoCase(output->Attribute("rotation"), "left") == 0 || + StringUtils::CompareNoCase(output->Attribute("rotation"), "right") == 0)) + { + xoutput.isRotated = true; + } + else + xoutput.isRotated = false; + + if (!xoutput.isConnected) + continue; + + bool hascurrent = false; + for (TiXmlElement* mode = output->FirstChildElement("mode"); mode; mode = mode->NextSiblingElement("mode")) + { + XMode xmode; + xmode.id = mode->Attribute("id"); + xmode.name = mode->Attribute("name"); + xmode.hz = atof(mode->Attribute("hz")); + xmode.w = atoi(mode->Attribute("w")); + xmode.h = atoi(mode->Attribute("h")); + xmode.isPreferred = (StringUtils::CompareNoCase(mode->Attribute("preferred"), "true") == 0); + xmode.isCurrent = (StringUtils::CompareNoCase(mode->Attribute("current"), "true") == 0); + xoutput.modes.push_back(xmode); + if (xmode.isCurrent) + hascurrent = true; + } + if (hascurrent || !ignoreoff) + m_outputs.push_back(xoutput); + else + CLog::Log(LOGWARNING, "CXRandR::Query - output {} has no current mode, assuming disconnected", + xoutput.name); + } + return m_outputs.size() > 0; +} + +bool CXRandR::TurnOffOutput(const std::string& name) +{ + XOutput *output = GetOutput(name); + if (!output) + return false; + + std::string cmd; + std::string appname = CCompileInfo::GetAppName(); + StringUtils::ToLower(appname); + + if (getenv("KODI_BIN_HOME")) + { + cmd = getenv("KODI_BIN_HOME"); + cmd += "/" + appname + "-xrandr"; + cmd = StringUtils::Format("{} --screen {} --output {} --off", cmd, output->screen, name); + } + + int status = system(cmd.c_str()); + if (status == -1) + return false; + + if (WEXITSTATUS(status) != 0) + return false; + + return true; +} + +bool CXRandR::TurnOnOutput(const std::string& name) +{ + XOutput *output = GetOutput(name); + if (!output) + return false; + + XMode mode = GetCurrentMode(output->name); + if (mode.isCurrent) + return true; + + // get preferred mode + for (unsigned int j = 0; j < m_outputs.size(); j++) + { + if (m_outputs[j].name == output->name) + { + for (unsigned int i = 0; i < m_outputs[j].modes.size(); i++) + { + if (m_outputs[j].modes[i].isPreferred) + { + mode = m_outputs[j].modes[i]; + break; + } + } + } + } + + if (!mode.isPreferred) + return false; + + if (!SetMode(*output, mode)) + return false; + + XbmcThreads::EndTime<> timeout(5s); + while (!timeout.IsTimePast()) + { + if (!Query(true)) + return false; + + output = GetOutput(name); + if (output && output->h > 0) + return true; + + KODI::TIME::Sleep(200ms); + } + + return false; +} + +std::vector<XOutput> CXRandR::GetModes(void) +{ + Query(); + return m_outputs; +} + +void CXRandR::SaveState() +{ + Query(true); +} + +bool CXRandR::SetMode(const XOutput& output, const XMode& mode) +{ + if ((output.name == "" && mode.id == "")) + return true; + + Query(); + + // Make sure the output exists, if not -- complain and exit + bool isOutputFound = false; + XOutput outputFound; + for (size_t i = 0; i < m_outputs.size(); i++) + { + if (m_outputs[i].name == output.name) + { + isOutputFound = true; + outputFound = m_outputs[i]; + } + } + + if (!isOutputFound) + { + CLog::Log(LOGERROR, + "CXRandR::SetMode: asked to change resolution for non existing output: {} mode: {}", + output.name, mode.id); + return false; + } + + // try to find the same exact mode (same id, resolution, hz) + bool isModeFound = false; + XMode modeFound; + for (size_t i = 0; i < outputFound.modes.size(); i++) + { + if (outputFound.modes[i].id == mode.id) + { + if (outputFound.modes[i].w == mode.w && + outputFound.modes[i].h == mode.h && + outputFound.modes[i].hz == mode.hz) + { + isModeFound = true; + modeFound = outputFound.modes[i]; + } + else + { + CLog::Log(LOGERROR, + "CXRandR::SetMode: asked to change resolution for mode that exists but with " + "different w/h/hz: {} mode: {}. Searching for similar modes...", + output.name, mode.id); + break; + } + } + } + + if (!isModeFound) + { + for (size_t i = 0; i < outputFound.modes.size(); i++) + { + if (outputFound.modes[i].w == mode.w && + outputFound.modes[i].h == mode.h && + outputFound.modes[i].hz == mode.hz) + { + isModeFound = true; + modeFound = outputFound.modes[i]; + CLog::Log(LOGWARNING, "CXRandR::SetMode: found alternative mode (same hz): {} mode: {}.", + output.name, outputFound.modes[i].id); + } + } + } + + if (!isModeFound) + { + for (size_t i = 0; i < outputFound.modes.size(); i++) + { + if (outputFound.modes[i].w == mode.w && + outputFound.modes[i].h == mode.h) + { + isModeFound = true; + modeFound = outputFound.modes[i]; + CLog::Log(LOGWARNING, + "CXRandR::SetMode: found alternative mode (different hz): {} mode: {}.", + output.name, outputFound.modes[i].id); + } + } + } + + // Let's try finding a mode that is the same + if (!isModeFound) + { + CLog::Log(LOGERROR, + "CXRandR::SetMode: asked to change resolution for non existing mode: {} mode: {}", + output.name, mode.id); + return false; + } + + m_currentOutput = outputFound.name; + m_currentMode = modeFound.id; + std::string appname = CCompileInfo::GetAppName(); + StringUtils::ToLower(appname); + char cmd[255]; + + if (getenv("KODI_BIN_HOME")) + snprintf(cmd, sizeof(cmd), "%s/%s-xrandr --screen %d --output %s --mode %s", + getenv("KODI_BIN_HOME"),appname.c_str(), + outputFound.screen, outputFound.name.c_str(), modeFound.id.c_str()); + else + return false; + CLog::Log(LOGINFO, "XRANDR: {}", cmd); + int status = system(cmd); + if (status == -1) + return false; + + if (WEXITSTATUS(status) != 0) + return false; + + return true; +} + +XMode CXRandR::GetCurrentMode(const std::string& outputName) +{ + Query(); + XMode result; + + for (unsigned int j = 0; j < m_outputs.size(); j++) + { + if (m_outputs[j].name == outputName || outputName == "") + { + for (unsigned int i = 0; i < m_outputs[j].modes.size(); i++) + { + if (m_outputs[j].modes[i].isCurrent) + { + result = m_outputs[j].modes[i]; + break; + } + } + } + } + + return result; +} + +XMode CXRandR::GetPreferredMode(const std::string& outputName) +{ + Query(); + XMode result; + + for (unsigned int j = 0; j < m_outputs.size(); j++) + { + if (m_outputs[j].name == outputName || outputName == "") + { + for (unsigned int i = 0; i < m_outputs[j].modes.size(); i++) + { + if (m_outputs[j].modes[i].isPreferred) + { + result = m_outputs[j].modes[i]; + break; + } + } + } + } + + return result; +} + +void CXRandR::LoadCustomModeLinesToAllOutputs(void) +{ + Query(); + CXBMCTinyXML xmlDoc; + + if (!xmlDoc.LoadFile("special://xbmc/userdata/ModeLines.xml")) + { + return; + } + + TiXmlElement *pRootElement = xmlDoc.RootElement(); + if (StringUtils::CompareNoCase(pRootElement->Value(), "modelines") != 0) + { + //! @todo ERROR + return; + } + + char cmd[255]; + std::string name; + std::string strModeLine; + + for (TiXmlElement* modeline = pRootElement->FirstChildElement("modeline"); modeline; modeline = modeline->NextSiblingElement("modeline")) + { + name = modeline->Attribute("label"); + StringUtils::Trim(name); + strModeLine = modeline->FirstChild()->Value(); + StringUtils::Trim(strModeLine); + std::string appname = CCompileInfo::GetAppName(); + StringUtils::ToLower(appname); + + if (getenv("KODI_BIN_HOME")) + { + snprintf(cmd, sizeof(cmd), "%s/%s-xrandr --newmode \"%s\" %s > /dev/null 2>&1", getenv("KODI_BIN_HOME"), + appname.c_str(), name.c_str(), strModeLine.c_str()); + if (system(cmd) != 0) + CLog::Log(LOGERROR, "Unable to create modeline \"{}\"", name); + } + + for (unsigned int i = 0; i < m_outputs.size(); i++) + { + if (getenv("KODI_BIN_HOME")) + { + snprintf(cmd, sizeof(cmd), "%s/%s-xrandr --addmode %s \"%s\" > /dev/null 2>&1", getenv("KODI_BIN_HOME"), + appname.c_str(), m_outputs[i].name.c_str(), name.c_str()); + if (system(cmd) != 0) + CLog::Log(LOGERROR, "Unable to add modeline \"{}\"", name); + } + } + } +} + +void CXRandR::SetNumScreens(unsigned int num) +{ + m_numScreens = num; + m_bInit = false; +} + +bool CXRandR::IsOutputConnected(const std::string& name) +{ + bool result = false; + Query(); + + for (unsigned int i = 0; i < m_outputs.size(); ++i) + { + if (m_outputs[i].name == name) + { + result = true; + break; + } + } + return result; +} + +XOutput* CXRandR::GetOutput(const std::string& outputName) +{ + XOutput *result = 0; + Query(); + for (unsigned int i = 0; i < m_outputs.size(); ++i) + { + if (m_outputs[i].name == outputName) + { + result = &m_outputs[i]; + break; + } + } + return result; +} + +int CXRandR::GetCrtc(int x, int y, float &hz) +{ + int crtc = 0; + for (unsigned int i = 0; i < m_outputs.size(); ++i) + { + if (!m_outputs[i].isConnected) + continue; + + if ((m_outputs[i].x <= x && (m_outputs[i].x+m_outputs[i].w) > x) && + (m_outputs[i].y <= y && (m_outputs[i].y+m_outputs[i].h) > y)) + { + crtc = m_outputs[i].crtc; + for (const auto& mode : m_outputs[i].modes) + { + if (mode.isCurrent) + { + hz = mode.hz; + break; + } + } + break; + } + } + return crtc; +} + +CXRandR g_xrandr; + +/* + int main() + { + CXRandR r; + r.LoadCustomModeLinesToAllOutputs(); + } +*/ diff --git a/xbmc/windowing/X11/XRandR.h b/xbmc/windowing/X11/XRandR.h new file mode 100644 index 0000000..6f5f74c --- /dev/null +++ b/xbmc/windowing/X11/XRandR.h @@ -0,0 +1,109 @@ +/* + * 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 + +#include <map> +#include <string> +#include <vector> + +class XMode +{ +public: + XMode() + { + hz=0.0f; + isPreferred=false; + isCurrent=false; + w=h=0; + } + bool operator==(XMode& mode) const + { + if (id != mode.id) + return false; + if (name != mode.name) + return false; + if (hz != mode.hz) + return false; + if (isPreferred != mode.isPreferred) + return false; + if (isCurrent != mode.isCurrent) + return false; + if (w != mode.w) + return false; + if (h != mode.h) + return false; + return true; + } + bool IsInterlaced() + { + return name.back() == 'i'; + } + std::string id; + std::string name; + float hz; + bool isPreferred; + bool isCurrent; + unsigned int w; + unsigned int h; +}; + +class XOutput +{ +public: + XOutput() + { + isConnected = false; + w = h = x = y = wmm = hmm = 0; + } + std::string name; + bool isConnected; + int screen; + int w; + int h; + int x; + int y; + int crtc; + int wmm; + int hmm; + std::vector<XMode> modes; + bool isRotated; +}; + +class CXRandR +{ +public: + explicit CXRandR(bool query=false); + bool Query(bool force=false, bool ignoreoff=true); + bool Query(bool force, int screennum, bool ignoreoff=true); + std::vector<XOutput> GetModes(void); + XMode GetCurrentMode(const std::string& outputName); + XMode GetPreferredMode(const std::string& outputName); + XOutput *GetOutput(const std::string& outputName); + bool SetMode(const XOutput& output, const XMode& mode); + void LoadCustomModeLinesToAllOutputs(void); + void SaveState(); + void SetNumScreens(unsigned int num); + bool IsOutputConnected(const std::string& name); + bool TurnOffOutput(const std::string& name); + bool TurnOnOutput(const std::string& name); + int GetCrtc(int x, int y, float &hz); + //bool Has1080i(); + //bool Has1080p(); + //bool Has720p(); + //bool Has480p(); + +private: + bool m_bInit; + std::vector<XOutput> m_outputs; + std::string m_currentOutput; + std::string m_currentMode; + unsigned int m_numScreens; +}; + +extern CXRandR g_xrandr; diff --git a/xbmc/windowing/XBMC_events.h b/xbmc/windowing/XBMC_events.h new file mode 100644 index 0000000..d9bf3ea --- /dev/null +++ b/xbmc/windowing/XBMC_events.h @@ -0,0 +1,134 @@ +/* + * SDL - Simple DirectMedia Layer + * Copyright (C) 1997-2009 Sam Lantinga + * Sam Lantinga + * slouken@libsdl.org + * + * 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 + +/* Include file for SDL event handling */ + +#include "Resolution.h" +#include "input/XBMC_keyboard.h" + +/* Event enumerations */ +typedef enum +{ + XBMC_NOEVENT = 0, /* Unused (do not remove) */ + XBMC_KEYDOWN, /* Keys pressed */ + XBMC_KEYUP, /* Keys released */ + XBMC_MOUSEMOTION, /* Mouse moved */ + XBMC_MOUSEBUTTONDOWN, /* Mouse button pressed */ + XBMC_MOUSEBUTTONUP, /* Mouse button released */ + XBMC_QUIT, /* User-requested quit */ + XBMC_VIDEORESIZE, /* User resized video mode */ + XBMC_FULLSCREEN_UPDATE, /* Triggered by an OS event when Kodi is running in fullscreen, rescale and repositioning is required */ + XBMC_VIDEOMOVE, /* User moved the window */ + XBMC_MODECHANGE, /* Video mode must be changed */ + XBMC_TOUCH, + XBMC_BUTTON, /* Button (remote) pressed */ + XBMC_SETFOCUS, + XBMC_USEREVENT, + + XBMC_MAXEVENT = 256 /* XBMC_EventType is represented as uchar */ +} XBMC_EventType; + +/* Keyboard event structure */ +typedef struct XBMC_KeyboardEvent { + XBMC_keysym keysym; +} XBMC_KeyboardEvent; + +/* Mouse motion event structure */ +typedef struct XBMC_MouseMotionEvent { + uint16_t x, y; /* The X/Y coordinates of the mouse */ +} XBMC_MouseMotionEvent; + +/* Mouse button event structure */ +typedef struct XBMC_MouseButtonEvent { + unsigned char button; /* The mouse button index */ + uint16_t x, y; /* The X/Y coordinates of the mouse at press time */ +} XBMC_MouseButtonEvent; + +/* The "window resized" event + When you get this event, you are responsible for setting a new video + mode with the new width and height. + */ +typedef struct XBMC_ResizeEvent { + int w; /* New width */ + int h; /* New height */ +} XBMC_ResizeEvent; + +typedef struct XBMC_MoveEvent { + int x; /* New x position */ + int y; /* New y position */ +} XBMC_MoveEvent; + +struct XBMC_ModeChangeEvent +{ + RESOLUTION res; +}; + +/* The "quit requested" event */ +typedef struct XBMC_QuitEvent { +} XBMC_QuitEvent; + +/* A user-defined event type */ +typedef struct XBMC_UserEvent { + int code; /* User defined event code */ + void *data1; /* User defined data pointer */ + void *data2; /* User defined data pointer */ +} XBMC_UserEvent; + +/* Multimedia keys on keyboards / remotes are mapped to APPCOMMAND events */ +typedef struct XBMC_AppCommandEvent { + unsigned int action; /* One of ACTION_... */ +} XBMC_AppCommandEvent; + +/* Mouse motion event structure */ +typedef struct XBMC_TouchEvent { + int action; /* action ID */ + float x, y; /* The X/Y coordinates of the mouse */ + float x2, y2; /* Additional X/Y coordinates */ + float x3, y3; /* Additional X/Y coordinates */ + int pointers; /* number of touch pointers */ +} XBMC_TouchEvent; + +typedef struct XBMC_SetFocusEvent { + int x; /* x position */ + int y; /* y position */ +} XBMC_SetFocusEvent; + +/* Button event structure */ +typedef struct XBMC_ButtonEvent +{ + uint32_t button; + uint32_t holdtime; +} XBMC_ButtonEvent; + +/* General event structure */ +typedef struct XBMC_Event { + uint8_t type; + union + { + XBMC_KeyboardEvent key; + XBMC_MouseMotionEvent motion; + XBMC_MouseButtonEvent button; + XBMC_ResizeEvent resize; + XBMC_MoveEvent move; + XBMC_ModeChangeEvent mode; + XBMC_QuitEvent quit; + XBMC_UserEvent user; + XBMC_AppCommandEvent appcommand; + XBMC_TouchEvent touch; + XBMC_ButtonEvent keybutton; + XBMC_SetFocusEvent focus; + }; +} XBMC_Event; + diff --git a/xbmc/windowing/android/AndroidUtils.cpp b/xbmc/windowing/android/AndroidUtils.cpp new file mode 100644 index 0000000..8c61fae --- /dev/null +++ b/xbmc/windowing/android/AndroidUtils.cpp @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2011-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 "AndroidUtils.h" + +#include "ServiceBroker.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/SettingsManager.h" +#include "utils/StringUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +#include "platform/android/activity/XBMCApp.h" + +#include <androidjni/MediaCodecInfo.h> +#include <androidjni/MediaCodecList.h> +#include <androidjni/System.h> +#include <androidjni/SystemProperties.h> +#include <androidjni/View.h> +#include <androidjni/Window.h> +#include <androidjni/WindowManager.h> + +static bool s_hasModeApi = false; +static std::vector<RESOLUTION_INFO> s_res_displayModes; +static RESOLUTION_INFO s_res_cur_displayMode; + +static float currentRefreshRate() +{ + if (s_hasModeApi) + return s_res_cur_displayMode.fRefreshRate; + + CJNIWindow window = CXBMCApp::getWindow(); + if (window) + { + float preferredRate = window.getAttributes().getpreferredRefreshRate(); + if (preferredRate > 20.0f) + { + CLog::Log(LOGINFO, "CAndroidUtils: Preferred refresh rate: {:f}", preferredRate); + return preferredRate; + } + CJNIView view(window.getDecorView()); + if (view) + { + CJNIDisplay display(view.getDisplay()); + if (display) + { + float reportedRate = display.getRefreshRate(); + if (reportedRate > 20.0f) + { + CLog::Log(LOGINFO, "CAndroidUtils: Current display refresh rate: {:f}", reportedRate); + return reportedRate; + } + } + } + } + CLog::Log(LOGDEBUG, "found no refresh rate"); + return 60.0; +} + +static void fetchDisplayModes() +{ + s_hasModeApi = false; + s_res_displayModes.clear(); + + CJNIDisplay display = CXBMCApp::getWindow().getDecorView().getDisplay(); + + if (display) + { + CJNIDisplayMode m = display.getMode(); + if (m) + { + if (m.getPhysicalWidth() > m.getPhysicalHeight()) // Assume unusable if portrait is returned + { + s_hasModeApi = true; + + CLog::Log(LOGDEBUG, "CAndroidUtils: current mode: {}: {}x{}@{:f}", m.getModeId(), + m.getPhysicalWidth(), m.getPhysicalHeight(), m.getRefreshRate()); + s_res_cur_displayMode.strId = std::to_string(m.getModeId()); + s_res_cur_displayMode.iWidth = s_res_cur_displayMode.iScreenWidth = m.getPhysicalWidth(); + s_res_cur_displayMode.iHeight = s_res_cur_displayMode.iScreenHeight = m.getPhysicalHeight(); + s_res_cur_displayMode.fRefreshRate = m.getRefreshRate(); + s_res_cur_displayMode.dwFlags = D3DPRESENTFLAG_PROGRESSIVE; + s_res_cur_displayMode.bFullScreen = true; + s_res_cur_displayMode.iSubtitles = s_res_cur_displayMode.iHeight; + s_res_cur_displayMode.fPixelRatio = 1.0f; + s_res_cur_displayMode.strMode = StringUtils::Format( + "{}x{} @ {:.6f}{} - Full Screen", s_res_cur_displayMode.iScreenWidth, + s_res_cur_displayMode.iScreenHeight, s_res_cur_displayMode.fRefreshRate, + s_res_cur_displayMode.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : ""); + + std::vector<CJNIDisplayMode> modes = display.getSupportedModes(); + for (CJNIDisplayMode& m : modes) + { + CLog::Log(LOGDEBUG, "CAndroidUtils: available mode: {}: {}x{}@{:f}", m.getModeId(), + m.getPhysicalWidth(), m.getPhysicalHeight(), m.getRefreshRate()); + + RESOLUTION_INFO res; + res.strId = std::to_string(m.getModeId()); + res.iWidth = res.iScreenWidth = m.getPhysicalWidth(); + res.iHeight = res.iScreenHeight = m.getPhysicalHeight(); + res.fRefreshRate = m.getRefreshRate(); + res.dwFlags = D3DPRESENTFLAG_PROGRESSIVE; + res.bFullScreen = true; + res.iSubtitles = res.iHeight; + res.fPixelRatio = 1.0f; + res.strMode = StringUtils::Format("{}x{} @ {:.6f}{} - Full Screen", res.iScreenWidth, + res.iScreenHeight, res.fRefreshRate, + res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : ""); + + s_res_displayModes.push_back(res); + } + } + } + } +} + +namespace +{ +const std::map<int, std::string> hdrTypeMap = { + {CAndroidUtils::HDRTypes::HDR10, "HDR10"}, + {CAndroidUtils::HDRTypes::HLG, "HLG"}, + {CAndroidUtils::HDRTypes::HDR10_PLUS, "HDR10+"}, + {CAndroidUtils::HDRTypes::DOLBY_VISION, "Dolby Vision"}}; + +std::string HdrTypeString(int type) +{ + auto hdr = hdrTypeMap.find(type); + if (hdr != hdrTypeMap.end()) + return hdr->second; + + return "Unknown"; +} +} // unnamed namespace + +const std::string CAndroidUtils::SETTING_LIMITGUI = "videoscreen.limitgui"; + +CAndroidUtils::CAndroidUtils() +{ + std::string displaySize; + m_width = m_height = 0; + + if (CJNIBase::GetSDKVersion() >= 23) + { + fetchDisplayModes(); + for (const RESOLUTION_INFO& res : s_res_displayModes) + { + if (res.iWidth > m_width || res.iHeight > m_height) + { + m_width = res.iWidth; + m_height = res.iHeight; + } + } + } + + if (!m_width || !m_height) + { + // Property available on some devices + displaySize = CJNISystemProperties::get("sys.display-size", ""); + if (!displaySize.empty()) + { + std::vector<std::string> aSize = StringUtils::Split(displaySize, "x"); + if (aSize.size() == 2) + { + m_width = StringUtils::IsInteger(aSize[0]) ? atoi(aSize[0].c_str()) : 0; + m_height = StringUtils::IsInteger(aSize[1]) ? atoi(aSize[1].c_str()) : 0; + } + CLog::Log(LOGDEBUG, "CAndroidUtils: display-size: {}({}x{})", displaySize, m_width, m_height); + } + } + + CLog::Log(LOGDEBUG, "CAndroidUtils: maximum/current resolution: {}x{}", m_width, m_height); + int limit = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CAndroidUtils::SETTING_LIMITGUI); + switch (limit) + { + case 0: // auto + m_width = 0; + m_height = 0; + break; + + case 9999: // unlimited + break; + + case 720: + if (m_height > 720) + { + m_width = 1280; + m_height = 720; + } + break; + + case 1080: + if (m_height > 1080) + { + m_width = 1920; + m_height = 1080; + } + break; + } + CLog::Log(LOGDEBUG, "CAndroidUtils: selected resolution: {}x{}", m_width, m_height); + + CServiceBroker::GetSettingsComponent()->GetSettings()->GetSettingsManager()->RegisterCallback( + this, {CAndroidUtils::SETTING_LIMITGUI}); + + LogDisplaySupportedHdrTypes(); +} + +bool CAndroidUtils::GetNativeResolution(RESOLUTION_INFO* res) const +{ + const std::shared_ptr<CNativeWindow> nativeWindow = CXBMCApp::Get().GetNativeWindow(30000); + if (!nativeWindow) + return false; + + if (!m_width || !m_height) + { + m_width = nativeWindow->GetWidth(); + m_height = nativeWindow->GetHeight(); + CLog::Log(LOGINFO, "CAndroidUtils: window resolution: {}x{}", m_width, m_height); + } + + if (s_hasModeApi) + { + *res = s_res_cur_displayMode; + res->iWidth = m_width; + res->iHeight = m_height; + } + else + { + res->strId = "-1"; + res->fRefreshRate = currentRefreshRate(); + res->dwFlags = D3DPRESENTFLAG_PROGRESSIVE; + res->bFullScreen = true; + res->iWidth = m_width; + res->iHeight = m_height; + res->fPixelRatio = 1.0f; + res->iScreenWidth = res->iWidth; + res->iScreenHeight = res->iHeight; + } + res->iSubtitles = res->iHeight; + res->strMode = + StringUtils::Format("{}x{} @ {:.6f}{} - Full Screen", res->iScreenWidth, res->iScreenHeight, + res->fRefreshRate, res->dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : ""); + CLog::Log(LOGINFO, "CAndroidUtils: Current resolution: {}x{} {}", res->iWidth, res->iHeight, + res->strMode); + return true; +} + +bool CAndroidUtils::SetNativeResolution(const RESOLUTION_INFO& res) +{ + CLog::Log(LOGINFO, "CAndroidUtils: SetNativeResolution: {}: {}x{} {}x{}@{:f}", res.strId, + res.iWidth, res.iHeight, res.iScreenWidth, res.iScreenHeight, res.fRefreshRate); + + if (s_hasModeApi) + { + CXBMCApp::Get().SetDisplayMode(std::atoi(res.strId.c_str()), res.fRefreshRate); + s_res_cur_displayMode = res; + } + else + CXBMCApp::Get().SetRefreshRate(res.fRefreshRate); + + CXBMCApp::Get().SetBuffersGeometry(res.iWidth, res.iHeight, 0); + + return true; +} + +bool CAndroidUtils::ProbeResolutions(std::vector<RESOLUTION_INFO>& resolutions) +{ + RESOLUTION_INFO cur_res; + bool ret = GetNativeResolution(&cur_res); + + CLog::Log(LOGDEBUG, "CAndroidUtils: ProbeResolutions: {}x{}", m_width, m_height); + + if (s_hasModeApi) + { + for (RESOLUTION_INFO res : s_res_displayModes) + { + if (m_width && m_height) + { + res.iWidth = std::min(res.iWidth, m_width); + res.iHeight = std::min(res.iHeight, m_height); + res.iSubtitles = res.iHeight; + } + resolutions.push_back(res); + } + return true; + } + + if (ret && cur_res.iWidth > 1 && cur_res.iHeight > 1) + { + std::vector<float> refreshRates; + CJNIWindow window = CXBMCApp::getWindow(); + if (window) + { + CJNIView view = window.getDecorView(); + if (view) + { + CJNIDisplay display = view.getDisplay(); + if (display) + { + refreshRates = display.getSupportedRefreshRates(); + } + } + + if (!refreshRates.empty()) + { + for (unsigned int i = 0; i < refreshRates.size(); i++) + { + if (refreshRates[i] < 20.0f) + continue; + cur_res.fRefreshRate = refreshRates[i]; + cur_res.strMode = StringUtils::Format( + "{}x{} @ {:.6f}{} - Full Screen", cur_res.iScreenWidth, cur_res.iScreenHeight, + cur_res.fRefreshRate, cur_res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : ""); + resolutions.push_back(cur_res); + } + } + } + if (resolutions.empty()) + { + /* No valid refresh rates available, just provide the current one */ + resolutions.push_back(cur_res); + } + return true; + } + return false; +} + +bool CAndroidUtils::UpdateDisplayModes() +{ + if (CJNIBase::GetSDKVersion() >= 23) + fetchDisplayModes(); + return true; +} + +bool CAndroidUtils::IsHDRDisplay() +{ + CJNIWindow window = CXBMCApp::getWindow(); + bool ret = false; + + if (window) + { + CJNIView view = window.getDecorView(); + if (view) + { + CJNIDisplay display = view.getDisplay(); + if (display) + ret = display.isHdr(); + } + } + CLog::Log(LOGDEBUG, "CAndroidUtils: IsHDRDisplay: {}", ret ? "true" : "false"); + return ret; +} + +void CAndroidUtils::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + const std::string& settingId = setting->GetId(); + /* Calibration (overscan / subtitles) are based on GUI size -> reset required */ + if (settingId == CAndroidUtils::SETTING_LIMITGUI) + CDisplaySettings::GetInstance().ClearCalibrations(); +} + +std::vector<int> CAndroidUtils::GetDisplaySupportedHdrTypes() +{ + CJNIWindow window = CXBMCApp::getWindow(); + + if (window) + { + CJNIView view = window.getDecorView(); + if (view) + { + CJNIDisplay display = view.getDisplay(); + if (display) + { + CJNIDisplayHdrCapabilities caps = display.getHdrCapabilities(); + if (caps) + return caps.getSupportedHdrTypes(); + } + } + } + + return {}; +} + +void CAndroidUtils::LogDisplaySupportedHdrTypes() +{ + const std::vector<int> hdrTypes = GetDisplaySupportedHdrTypes(); + std::string text; + + for (const int& type : hdrTypes) + { + text += " " + HdrTypeString(type); + } + + CLog::Log(LOGDEBUG, "CAndroidUtils: Display supported HDR types:{}", + text.empty() ? " None" : text); +} + +CHDRCapabilities CAndroidUtils::GetDisplayHDRCapabilities() +{ + CHDRCapabilities caps; + const std::vector<int> types = GetDisplaySupportedHdrTypes(); + + if (std::find(types.begin(), types.end(), CAndroidUtils::HDRTypes::HDR10) != types.end()) + caps.SetHDR10(); + + if (std::find(types.begin(), types.end(), CAndroidUtils::HDRTypes::HLG) != types.end()) + caps.SetHLG(); + + if (std::find(types.begin(), types.end(), CAndroidUtils::HDRTypes::HDR10_PLUS) != types.end()) + caps.SetHDR10Plus(); + + if (std::find(types.begin(), types.end(), CAndroidUtils::HDRTypes::DOLBY_VISION) != types.end()) + caps.SetDolbyVision(); + + return caps; +} + +bool CAndroidUtils::SupportsMediaCodecMimeType(const std::string& mimeType) +{ + const std::vector<CJNIMediaCodecInfo> codecInfos = + CJNIMediaCodecList(CJNIMediaCodecList::REGULAR_CODECS).getCodecInfos(); + + for (const CJNIMediaCodecInfo& codec_info : codecInfos) + { + if (codec_info.isEncoder()) + continue; + + std::vector<std::string> types = codec_info.getSupportedTypes(); + if (std::find(types.begin(), types.end(), mimeType) != types.end()) + return true; + } + + return false; +} diff --git a/xbmc/windowing/android/AndroidUtils.h b/xbmc/windowing/android/AndroidUtils.h new file mode 100644 index 0000000..426baee --- /dev/null +++ b/xbmc/windowing/android/AndroidUtils.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011-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 "settings/lib/ISettingCallback.h" +#include "utils/HDRCapabilities.h" +#include "windowing/Resolution.h" + +#include <string> +#include <vector> + +#include <androidjni/Display.h> + +class CAndroidUtils : public ISettingCallback +{ +public: + CAndroidUtils(); + ~CAndroidUtils() override = default; + bool GetNativeResolution(RESOLUTION_INFO* res) const; + bool SetNativeResolution(const RESOLUTION_INFO& res); + bool ProbeResolutions(std::vector<RESOLUTION_INFO>& resolutions); + bool UpdateDisplayModes(); + bool IsHDRDisplay(); + + // Implementation of ISettingCallback + static const std::string SETTING_LIMITGUI; + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + + static bool SupportsMediaCodecMimeType(const std::string& mimeType); + + // Android specific HDR type mapping + // https://developer.android.com/reference/android/view/Display.HdrCapabilities#constants_1 + enum HDRTypes + { + DOLBY_VISION = 1, + HDR10 = 2, + HLG = 3, + HDR10_PLUS = 4 + }; + + static std::vector<int> GetDisplaySupportedHdrTypes(); + static CHDRCapabilities GetDisplayHDRCapabilities(); + +protected: + mutable int m_width; + mutable int m_height; + +private: + static void LogDisplaySupportedHdrTypes(); +}; diff --git a/xbmc/windowing/android/CMakeLists.txt b/xbmc/windowing/android/CMakeLists.txt new file mode 100644 index 0000000..fd489d1 --- /dev/null +++ b/xbmc/windowing/android/CMakeLists.txt @@ -0,0 +1,18 @@ +set(SOURCES OSScreenSaverAndroid.cpp + WinEventsAndroid.cpp + WinSystemAndroid.cpp + AndroidUtils.cpp + VideoSyncAndroid.cpp) + +set(HEADERS OSScreenSaverAndroid.h + WinEventsAndroid.h + WinSystemAndroid.h + AndroidUtils.h + VideoSyncAndroid.h) + +if(OPENGLES_FOUND) + list(APPEND SOURCES WinSystemAndroidGLESContext.cpp) + list(APPEND HEADERS WinSystemAndroidGLESContext.h) +endif() + +core_add_library(windowing_android) diff --git a/xbmc/windowing/android/OSScreenSaverAndroid.cpp b/xbmc/windowing/android/OSScreenSaverAndroid.cpp new file mode 100644 index 0000000..aa6dfc5 --- /dev/null +++ b/xbmc/windowing/android/OSScreenSaverAndroid.cpp @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2018 Chris Browet + * 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 "OSScreenSaverAndroid.h" + +#include "platform/android/activity/XBMCApp.h" + +void COSScreenSaverAndroid::Inhibit() +{ + CXBMCApp::Get().EnableWakeLock(true); +} + +void COSScreenSaverAndroid::Uninhibit() +{ + CXBMCApp::Get().EnableWakeLock(false); +} diff --git a/xbmc/windowing/android/OSScreenSaverAndroid.h b/xbmc/windowing/android/OSScreenSaverAndroid.h new file mode 100644 index 0000000..1b67d85 --- /dev/null +++ b/xbmc/windowing/android/OSScreenSaverAndroid.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2018 Chris Browet + * 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" + +class COSScreenSaverAndroid : public KODI::WINDOWING::IOSScreenSaver +{ +public: + explicit COSScreenSaverAndroid() = default; + void Inhibit() override; + void Uninhibit() override; +}; diff --git a/xbmc/windowing/android/VideoSyncAndroid.cpp b/xbmc/windowing/android/VideoSyncAndroid.cpp new file mode 100644 index 0000000..cee6f3e --- /dev/null +++ b/xbmc/windowing/android/VideoSyncAndroid.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015-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 "VideoSyncAndroid.h" + +#include "ServiceBroker.h" +#include "cores/VideoPlayer/VideoReferenceClock.h" +#include "utils/MathUtils.h" +#include "utils/TimeUtils.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/WinSystem.h" + +#include "platform/android/activity/XBMCApp.h" + + +bool CVideoSyncAndroid::Setup(PUPDATECLOCK func) +{ + CLog::Log(LOGDEBUG, "CVideoSyncAndroid::{} setting up", __FUNCTION__); + + //init the vblank timestamp + m_LastVBlankTime = CurrentHostCounter(); + UpdateClock = func; + m_abortEvent.Reset(); + + CXBMCApp::Get().InitFrameCallback(this); + CServiceBroker::GetWinSystem()->Register(this); + + return true; +} + +void CVideoSyncAndroid::Run(CEvent& stopEvent) +{ + XbmcThreads::CEventGroup waitGroup{&stopEvent, &m_abortEvent}; + waitGroup.wait(); +} + +void CVideoSyncAndroid::Cleanup() +{ + CLog::Log(LOGDEBUG, "CVideoSyncAndroid::{} cleaning up", __FUNCTION__); + CXBMCApp::Get().DeinitFrameCallback(); + CServiceBroker::GetWinSystem()->Unregister(this); +} + +float CVideoSyncAndroid::GetFps() +{ + m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS(); + CLog::Log(LOGDEBUG, "CVideoSyncAndroid::{} Detected refreshrate: {:f} hertz", __FUNCTION__, + m_fps); + return m_fps; +} + +void CVideoSyncAndroid::OnResetDisplay() +{ + m_abortEvent.Set(); +} + +void CVideoSyncAndroid::FrameCallback(int64_t frameTimeNanos) +{ + int NrVBlanks; + double VBlankTime; + + //calculate how many vblanks happened + VBlankTime = (double)(frameTimeNanos - m_LastVBlankTime) / (double)CurrentHostFrequency(); + NrVBlanks = MathUtils::round_int(VBlankTime * static_cast<double>(m_fps)); + + //save the timestamp of this vblank so we can calculate how many happened next time + m_LastVBlankTime = frameTimeNanos; + + //update the vblank timestamp, update the clock and send a signal that we got a vblank + UpdateClock(NrVBlanks, frameTimeNanos, m_refClock); +} diff --git a/xbmc/windowing/android/VideoSyncAndroid.h b/xbmc/windowing/android/VideoSyncAndroid.h new file mode 100644 index 0000000..6bf76c0 --- /dev/null +++ b/xbmc/windowing/android/VideoSyncAndroid.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015-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 "guilib/DispResource.h" +#include "windowing/VideoSync.h" + +class CVideoSyncAndroid : public CVideoSync, IDispResource +{ +public: + CVideoSyncAndroid(void *clock) : CVideoSync(clock), m_LastVBlankTime(0) {} + + // CVideoSync interface + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stop) override; + void Cleanup() override; + float GetFps() override; + + // IDispResource interface + void OnResetDisplay() override; + + // Choreographer callback + void FrameCallback(int64_t frameTimeNanos); + +private: + int64_t m_LastVBlankTime; //timestamp of the last vblank, used for calculating how many vblanks happened + CEvent m_abortEvent; +}; diff --git a/xbmc/windowing/android/WinEventsAndroid.cpp b/xbmc/windowing/android/WinEventsAndroid.cpp new file mode 100644 index 0000000..a384902 --- /dev/null +++ b/xbmc/windowing/android/WinEventsAndroid.cpp @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2010-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 "WinEventsAndroid.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "input/InputManager.h" +#include "input/XBMC_vkeys.h" +#include "utils/log.h" + +#include <mutex> + +#define ALMOST_ZERO 0.125f +enum { + EVENT_STATE_TEST, + EVENT_STATE_HOLD, + EVENT_STATE_REPEAT +}; + +/************************************************************************/ +/************************************************************************/ +static bool different_event(XBMC_Event &curEvent, XBMC_Event &newEvent) +{ + // different type + if (curEvent.type != newEvent.type) + return true; + + return false; +} + +/************************************************************************/ +/************************************************************************/ +CWinEventsAndroid::CWinEventsAndroid() +: CThread("CWinEventsAndroid") +{ + CLog::Log(LOGDEBUG, "CWinEventsAndroid::CWinEventsAndroid"); + Create(); +} + +CWinEventsAndroid::~CWinEventsAndroid() +{ + m_bStop = true; + StopThread(true); +} + +void CWinEventsAndroid::MessagePush(XBMC_Event *newEvent) +{ + std::unique_lock<CCriticalSection> lock(m_eventsCond); + + m_events.push_back(*newEvent); +} + +void CWinEventsAndroid::MessagePushRepeat(XBMC_Event *repeatEvent) +{ + std::unique_lock<CCriticalSection> lock(m_eventsCond); + + std::list<XBMC_Event>::iterator itt; + for (itt = m_events.begin(); itt != m_events.end(); ++itt) + { + // we have events pending, if we we just + // repush, we might push the repeat event + // in back of a canceling non-active event. + // do not repush if pending are different event. + if (different_event(*itt, *repeatEvent)) + return; + } + + // is a repeat, push it + m_events.push_back(*repeatEvent); +} + +bool CWinEventsAndroid::MessagePump() +{ + bool ret = false; + + // Do not always loop, only pump the initial queued count events. else if ui keep pushing + // events the loop won't finish then it will block xbmc main message loop. + for (size_t pumpEventCount = GetQueueSize(); pumpEventCount > 0; --pumpEventCount) + { + // Pop up only one event per time since in App::OnEvent it may init modal dialog which init + // deeper message loop and call the deeper MessagePump from there. + XBMC_Event pumpEvent; + { + std::unique_lock<CCriticalSection> lock(m_eventsCond); + if (m_events.empty()) + return ret; + pumpEvent = m_events.front(); + m_events.pop_front(); + } + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(pumpEvent); + + if (pumpEvent.type == XBMC_MOUSEBUTTONUP) + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_UNFOCUS_ALL, 0, 0, 0, 0); + } + + return ret; +} + +size_t CWinEventsAndroid::GetQueueSize() +{ + std::unique_lock<CCriticalSection> lock(m_eventsCond); + return m_events.size(); +} + +void CWinEventsAndroid::Process() +{ + uint32_t timeout = 10; + uint32_t holdTimeout = 500; + uint32_t repeatTimeout = 100; + uint32_t repeatDuration = 0; + + XBMC_Event cur_event; + int state = EVENT_STATE_TEST; + while (!m_bStop) + { + // run a 10ms (timeout) wait cycle + CThread::Sleep(std::chrono::milliseconds(timeout)); + + std::unique_lock<CCriticalSection> lock(m_lasteventCond); + + switch(state) + { + default: + case EVENT_STATE_TEST: + // non-active event, eat it + if (!m_lastevent.empty()) + m_lastevent.pop(); + break; + + case EVENT_STATE_HOLD: + repeatDuration += timeout; + if (!m_lastevent.empty()) + { + if (different_event(cur_event, m_lastevent.front())) + { + // different event, cycle back to test + state = EVENT_STATE_TEST; + break; + } + + // same event, eat it + m_lastevent.pop(); + } + + if (repeatDuration >= holdTimeout) + { + CLog::Log(LOGDEBUG, "hold ->repeat, size({}), repeatDuration({})", m_lastevent.size(), + repeatDuration); + state = EVENT_STATE_REPEAT; + } + break; + + case EVENT_STATE_REPEAT: + repeatDuration += timeout; + if (!m_lastevent.empty()) + { + if (different_event(cur_event, m_lastevent.front())) + { + // different event, cycle back to test + state = EVENT_STATE_TEST; + break; + } + + // same event, eat it + m_lastevent.pop(); + } + + if (repeatDuration >= holdTimeout) + { + // this is a repeat, push it + MessagePushRepeat(&cur_event); + // assuming holdTimeout > repeatTimeout, + // just subtract the repeatTimeout + // to get the next cycle time + repeatDuration -= repeatTimeout; + } + break; + } + } +} diff --git a/xbmc/windowing/android/WinEventsAndroid.h b/xbmc/windowing/android/WinEventsAndroid.h new file mode 100644 index 0000000..62b04b5 --- /dev/null +++ b/xbmc/windowing/android/WinEventsAndroid.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2010-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 "threads/Event.h" +#include "threads/Thread.h" +#include "windowing/WinEvents.h" + +#include <list> +#include <queue> +#include <string> +#include <vector> + +class CWinEventsAndroid : public IWinEvents, public CThread +{ +public: + CWinEventsAndroid(); + ~CWinEventsAndroid() override; + + void MessagePush(XBMC_Event *newEvent); + void MessagePushRepeat(XBMC_Event *repeatEvent); + bool MessagePump() override; + +private: + size_t GetQueueSize(); + + // for CThread + void Process() override; + + CCriticalSection m_eventsCond; + std::list<XBMC_Event> m_events; + + CCriticalSection m_lasteventCond; + std::queue<XBMC_Event> m_lastevent; +}; + diff --git a/xbmc/windowing/android/WinSystemAndroid.cpp b/xbmc/windowing/android/WinSystemAndroid.cpp new file mode 100644 index 0000000..d8ddbb0 --- /dev/null +++ b/xbmc/windowing/android/WinSystemAndroid.cpp @@ -0,0 +1,329 @@ +/* + * 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 "WinSystemAndroid.h" + +#include "OSScreenSaverAndroid.h" +#include "ServiceBroker.h" +#include "WinEventsAndroid.h" +#include "addons/interfaces/platform/android/System.h" +#include "cores/RetroPlayer/process/android/RPProcessInfoAndroid.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h" +#include "cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.h" +#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.h" +#include "cores/VideoPlayer/Process/android/ProcessInfoAndroid.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.h" +#include "guilib/DispResource.h" +#include "rendering/gles/ScreenshotSurfaceGLES.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "system_egl.h" +#include "utils/HDRCapabilities.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/Resolution.h" + +#include "platform/android/activity/XBMCApp.h" +#include "platform/android/media/decoderfilter/MediaCodecDecoderFilterManager.h" +#include "platform/android/media/drm/MediaDrmCryptoSession.h" + +#include <float.h> +#include <mutex> +#include <string.h> + +#include <EGL/eglplatform.h> + +using namespace KODI; +using namespace std::chrono_literals; + +CWinSystemAndroid::CWinSystemAndroid() +{ + m_displayWidth = 0; + m_displayHeight = 0; + + m_stereo_mode = RENDER_STEREO_MODE_OFF; + + m_dispResetTimer = new CTimer(this); + + m_android = nullptr; + + m_winEvents.reset(new CWinEventsAndroid()); +} + +CWinSystemAndroid::~CWinSystemAndroid() +{ + delete m_dispResetTimer; +} + +bool CWinSystemAndroid::InitWindowSystem() +{ + m_nativeDisplay = EGL_DEFAULT_DISPLAY; + + m_android = new CAndroidUtils(); + + m_decoderFilterManager = new(CMediaCodecDecoderFilterManager); + CServiceBroker::RegisterDecoderFilterManager(m_decoderFilterManager); + + CDVDVideoCodecAndroidMediaCodec::Register(); + CDVDAudioCodecAndroidMediaCodec::Register(); + + CLinuxRendererGLES::Register(); + RETRO::CRPProcessInfoAndroid::Register(); + RETRO::CRPProcessInfoAndroid::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES); + CRendererMediaCodec::Register(); + CRendererMediaCodecSurface::Register(); + ADDON::Interface_Android::Register(); + DRM::CMediaDrmCryptoSession::Register(); + VIDEOPLAYER::CProcessInfoAndroid::Register(); + + CScreenshotSurfaceGLES::Register(); + + return CWinSystemBase::InitWindowSystem(); +} + +bool CWinSystemAndroid::DestroyWindowSystem() +{ + CLog::Log(LOGINFO, "CWinSystemAndroid::{}", __FUNCTION__); + + delete m_android; + m_android = nullptr; + + CServiceBroker::RegisterDecoderFilterManager(nullptr); + delete m_decoderFilterManager; + m_decoderFilterManager = nullptr; + + return true; +} + +bool CWinSystemAndroid::CreateNewWindow(const std::string& name, + bool fullScreen, + RESOLUTION_INFO& res) +{ + RESOLUTION_INFO current_resolution; + current_resolution.iWidth = current_resolution.iHeight = 0; + RENDER_STEREO_MODE stereo_mode = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode(); + + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_displayWidth = res.iScreenWidth; + m_displayHeight = res.iScreenHeight; + m_fRefreshRate = res.fRefreshRate; + + if ((m_bWindowCreated && m_android->GetNativeResolution(¤t_resolution)) && + current_resolution.iWidth == res.iWidth && current_resolution.iHeight == res.iHeight && + current_resolution.iScreenWidth == res.iScreenWidth && current_resolution.iScreenHeight == res.iScreenHeight && + m_bFullScreen == fullScreen && current_resolution.fRefreshRate == res.fRefreshRate && + (current_resolution.dwFlags & D3DPRESENTFLAG_MODEMASK) == (res.dwFlags & D3DPRESENTFLAG_MODEMASK) && + m_stereo_mode == stereo_mode) + { + CLog::Log(LOGDEBUG, "CWinSystemAndroid::CreateNewWindow: No need to create a new window"); + return true; + } + + m_dispResetTimer->Stop(); + m_HdmiModeTriggered = false; + + m_stereo_mode = stereo_mode; + m_bFullScreen = fullScreen; + + m_nativeWindow = CXBMCApp::Get().GetNativeWindow(2000); + if (!m_nativeWindow) + { + CLog::Log(LOGERROR, "CWinSystemAndroid::CreateNewWindow: failed"); + return false; + } + + m_android->SetNativeResolution(res); + + return true; +} + +bool CWinSystemAndroid::DestroyWindow() +{ + CLog::Log(LOGINFO, "CWinSystemAndroid::{}", __FUNCTION__); + m_nativeWindow.reset(); + m_bWindowCreated = false; + return true; +} + +void CWinSystemAndroid::UpdateResolutions() +{ + UpdateResolutions(true); +} + +void CWinSystemAndroid::UpdateResolutions(bool bUpdateDesktopRes) +{ + CWinSystemBase::UpdateResolutions(); + + std::vector<RESOLUTION_INFO> resolutions; + if (!m_android->ProbeResolutions(resolutions) || resolutions.empty()) + { + CLog::Log(LOGWARNING, "CWinSystemAndroid::{} failed.", __FUNCTION__); + } + + const RESOLUTION_INFO resWindow = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + + RESOLUTION_INFO resDesktop; + if (bUpdateDesktopRes) + { + // ProbeResolutions includes already all resolutions. + // Only get desktop resolution so we can replace Kodi's desktop res. + RESOLUTION_INFO curDisplay; + if (m_android->GetNativeResolution(&curDisplay)) + resDesktop = curDisplay; + } + else + { + // Do not replace Kodi's desktop res, just update the data. + resDesktop = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP); + } + + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + for (auto& res : resolutions) + { + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + + if (resDesktop.iScreenWidth == res.iScreenWidth && + resDesktop.iScreenHeight == res.iScreenHeight && + (resDesktop.dwFlags & D3DPRESENTFLAG_MODEMASK) == (res.dwFlags & D3DPRESENTFLAG_MODEMASK) && + std::fabs(resDesktop.fRefreshRate - res.fRefreshRate) < FLT_EPSILON) + { + CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP) = res; + } + + if (resWindow.iScreenWidth == res.iScreenWidth && + resWindow.iScreenHeight == res.iScreenHeight && + (resWindow.dwFlags & D3DPRESENTFLAG_MODEMASK) == (res.dwFlags & D3DPRESENTFLAG_MODEMASK) && + std::fabs(resWindow.fRefreshRate - res.fRefreshRate) < FLT_EPSILON) + { + CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW) = res; + } + } + + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +void CWinSystemAndroid::OnTimeout() +{ + m_HdmiModeTriggered = true; +} + +void CWinSystemAndroid::InitiateModeChange() +{ + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + + if (delay < 2000ms) + delay = 2000ms; + m_dispResetTimer->Stop(); + m_dispResetTimer->Start(delay); + + SetHdmiState(false); +} + +void CWinSystemAndroid::SetHdmiState(bool connected) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + CLog::Log(LOGDEBUG, "CWinSystemAndroid::SetHdmiState: state: {}", static_cast<int>(connected)); + + if (connected) + { + if (m_dispResetTimer->IsRunning()) + { + // We stop the timer if OS supports HDMI_AUDIO_PLUG intent + // and configured delay is smaller than the time HDMI_PLUG took. + // Note that timer is always started with minimum of 2 seconds + // regardless if the configured delay is smaller + if (m_dispResetTimer->GetElapsedMilliseconds() >= + CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100) + m_dispResetTimer->Stop(); + else + return; + } + + for (auto resource : m_resources) + resource->OnResetDisplay(); + m_dispResetTimer->Stop(); + m_HdmiModeTriggered = false; + } + else + { + for (auto resource : m_resources) + resource->OnLostDisplay(); + } +} + +void CWinSystemAndroid::UpdateDisplayModes() +{ + // re-fetch display modes + m_android->UpdateDisplayModes(); + + if (m_nativeWindow) + { + // update display settings + UpdateResolutions(false); + } +} + +bool CWinSystemAndroid::Hide() +{ + return false; +} + +bool CWinSystemAndroid::Show(bool raise) +{ + return false; +} + +void CWinSystemAndroid::Register(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemAndroid::Unregister(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +void CWinSystemAndroid::MessagePush(XBMC_Event *newEvent) +{ + dynamic_cast<CWinEventsAndroid&>(*m_winEvents).MessagePush(newEvent); +} + +bool CWinSystemAndroid::MessagePump() +{ + return m_winEvents->MessagePump(); +} + +bool CWinSystemAndroid::IsHDRDisplay() +{ + return m_android->IsHDRDisplay(); +} + +std::unique_ptr<WINDOWING::IOSScreenSaver> CWinSystemAndroid::GetOSScreenSaverImpl() +{ + std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> ret(new COSScreenSaverAndroid()); + return ret; +} + +CHDRCapabilities CWinSystemAndroid::GetDisplayHDRCapabilities() const +{ + return CAndroidUtils::GetDisplayHDRCapabilities(); +} diff --git a/xbmc/windowing/android/WinSystemAndroid.h b/xbmc/windowing/android/WinSystemAndroid.h new file mode 100644 index 0000000..0e39193 --- /dev/null +++ b/xbmc/windowing/android/WinSystemAndroid.h @@ -0,0 +1,85 @@ +/* + * 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 + +#include "AndroidUtils.h" +#include "rendering/gles/RenderSystemGLES.h" +#include "system_egl.h" +#include "threads/CriticalSection.h" +#include "threads/Timer.h" +#include "utils/HDRCapabilities.h" +#include "windowing/WinSystem.h" + +#include <memory> + +class CDecoderFilterManager; +class IDispResource; +class CNativeWindow; + +class CWinSystemAndroid : public CWinSystemBase, public ITimerCallback +{ +public: + CWinSystemAndroid(); + ~CWinSystemAndroid() override; + + bool InitWindowSystem() override; + bool DestroyWindowSystem() override; + + bool CreateNewWindow(const std::string& name, + bool fullScreen, + RESOLUTION_INFO& res) override; + + bool DestroyWindow() override; + void UpdateResolutions() override; + + void InitiateModeChange(); + bool IsHdmiModeTriggered() const { return m_HdmiModeTriggered; } + void SetHdmiState(bool connected); + + void UpdateDisplayModes(); + + bool HasCursor() override { return false; } + + bool Hide() override; + bool Show(bool raise = true) override; + void Register(IDispResource *resource) override; + void Unregister(IDispResource *resource) override; + + void MessagePush(XBMC_Event *newEvent); + + // winevents override + bool MessagePump() override; + bool IsHDRDisplay() override; + + CHDRCapabilities GetDisplayHDRCapabilities() const override; + +protected: + std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override; + void OnTimeout() override; + + CAndroidUtils *m_android; + + EGLDisplay m_nativeDisplay = EGL_NO_DISPLAY; + std::shared_ptr<CNativeWindow> m_nativeWindow; + + int m_displayWidth; + int m_displayHeight; + + RENDER_STEREO_MODE m_stereo_mode; + + CTimer *m_dispResetTimer; + + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + CDecoderFilterManager *m_decoderFilterManager; + +private: + bool m_HdmiModeTriggered = false; + void UpdateResolutions(bool bUpdateDesktopRes); +}; diff --git a/xbmc/windowing/android/WinSystemAndroidGLESContext.cpp b/xbmc/windowing/android/WinSystemAndroidGLESContext.cpp new file mode 100644 index 0000000..ac562ae --- /dev/null +++ b/xbmc/windowing/android/WinSystemAndroidGLESContext.cpp @@ -0,0 +1,270 @@ +/* + * 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 "WinSystemAndroidGLESContext.h" + +#include "ServiceBroker.h" +#include "VideoSyncAndroid.h" +#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SingleLock.h" +#include "utils/log.h" +#include "windowing/WindowSystemFactory.h" + +#include "platform/android/activity/XBMCApp.h" + +#include <EGL/eglext.h> +#include <unistd.h> + +#include "PlatformDefs.h" + +void CWinSystemAndroidGLESContext::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem); +} + +std::unique_ptr<CWinSystemBase> CWinSystemAndroidGLESContext::CreateWinSystem() +{ + return std::make_unique<CWinSystemAndroidGLESContext>(); +} + +bool CWinSystemAndroidGLESContext::InitWindowSystem() +{ + if (!CWinSystemAndroid::InitWindowSystem()) + { + return false; + } + + if (!m_pGLContext.CreateDisplay(m_nativeDisplay)) + { + return false; + } + + if (!m_pGLContext.InitializeDisplay(EGL_OPENGL_ES_API)) + { + return false; + } + + if (!m_pGLContext.ChooseConfig(EGL_OPENGL_ES2_BIT)) + { + return false; + } + + m_hasHDRConfig = m_pGLContext.ChooseConfig(EGL_OPENGL_ES2_BIT, 0, true); + + m_hasEGL_BT2020_PQ_Colorspace_Extension = + CEGLUtils::HasExtension(m_pGLContext.GetEGLDisplay(), "EGL_EXT_gl_colorspace_bt2020_pq"); + m_hasEGL_ST2086_Extension = + CEGLUtils::HasExtension(m_pGLContext.GetEGLDisplay(), "EGL_EXT_surface_SMPTE2086_metadata"); + + bool hasEGLHDRExtensions = m_hasEGL_BT2020_PQ_Colorspace_Extension && m_hasEGL_ST2086_Extension; + + CLog::Log(LOGDEBUG, + "CWinSystemAndroidGLESContext::InitWindowSystem: HDRConfig: {}, HDRExtensions: {}", + static_cast<int>(m_hasHDRConfig), static_cast<int>(hasEGLHDRExtensions)); + + CEGLAttributesVec contextAttribs; + contextAttribs.Add({{EGL_CONTEXT_CLIENT_VERSION, 2}}); + + if (!m_pGLContext.CreateContext(contextAttribs)) + { + return false; + } + + return true; +} + +bool CWinSystemAndroidGLESContext::CreateNewWindow(const std::string& name, + bool fullScreen, + RESOLUTION_INFO& res) +{ + m_pGLContext.DestroySurface(); + + if (!CWinSystemAndroid::CreateNewWindow(name, fullScreen, res)) + { + return false; + } + + if (!CreateSurface()) + { + return false; + } + + if (!m_pGLContext.BindContext()) + { + return false; + } + + return true; +} + +bool CWinSystemAndroidGLESContext::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight); + return true; +} + +bool CWinSystemAndroidGLESContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + CreateNewWindow("", fullScreen, res); + CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight); + return true; +} + +void CWinSystemAndroidGLESContext::SetVSyncImpl(bool enable) +{ + // We use Choreographer for timing + m_pGLContext.SetVSync(false); +} + +void CWinSystemAndroidGLESContext::PresentRenderImpl(bool rendered) +{ + if (!m_nativeWindow) + { + usleep(10000); + return; + } + + // Mode change finalization was triggered by timer + if (IsHdmiModeTriggered()) + SetHdmiState(true); + + // Ignore EGL_BAD_SURFACE: It seems to happen during/after mode changes, but + // we can't actually do anything about it + if (rendered && !m_pGLContext.TrySwapBuffers()) + CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed"); + + CXBMCApp::Get().WaitVSync(1000); +} + +float CWinSystemAndroidGLESContext::GetFrameLatencyAdjustment() +{ + return CXBMCApp::Get().GetFrameLatencyMs(); +} + +EGLDisplay CWinSystemAndroidGLESContext::GetEGLDisplay() const +{ + return m_pGLContext.GetEGLDisplay(); +} + +EGLSurface CWinSystemAndroidGLESContext::GetEGLSurface() const +{ + return m_pGLContext.GetEGLSurface(); +} + +EGLContext CWinSystemAndroidGLESContext::GetEGLContext() const +{ + return m_pGLContext.GetEGLContext(); +} + +EGLConfig CWinSystemAndroidGLESContext::GetEGLConfig() const +{ + return m_pGLContext.GetEGLConfig(); +} + +std::unique_ptr<CVideoSync> CWinSystemAndroidGLESContext::GetVideoSync(void *clock) +{ + std::unique_ptr<CVideoSync> pVSync(new CVideoSyncAndroid(clock)); + return pVSync; +} + +bool CWinSystemAndroidGLESContext::CreateSurface() +{ + if (!m_pGLContext.CreateSurface(static_cast<EGLNativeWindowType>(m_nativeWindow->m_window), + m_HDRColorSpace)) + { + if (m_HDRColorSpace != EGL_NONE) + { + m_HDRColorSpace = EGL_NONE; + m_displayMetadata = nullptr; + m_lightMetadata = nullptr; + if (!m_pGLContext.CreateSurface(static_cast<EGLNativeWindowType>(m_nativeWindow->m_window))) + return false; + } + else + return false; + } + +#if EGL_EXT_surface_SMPTE2086_metadata + if (m_displayMetadata) + { + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_RX_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[0][0]) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_RY_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[0][1]) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_GX_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[1][0]) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_GY_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[1][1]) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_BX_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[2][0]) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_BY_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[2][1]) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_WHITE_POINT_X_EXT, static_cast<int>(av_q2d(m_displayMetadata->white_point[0]) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_WHITE_POINT_Y_EXT, static_cast<int>(av_q2d(m_displayMetadata->white_point[1]) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_MAX_LUMINANCE_EXT, static_cast<int>(av_q2d(m_displayMetadata->max_luminance) * EGL_METADATA_SCALING_EXT + 0.5)); + m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_MIN_LUMINANCE_EXT, static_cast<int>(av_q2d(m_displayMetadata->min_luminance) * EGL_METADATA_SCALING_EXT + 0.5)); + } + if (m_lightMetadata) + { + m_pGLContext.SurfaceAttrib(EGL_CTA861_3_MAX_CONTENT_LIGHT_LEVEL_EXT, static_cast<int>(m_lightMetadata->MaxCLL * EGL_METADATA_SCALING_EXT)); + m_pGLContext.SurfaceAttrib(EGL_CTA861_3_MAX_FRAME_AVERAGE_LEVEL_EXT, static_cast<int>(m_lightMetadata->MaxFALL * EGL_METADATA_SCALING_EXT)); + } +#endif + return true; +} + +bool CWinSystemAndroidGLESContext::IsHDRDisplay() +{ + return m_hasHDRConfig && (m_hasEGL_BT2020_PQ_Colorspace_Extension || m_hasEGL_ST2086_Extension) && + CWinSystemAndroid::IsHDRDisplay(); +} + +bool CWinSystemAndroidGLESContext::SetHDR(const VideoPicture* videoPicture) +{ + if (!CWinSystemAndroid::IsHDRDisplay() || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(SETTING_WINSYSTEM_IS_HDR_DISPLAY)) + return false; + + EGLint HDRColorSpace = 0; + +#if EGL_EXT_gl_colorspace_bt2020_linear + if (m_hasHDRConfig && m_hasEGL_BT2020_PQ_Colorspace_Extension && m_hasEGL_ST2086_Extension) + { + HDRColorSpace = EGL_NONE; + if (videoPicture && videoPicture->hasDisplayMetadata) + { + switch (videoPicture->color_space) + { + case AVCOL_SPC_BT2020_NCL: + case AVCOL_SPC_BT2020_CL: + case AVCOL_SPC_BT709: + HDRColorSpace = EGL_GL_COLORSPACE_BT2020_PQ_EXT; + break; + default: + m_displayMetadata = nullptr; + m_lightMetadata = nullptr; + } + } + else + { + m_displayMetadata = nullptr; + m_lightMetadata = nullptr; + } + + if (HDRColorSpace != m_HDRColorSpace) + { + CLog::Log(LOGDEBUG, "CWinSystemAndroidGLESContext::SetHDR: ColorSpace: {}", HDRColorSpace); + + m_HDRColorSpace = HDRColorSpace; + m_displayMetadata = m_HDRColorSpace == EGL_NONE ? nullptr : std::unique_ptr<AVMasteringDisplayMetadata>(new AVMasteringDisplayMetadata(videoPicture->displayMetadata)); + // TODO: discuss with NVIDIA why this prevent turning HDR display off + //m_lightMetadata = !videoPicture || m_HDRColorSpace == EGL_NONE ? nullptr : std::unique_ptr<AVContentLightMetadata>(new AVContentLightMetadata(videoPicture->lightMetadata)); + m_pGLContext.DestroySurface(); + CreateSurface(); + m_pGLContext.BindContext(); + } + } +#endif + + return m_HDRColorSpace == HDRColorSpace; +} diff --git a/xbmc/windowing/android/WinSystemAndroidGLESContext.h b/xbmc/windowing/android/WinSystemAndroidGLESContext.h new file mode 100644 index 0000000..b04ff41 --- /dev/null +++ b/xbmc/windowing/android/WinSystemAndroidGLESContext.h @@ -0,0 +1,63 @@ +/* + * 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 + +#include "WinSystemAndroid.h" +#include "rendering/gles/RenderSystemGLES.h" +#include "utils/EGLUtils.h" +#include "utils/GlobalsHandling.h" + +struct AVMasteringDisplayMetadata; +struct AVContentLightMetadata; + +class CWinSystemAndroidGLESContext : public CWinSystemAndroid, public CRenderSystemGLES +{ +public: + CWinSystemAndroidGLESContext() = default; + ~CWinSystemAndroidGLESContext() override = default; + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystemBase via CWinSystemAndroid + CRenderSystemBase *GetRenderSystem() override { return this; } + bool InitWindowSystem() override; + bool CreateNewWindow(const std::string& name, + bool fullScreen, + RESOLUTION_INFO& res) override; + + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + + std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override; + + float GetFrameLatencyAdjustment() override; + bool IsHDRDisplay() override; + bool SetHDR(const VideoPicture* videoPicture) override; + + EGLDisplay GetEGLDisplay() const; + EGLSurface GetEGLSurface() const; + EGLContext GetEGLContext() const; + EGLConfig GetEGLConfig() const; +protected: + void SetVSyncImpl(bool enable) override; + void PresentRenderImpl(bool rendered) override; + +private: + bool CreateSurface(); + + CEGLContextUtils m_pGLContext; + bool m_hasHDRConfig = false; + + std::unique_ptr<AVMasteringDisplayMetadata> m_displayMetadata; + std::unique_ptr<AVContentLightMetadata> m_lightMetadata; + EGLint m_HDRColorSpace = EGL_NONE; + bool m_hasEGL_ST2086_Extension = false; + bool m_hasEGL_BT2020_PQ_Colorspace_Extension = false; +}; diff --git a/xbmc/windowing/gbm/CMakeLists.txt b/xbmc/windowing/gbm/CMakeLists.txt new file mode 100644 index 0000000..254b2db --- /dev/null +++ b/xbmc/windowing/gbm/CMakeLists.txt @@ -0,0 +1,26 @@ +add_subdirectory(drm) + +set(SOURCES OptionalsReg.cpp + WinSystemGbm.cpp + VideoSyncGbm.cpp + GBMUtils.cpp + WinSystemGbmEGLContext.cpp + GBMDPMSSupport.cpp) + +set(HEADERS OptionalsReg.h + WinSystemGbm.h + VideoSyncGbm.h + GBMUtils.h + WinSystemGbmEGLContext.h + GBMDPMSSupport.h) + +if (OPENGL_FOUND) + list(APPEND SOURCES WinSystemGbmGLContext.cpp) + list(APPEND HEADERS WinSystemGbmGLContext.h) +endif() +if(OPENGLES_FOUND) + list(APPEND SOURCES WinSystemGbmGLESContext.cpp) + list(APPEND HEADERS WinSystemGbmGLESContext.h) +endif() + +core_add_library(windowing_gbm) diff --git a/xbmc/windowing/gbm/GBMDPMSSupport.cpp b/xbmc/windowing/gbm/GBMDPMSSupport.cpp new file mode 100644 index 0000000..6544587 --- /dev/null +++ b/xbmc/windowing/gbm/GBMDPMSSupport.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2009-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 "GBMDPMSSupport.h" + +#include "ServiceBroker.h" +#include "windowing/gbm/WinSystemGbm.h" + +using namespace KODI::WINDOWING::GBM; + +CGBMDPMSSupport::CGBMDPMSSupport() +{ + m_supportedModes.push_back(OFF); +} + +bool CGBMDPMSSupport::EnablePowerSaving(PowerSavingMode mode) +{ + auto winSystem = dynamic_cast<CWinSystemGbm*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return false; + + switch (mode) + { + case OFF: + return winSystem->Hide(); + default: + return false; + } +} + +bool CGBMDPMSSupport::DisablePowerSaving() +{ + auto winSystem = dynamic_cast<CWinSystemGbm*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return false; + + return winSystem->Show(); +} diff --git a/xbmc/windowing/gbm/GBMDPMSSupport.h b/xbmc/windowing/gbm/GBMDPMSSupport.h new file mode 100644 index 0000000..f5fabf8 --- /dev/null +++ b/xbmc/windowing/gbm/GBMDPMSSupport.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2009-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 "powermanagement/DPMSSupport.h" + +#include <memory> + +class CGBMDPMSSupport : public CDPMSSupport +{ +public: + CGBMDPMSSupport(); + ~CGBMDPMSSupport() override = default; + +protected: + bool EnablePowerSaving(PowerSavingMode mode) override; + bool DisablePowerSaving() override; +}; diff --git a/xbmc/windowing/gbm/GBMUtils.cpp b/xbmc/windowing/gbm/GBMUtils.cpp new file mode 100644 index 0000000..5267c93 --- /dev/null +++ b/xbmc/windowing/gbm/GBMUtils.cpp @@ -0,0 +1,107 @@ +/* + * 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 "GBMUtils.h" + +#include "utils/log.h" + +#include <mutex> + +using namespace KODI::WINDOWING::GBM; + +namespace +{ +std::once_flag flag; +} + +bool CGBMUtils::CreateDevice(int fd) +{ + auto device = gbm_create_device(fd); + if (!device) + { + CLog::Log(LOGERROR, "CGBMUtils::{} - failed to create device: {}", __FUNCTION__, + strerror(errno)); + return false; + } + + m_device.reset(new CGBMDevice(device)); + + return true; +} + +CGBMUtils::CGBMDevice::CGBMDevice(gbm_device* device) : m_device(device) +{ +} + +bool CGBMUtils::CGBMDevice::CreateSurface( + int width, int height, uint32_t format, const uint64_t* modifiers, const int modifiers_count) +{ + gbm_surface* surface{nullptr}; +#if defined(HAS_GBM_MODIFIERS) + if (modifiers) + { + surface = gbm_surface_create_with_modifiers(m_device, width, height, format, modifiers, + modifiers_count); + } +#endif + if (!surface) + { + surface = gbm_surface_create(m_device, width, height, format, + GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING); + } + + if (!surface) + { + CLog::Log(LOGERROR, "CGBMUtils::{} - failed to create surface: {}", __FUNCTION__, + strerror(errno)); + return false; + } + + CLog::Log(LOGDEBUG, "CGBMUtils::{} - created surface with size {}x{}", __FUNCTION__, width, + height); + + m_surface.reset(new CGBMSurface(surface)); + + return true; +} + +CGBMUtils::CGBMDevice::CGBMSurface::CGBMSurface(gbm_surface* surface) : m_surface(surface) +{ +} + +CGBMUtils::CGBMDevice::CGBMSurface::CGBMSurfaceBuffer* CGBMUtils::CGBMDevice::CGBMSurface:: + LockFrontBuffer() +{ + m_buffers.emplace(std::make_unique<CGBMSurfaceBuffer>(m_surface)); + + if (!static_cast<bool>(gbm_surface_has_free_buffers(m_surface))) + { + /* + * We want to use call_once here because we want it to be logged the first time that + * we have to release buffers. This means that the maximum amount of buffers had been reached. + * For mesa this should be 4 buffers but it may vary across other implementations. + */ + std::call_once( + flag, [this]() { CLog::Log(LOGDEBUG, "CGBMUtils - using {} buffers", m_buffers.size()); }); + + m_buffers.pop(); + } + + return m_buffers.back().get(); +} + +CGBMUtils::CGBMDevice::CGBMSurface::CGBMSurfaceBuffer::CGBMSurfaceBuffer(gbm_surface* surface) + : m_surface(surface), m_buffer(gbm_surface_lock_front_buffer(surface)) +{ +} + +CGBMUtils::CGBMDevice::CGBMSurface::CGBMSurfaceBuffer::~CGBMSurfaceBuffer() +{ + if (m_surface && m_buffer) + gbm_surface_release_buffer(m_surface, m_buffer); +} diff --git a/xbmc/windowing/gbm/GBMUtils.h b/xbmc/windowing/gbm/GBMUtils.h new file mode 100644 index 0000000..291a93a --- /dev/null +++ b/xbmc/windowing/gbm/GBMUtils.h @@ -0,0 +1,177 @@ +/* + * 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 + +#include <memory> +#include <queue> + +#include <gbm.h> + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +/** + * @brief A wrapper for gbm c classes to allow OOP and RAII. + * + */ +class CGBMUtils +{ +public: + CGBMUtils(const CGBMUtils&) = delete; + CGBMUtils& operator=(const CGBMUtils&) = delete; + CGBMUtils() = default; + ~CGBMUtils() = default; + + /** + * @brief Create a gbm device for allocating buffers + * + * @param fd The file descriptor for a backend device + * @return true The device creation succeeded + * @return false The device creation failed + */ + bool CreateDevice(int fd); + + /** + * @brief A wrapper for gbm_device to allow OOP and RAII + * + */ + class CGBMDevice + { + public: + CGBMDevice(const CGBMDevice&) = delete; + CGBMDevice& operator=(const CGBMDevice&) = delete; + explicit CGBMDevice(gbm_device* device); + ~CGBMDevice() = default; + + /** + * @brief Create a gbm surface + * + * @param width The width to use for the surface + * @param height The height to use for the surface + * @param format The format to use for the surface + * @param modifiers The modifiers to use for the surface + * @param modifiers_count The amount of modifiers in the modifiers param + * @return true The surface creation succeeded + * @return false The surface creation failed + */ + bool CreateSurface(int width, + int height, + uint32_t format, + const uint64_t* modifiers, + const int modifiers_count); + + /** + * @brief Get the underlying gbm_device + * + * @return gbm_device* A pointer to the underlying gbm_device + */ + gbm_device* Get() const { return m_device; } + + /** + * @brief A wrapper for gbm_surface to allow OOP and RAII + * + */ + class CGBMSurface + { + public: + CGBMSurface(const CGBMSurface&) = delete; + CGBMSurface& operator=(const CGBMSurface&) = delete; + explicit CGBMSurface(gbm_surface* surface); + ~CGBMSurface() = default; + + /** + * @brief Get the underlying gbm_surface + * + * @return gbm_surface* A pointer to the underlying gbm_surface + */ + gbm_surface* Get() const { return m_surface; } + + /** + * @brief A wrapper for gbm_bo to allow OOP and RAII + * + */ + class CGBMSurfaceBuffer + { + public: + CGBMSurfaceBuffer(const CGBMSurfaceBuffer&) = delete; + CGBMSurfaceBuffer& operator=(const CGBMSurfaceBuffer&) = delete; + explicit CGBMSurfaceBuffer(gbm_surface* surface); + ~CGBMSurfaceBuffer(); + + /** + * @brief Get the underlying gbm_bo + * + * @return gbm_bo* A pointer to the underlying gbm_bo + */ + gbm_bo* Get() const { return m_buffer; } + + private: + gbm_surface* m_surface{nullptr}; + gbm_bo* m_buffer{nullptr}; + }; + + /** + * @brief Lock the surface's current front buffer. + * + * @return CGBMSurfaceBuffer* A pointer to a CGBMSurfaceBuffer object + */ + CGBMSurfaceBuffer* LockFrontBuffer(); + + private: + gbm_surface* m_surface{nullptr}; + std::queue<std::unique_ptr<CGBMSurfaceBuffer>> m_buffers; + }; + + /** + * @brief Get the CGBMSurface object + * + * @return CGBMSurface* A pointer to the CGBMSurface object + */ + CGBMDevice::CGBMSurface* GetSurface() const { return m_surface.get(); } + + private: + gbm_device* m_device{nullptr}; + + struct CGBMSurfaceDeleter + { + void operator()(CGBMSurface* p) const + { + if (p) + gbm_surface_destroy(p->Get()); + } + }; + std::unique_ptr<CGBMSurface, CGBMSurfaceDeleter> m_surface; + }; + + /** + * @brief Get the CGBMDevice object + * + * @return CGBMDevice* A pointer to the CGBMDevice object + */ + CGBMUtils::CGBMDevice* GetDevice() const { return m_device.get(); } + +private: + struct CGBMDeviceDeleter + { + void operator()(CGBMDevice* p) const + { + if (p) + gbm_device_destroy(p->Get()); + } + }; + std::unique_ptr<CGBMDevice, CGBMDeviceDeleter> m_device; +}; + +} +} +} diff --git a/xbmc/windowing/gbm/OptionalsReg.cpp b/xbmc/windowing/gbm/OptionalsReg.cpp new file mode 100644 index 0000000..9f5076f --- /dev/null +++ b/xbmc/windowing/gbm/OptionalsReg.cpp @@ -0,0 +1,139 @@ +/* + * 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) +#include <va/va_drm.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 GBM +{ + +class CVaapiProxy : public VAAPI::IVaapiWinSystem +{ +public: + CVaapiProxy(int fd) : m_fd(fd) {}; + virtual ~CVaapiProxy() = default; + VADisplay GetVADisplay() override; + void *GetEGLDisplay() override { return eglDisplay; }; + + VADisplay vaDpy; + void *eglDisplay; + +private: + int m_fd{-1}; +}; + +VADisplay CVaapiProxy::GetVADisplay() +{ + return vaGetDisplayDRM(m_fd); +} + +CVaapiProxy* VaapiProxyCreate(int fd) +{ + return new CVaapiProxy(fd); +} + +void VaapiProxyDelete(CVaapiProxy *proxy) +{ + delete proxy; +} + +void VaapiProxyConfig(CVaapiProxy *proxy, void *eglDpy) +{ + proxy->vaDpy = proxy->GetVADisplay(); + 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) +{ + CRendererVAAPIGL::Register(winSystem, winSystem->vaDpy, winSystem->eglDisplay, general, + deepColor); +} +#endif + +#if defined(HAS_GLES) +void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor) +{ + CRendererVAAPIGLES::Register(winSystem, winSystem->vaDpy, winSystem->eglDisplay, general, + deepColor); +} +#endif +} +} +} + +#else + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CVaapiProxy +{ +}; + +CVaapiProxy* VaapiProxyCreate(int fd) +{ + return nullptr; +} + +void VaapiProxyDelete(CVaapiProxy *proxy) +{ +} + +void VaapiProxyConfig(CVaapiProxy *proxy, 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 +} +} +} + +#endif diff --git a/xbmc/windowing/gbm/OptionalsReg.h b/xbmc/windowing/gbm/OptionalsReg.h new file mode 100644 index 0000000..ee459f6 --- /dev/null +++ b/xbmc/windowing/gbm/OptionalsReg.h @@ -0,0 +1,36 @@ +/* + * 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 GBM +{ +class CVaapiProxy; + +CVaapiProxy* VaapiProxyCreate(int fd); +void VaapiProxyDelete(CVaapiProxy *proxy); +void VaapiProxyConfig(CVaapiProxy *proxy, 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 +} +} +} diff --git a/xbmc/windowing/gbm/VideoLayerBridge.h b/xbmc/windowing/gbm/VideoLayerBridge.h new file mode 100644 index 0000000..7070567 --- /dev/null +++ b/xbmc/windowing/gbm/VideoLayerBridge.h @@ -0,0 +1,27 @@ +/* + * 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 + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CVideoLayerBridge +{ +public: + virtual ~CVideoLayerBridge() = default; + virtual void Disable() {} +}; + +} +} +} diff --git a/xbmc/windowing/gbm/VideoSyncGbm.cpp b/xbmc/windowing/gbm/VideoSyncGbm.cpp new file mode 100644 index 0000000..d113c90 --- /dev/null +++ b/xbmc/windowing/gbm/VideoSyncGbm.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2005-2021 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 "VideoSyncGbm.h" + +#include "ServiceBroker.h" +#include "threads/Thread.h" +#include "utils/TimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/WinSystem.h" +#include "windowing/gbm/WinSystemGbm.h" +#include "xf86drm.h" +#include "xf86drmMode.h" + +#include <assert.h> +#include <stdint.h> +#include <stdlib.h> + +#include <unistd.h> + +CVideoSyncGbm::CVideoSyncGbm(void* clock) + : CVideoSync(clock), m_winSystem(CServiceBroker::GetWinSystem()) +{ + if (!m_winSystem) + throw std::runtime_error("window system not available"); +} + +bool CVideoSyncGbm::Setup(PUPDATECLOCK func) +{ + UpdateClock = func; + m_abort = false; + m_winSystem->Register(this); + CLog::Log(LOGDEBUG, "CVideoSyncGbm::{} setting up", __FUNCTION__); + + auto winSystemGbm = dynamic_cast<KODI::WINDOWING::GBM::CWinSystemGbm*>(m_winSystem); + if (!winSystemGbm) + { + CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: failed to get winSystem", __FUNCTION__); + return false; + } + + auto drm = winSystemGbm->GetDrm(); + if (!drm) + { + CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: failed to get drm", __FUNCTION__); + return false; + } + + auto crtc = drm->GetCrtc(); + if (!crtc) + { + CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: failed to get crtc", __FUNCTION__); + return false; + } + + uint64_t ns = 0; + m_crtcId = crtc->GetCrtcId(); + m_fd = drm->GetFileDescriptor(); + int s = drmCrtcGetSequence(m_fd, m_crtcId, &m_sequence, &ns); + m_offset = CurrentHostCounter() - ns; + if (s != 0) + { + CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: drmCrtcGetSequence failed ({})", __FUNCTION__, s); + return false; + } + + CLog::Log(LOGINFO, "CVideoSyncGbm::{}: opened (fd:{} crtc:{} seq:{} ns:{}:{})", __FUNCTION__, + m_fd, m_crtcId, m_sequence, ns, m_offset + ns); + return true; +} + +void CVideoSyncGbm::Run(CEvent& stopEvent) +{ + /* This shouldn't be very busy and timing is important so increase priority */ + CThread::GetCurrentThread()->SetPriority(ThreadPriority::ABOVE_NORMAL); + + if (m_fd < 0) + { + CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: failed to open device ({})", __FUNCTION__, m_fd); + return; + } + CLog::Log(LOGDEBUG, "CVideoSyncGbm::{}: started {}", __FUNCTION__, m_fd); + + while (!stopEvent.Signaled() && !m_abort) + { + uint64_t sequence = 0, ns = 0; + usleep(1000); + int s = drmCrtcGetSequence(m_fd, m_crtcId, &sequence, &ns); + if (s != 0) + { + CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: drmCrtcGetSequence failed ({})", __FUNCTION__, s); + break; + } + + if (sequence == m_sequence) + continue; + + UpdateClock(sequence - m_sequence, m_offset + ns, m_refClock); + m_sequence = sequence; + } +} + +void CVideoSyncGbm::Cleanup() +{ + CLog::Log(LOGDEBUG, "CVideoSyncGbm::{}: cleaning up", __FUNCTION__); + m_winSystem->Unregister(this); +} + +float CVideoSyncGbm::GetFps() +{ + m_fps = m_winSystem->GetGfxContext().GetFPS(); + CLog::Log(LOGDEBUG, "CVideoSyncGbm::{}: fps:{}", __FUNCTION__, m_fps); + return m_fps; +} + +void CVideoSyncGbm::OnResetDisplay() +{ + m_abort = true; +} + +void CVideoSyncGbm::RefreshChanged() +{ + if (m_fps != m_winSystem->GetGfxContext().GetFPS()) + m_abort = true; +} diff --git a/xbmc/windowing/gbm/VideoSyncGbm.h b/xbmc/windowing/gbm/VideoSyncGbm.h new file mode 100644 index 0000000..610d988 --- /dev/null +++ b/xbmc/windowing/gbm/VideoSyncGbm.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2021 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 "guilib/DispResource.h" +#include "windowing/VideoSync.h" + +#include <atomic> + +class CWinSystemBase; + +class CVideoSyncGbm : public CVideoSync, IDispResource +{ +public: + explicit CVideoSyncGbm(void* clock); + CVideoSyncGbm() = delete; + ~CVideoSyncGbm() override = default; + + // CVideoSync overrides + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stopEvent) override; + void Cleanup() override; + float GetFps() override; + void RefreshChanged() override; + + // IDispResource overrides + void OnResetDisplay() override; + +private: + int m_fd = -1; + uint32_t m_crtcId = 0; + uint64_t m_sequence = 0; + uint64_t m_offset = 0; + std::atomic<bool> m_abort{false}; + + CWinSystemBase* m_winSystem; +}; diff --git a/xbmc/windowing/gbm/WinSystemGbm.cpp b/xbmc/windowing/gbm/WinSystemGbm.cpp new file mode 100644 index 0000000..4fd2da4 --- /dev/null +++ b/xbmc/windowing/gbm/WinSystemGbm.cpp @@ -0,0 +1,445 @@ +/* + * 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 "WinSystemGbm.h" + +#include "GBMDPMSSupport.h" +#include "OptionalsReg.h" +#include "ServiceBroker.h" +#include "VideoSyncGbm.h" +#include "cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h" +#include "drm/DRMAtomic.h" +#include "drm/DRMLegacy.h" +#include "drm/OffScreenModeSetting.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "utils/StringUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +#include <mutex> +#include <string.h> + +#ifndef HAVE_HDR_OUTPUT_METADATA +// HDR structs is copied from linux include/linux/hdmi.h +struct hdr_metadata_infoframe +{ + uint8_t eotf; + uint8_t metadata_type; + struct + { + uint16_t x, y; + } display_primaries[3]; + struct + { + uint16_t x, y; + } white_point; + uint16_t max_display_mastering_luminance; + uint16_t min_display_mastering_luminance; + uint16_t max_cll; + uint16_t max_fall; +}; +struct hdr_output_metadata +{ + uint32_t metadata_type; + union + { + struct hdr_metadata_infoframe hdmi_metadata_type1; + }; +}; +#endif + +using namespace KODI::WINDOWING::GBM; + +using namespace std::chrono_literals; + +CWinSystemGbm::CWinSystemGbm() : + m_DRM(nullptr), + m_GBM(new CGBMUtils), + m_libinput(new CLibInputHandler) +{ + m_dpms = std::make_shared<CGBMDPMSSupport>(); + m_libinput->Start(); +} + +bool CWinSystemGbm::InitWindowSystem() +{ + const char* x11 = getenv("DISPLAY"); + const char* wayland = getenv("WAYLAND_DISPLAY"); + if (x11 || wayland) + { + CLog::Log(LOGDEBUG, "CWinSystemGbm::{} - not allowed to run GBM under a window manager", + __FUNCTION__); + return false; + } + + m_DRM = std::make_shared<CDRMAtomic>(); + + if (!m_DRM->InitDrm()) + { + CLog::Log(LOGERROR, "CWinSystemGbm::{} - failed to initialize Atomic DRM", __FUNCTION__); + m_DRM.reset(); + + m_DRM = std::make_shared<CDRMLegacy>(); + + if (!m_DRM->InitDrm()) + { + CLog::Log(LOGERROR, "CWinSystemGbm::{} - failed to initialize Legacy DRM", __FUNCTION__); + m_DRM.reset(); + + m_DRM = std::make_shared<COffScreenModeSetting>(); + if (!m_DRM->InitDrm()) + { + CLog::Log(LOGERROR, "CWinSystemGbm::{} - failed to initialize off screen DRM", + __FUNCTION__); + m_DRM.reset(); + return false; + } + } + } + + if (!m_GBM->CreateDevice(m_DRM->GetFileDescriptor())) + { + m_GBM.reset(); + return false; + } + + auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + return false; + + auto settings = settingsComponent->GetSettings(); + if (!settings) + return false; + + auto setting = settings->GetSetting(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE); + if (setting) + setting->SetVisible(true); + + setting = settings->GetSetting("videoscreen.limitguisize"); + if (setting) + setting->SetVisible(true); + + CLog::Log(LOGDEBUG, "CWinSystemGbm::{} - initialized DRM", __FUNCTION__); + return CWinSystemBase::InitWindowSystem(); +} + +bool CWinSystemGbm::DestroyWindowSystem() +{ + CLog::Log(LOGDEBUG, "CWinSystemGbm::{} - deinitialized DRM", __FUNCTION__); + + m_libinput.reset(); + + return true; +} + +void CWinSystemGbm::UpdateResolutions() +{ + RESOLUTION_INFO current = m_DRM->GetCurrentMode(); + + auto resolutions = m_DRM->GetModes(); + if (resolutions.empty()) + { + CLog::Log(LOGWARNING, "CWinSystemGbm::{} - Failed to get resolutions", __FUNCTION__); + } + else + { + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + for (auto &res : resolutions) + { + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + + if (current.iScreenWidth == res.iScreenWidth && + current.iScreenHeight == res.iScreenHeight && + current.iWidth == res.iWidth && + current.iHeight == res.iHeight && + current.fRefreshRate == res.fRefreshRate && + current.dwFlags == res.dwFlags) + { + CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP) = res; + } + + CLog::Log(LOGINFO, "Found resolution {}x{} with {}x{}{} @ {:f} Hz", res.iWidth, res.iHeight, + res.iScreenWidth, res.iScreenHeight, + res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "", res.fRefreshRate); + } + } + + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +bool CWinSystemGbm::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + return true; +} + +bool CWinSystemGbm::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + // Notify other subsystems that we will change resolution + OnLostDevice(); + + if(!m_DRM->SetMode(res)) + { + CLog::Log(LOGERROR, "CWinSystemGbm::{} - failed to set DRM mode", __FUNCTION__); + return false; + } + + struct gbm_bo *bo = nullptr; + + if (!std::dynamic_pointer_cast<CDRMAtomic>(m_DRM)) + { + bo = m_GBM->GetDevice()->GetSurface()->LockFrontBuffer()->Get(); + } + + auto result = m_DRM->SetVideoMode(res, bo); + + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + if (delay > 0ms) + m_dispResetTimer.Set(delay); + + return result; +} + +bool CWinSystemGbm::DisplayHardwareScalingEnabled() +{ + auto drmAtomic = std::dynamic_pointer_cast<CDRMAtomic>(m_DRM); + if (drmAtomic && drmAtomic->DisplayHardwareScalingEnabled()) + return true; + + return false; +} + +void CWinSystemGbm::UpdateDisplayHardwareScaling(const RESOLUTION_INFO& resInfo) +{ + if (!DisplayHardwareScalingEnabled()) + return; + + //! @todo The PR that made the res struct constant was abandoned due to drama. + // It should be const-corrected and changed here. + RESOLUTION_INFO& resMutable = const_cast<RESOLUTION_INFO&>(resInfo); + + SetFullScreen(true, resMutable, false); +} + +void CWinSystemGbm::FlipPage(bool rendered, bool videoLayer) +{ + if (m_videoLayerBridge && !videoLayer) + { + // disable video plane when video layer no longer is active + m_videoLayerBridge->Disable(); + } + + struct gbm_bo *bo = nullptr; + + if (rendered) + { + bo = m_GBM->GetDevice()->GetSurface()->LockFrontBuffer()->Get(); + } + + m_DRM->FlipPage(bo, rendered, videoLayer); + + if (m_videoLayerBridge && !videoLayer) + { + // delete video layer bridge when video layer no longer is active + m_videoLayerBridge.reset(); + } +} + +bool CWinSystemGbm::UseLimitedColor() +{ + return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE); +} + +bool CWinSystemGbm::Hide() +{ + bool ret = m_DRM->SetActive(false); + FlipPage(false, false); + return ret; +} + +bool CWinSystemGbm::Show(bool raise) +{ + bool ret = m_DRM->SetActive(true); + FlipPage(false, false); + return ret; +} + +void CWinSystemGbm::Register(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemGbm::Unregister(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + { + m_resources.erase(i); + } +} + +void CWinSystemGbm::OnLostDevice() +{ + CLog::Log(LOGDEBUG, "{} - notify display change event", __FUNCTION__); + m_dispReset = true; + + std::unique_lock<CCriticalSection> lock(m_resourceSection); + for (auto resource : m_resources) + resource->OnLostDisplay(); +} + +std::unique_ptr<CVideoSync> CWinSystemGbm::GetVideoSync(void* clock) +{ + return std::make_unique<CVideoSyncGbm>(clock); +} + +std::vector<std::string> CWinSystemGbm::GetConnectedOutputs() +{ + return m_DRM->GetConnectedConnectorNames(); +} + +bool CWinSystemGbm::SetHDR(const VideoPicture* videoPicture) +{ + auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + return false; + + auto settings = settingsComponent->GetSettings(); + if (!settings) + return false; + + if (!settings->GetBool(SETTING_WINSYSTEM_IS_HDR_DISPLAY)) + return false; + + auto drm = std::dynamic_pointer_cast<CDRMAtomic>(m_DRM); + if (!drm) + return false; + + if (!videoPicture) + { + auto connector = drm->GetConnector(); + if (connector->SupportsProperty("HDR_OUTPUT_METADATA")) + { + drm->AddProperty(connector, "HDR_OUTPUT_METADATA", 0); + drm->SetActive(true); + + if (m_hdr_blob_id) + drmModeDestroyPropertyBlob(drm->GetFileDescriptor(), m_hdr_blob_id); + m_hdr_blob_id = 0; + } + + return true; + } + + auto connector = drm->GetConnector(); + if (connector->SupportsProperty("HDR_OUTPUT_METADATA")) + { + hdr_output_metadata hdr_metadata = {}; + + hdr_metadata.metadata_type = DRMPRIME::HDMI_STATIC_METADATA_TYPE1; + hdr_metadata.hdmi_metadata_type1.eotf = DRMPRIME::GetEOTF(*videoPicture); + hdr_metadata.hdmi_metadata_type1.metadata_type = DRMPRIME::HDMI_STATIC_METADATA_TYPE1; + + if (m_hdr_blob_id) + drmModeDestroyPropertyBlob(drm->GetFileDescriptor(), m_hdr_blob_id); + m_hdr_blob_id = 0; + + if (hdr_metadata.hdmi_metadata_type1.eotf) + { + const AVMasteringDisplayMetadata* mdmd = DRMPRIME::GetMasteringDisplayMetadata(*videoPicture); + if (mdmd && mdmd->has_primaries) + { + // Convert to unsigned 16-bit values in units of 0.00002, + // where 0x0000 represents zero and 0xC350 represents 1.0000 + for (int i = 0; i < 3; i++) + { + hdr_metadata.hdmi_metadata_type1.display_primaries[i].x = + std::round(av_q2d(mdmd->display_primaries[i][0]) * 50000.0); + hdr_metadata.hdmi_metadata_type1.display_primaries[i].y = + std::round(av_q2d(mdmd->display_primaries[i][1]) * 50000.0); + + CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - display_primaries[{}].x: {}", + __FUNCTION__, i, hdr_metadata.hdmi_metadata_type1.display_primaries[i].x); + CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - display_primaries[{}].y: {}", + __FUNCTION__, i, hdr_metadata.hdmi_metadata_type1.display_primaries[i].y); + } + hdr_metadata.hdmi_metadata_type1.white_point.x = + std::round(av_q2d(mdmd->white_point[0]) * 50000.0); + hdr_metadata.hdmi_metadata_type1.white_point.y = + std::round(av_q2d(mdmd->white_point[1]) * 50000.0); + + CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - white_point.x: {}", __FUNCTION__, + hdr_metadata.hdmi_metadata_type1.white_point.x); + CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - white_point.y: {}", __FUNCTION__, + hdr_metadata.hdmi_metadata_type1.white_point.y); + } + if (mdmd && mdmd->has_luminance) + { + // Convert to unsigned 16-bit value in units of 1 cd/m2, + // where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2 + hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance = + std::round(av_q2d(mdmd->max_luminance)); + + // Convert to unsigned 16-bit value in units of 0.0001 cd/m2, + // where 0x0001 represents 0.0001 cd/m2 and 0xFFFF represents 6.5535 cd/m2 + hdr_metadata.hdmi_metadata_type1.min_display_mastering_luminance = + std::round(av_q2d(mdmd->min_luminance) * 10000.0); + + CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - max_display_mastering_luminance: {}", + __FUNCTION__, hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance); + CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - min_display_mastering_luminance: {}", + __FUNCTION__, hdr_metadata.hdmi_metadata_type1.min_display_mastering_luminance); + } + + const AVContentLightMetadata* clmd = DRMPRIME::GetContentLightMetadata(*videoPicture); + if (clmd) + { + hdr_metadata.hdmi_metadata_type1.max_cll = clmd->MaxCLL; + hdr_metadata.hdmi_metadata_type1.max_fall = clmd->MaxFALL; + + CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - max_cll: {}", __FUNCTION__, + hdr_metadata.hdmi_metadata_type1.max_cll); + CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - max_fall: {}", __FUNCTION__, + hdr_metadata.hdmi_metadata_type1.max_fall); + } + + drmModeCreatePropertyBlob(drm->GetFileDescriptor(), &hdr_metadata, sizeof(hdr_metadata), + &m_hdr_blob_id); + } + + drm->AddProperty(connector, "HDR_OUTPUT_METADATA", m_hdr_blob_id); + drm->SetActive(true); + } + + return true; +} + +bool CWinSystemGbm::IsHDRDisplay() +{ + auto drm = std::dynamic_pointer_cast<CDRMAtomic>(m_DRM); + if (!drm) + return false; + + auto connector = drm->GetConnector(); + if (!connector) + return false; + + //! @todo: improve detection (edid?) + // we have no way to know if the display is actually HDR capable and we blindly set the HDR metadata + return connector->SupportsProperty("HDR_OUTPUT_METADATA"); +} diff --git a/xbmc/windowing/gbm/WinSystemGbm.h b/xbmc/windowing/gbm/WinSystemGbm.h new file mode 100644 index 0000000..b313d33 --- /dev/null +++ b/xbmc/windowing/gbm/WinSystemGbm.h @@ -0,0 +1,96 @@ +/* + * 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 + +#include "VideoLayerBridge.h" +#include "drm/DRMUtils.h" +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "windowing/WinSystem.h" + +#include "platform/linux/input/LibInputHandler.h" + +#include <utility> + +#include <gbm.h> + +class IDispResource; + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CWinSystemGbm : public CWinSystemBase +{ +public: + CWinSystemGbm(); + ~CWinSystemGbm() override = default; + + const std::string GetName() override { return "gbm"; } + + bool InitWindowSystem() override; + bool DestroyWindowSystem() override; + + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + bool DisplayHardwareScalingEnabled() override; + void UpdateDisplayHardwareScaling(const RESOLUTION_INFO& resInfo) override; + + void FlipPage(bool rendered, bool videoLayer); + + bool CanDoWindowed() override { return false; } + void UpdateResolutions() override; + + bool UseLimitedColor() override; + + bool Hide() override; + bool Show(bool raise = true) override; + void Register(IDispResource* resource) override; + void Unregister(IDispResource* resource) override; + + bool SetHDR(const VideoPicture* videoPicture) override; + bool IsHDRDisplay() override; + + std::shared_ptr<CVideoLayerBridge> GetVideoLayerBridge() const { return m_videoLayerBridge; } + void RegisterVideoLayerBridge(std::shared_ptr<CVideoLayerBridge> bridge) + { + m_videoLayerBridge = std::move(bridge); + }; + + CGBMUtils::CGBMDevice* GetGBMDevice() const { return m_GBM->GetDevice(); } + std::shared_ptr<CDRMUtils> GetDrm() const { return m_DRM; } + + std::vector<std::string> GetConnectedOutputs() override; + +protected: + void OnLostDevice(); + + std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override; + + std::shared_ptr<CDRMUtils> m_DRM; + std::unique_ptr<CGBMUtils> m_GBM; + std::shared_ptr<CVideoLayerBridge> m_videoLayerBridge; + + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + + bool m_dispReset = false; + XbmcThreads::EndTime<> m_dispResetTimer; + std::unique_ptr<CLibInputHandler> m_libinput; + +private: + uint32_t m_hdr_blob_id = 0; +}; + +} +} +} diff --git a/xbmc/windowing/gbm/WinSystemGbmEGLContext.cpp b/xbmc/windowing/gbm/WinSystemGbmEGLContext.cpp new file mode 100644 index 0000000..83a5941 --- /dev/null +++ b/xbmc/windowing/gbm/WinSystemGbmEGLContext.cpp @@ -0,0 +1,141 @@ +/* + * 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 "WinSystemGbmEGLContext.h" + +#include "OptionalsReg.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "utils/log.h" + +using namespace KODI::WINDOWING::GBM; +using namespace KODI::WINDOWING::LINUX; + +bool CWinSystemGbmEGLContext::InitWindowSystemEGL(EGLint renderableType, EGLint apiType) +{ + if (!CWinSystemGbm::InitWindowSystem()) + { + return false; + } + + if (!m_eglContext.CreatePlatformDisplay(m_GBM->GetDevice()->Get(), m_GBM->GetDevice()->Get())) + { + return false; + } + + if (!m_eglContext.InitializeDisplay(apiType)) + { + return false; + } + + auto plane = m_DRM->GetGuiPlane(); + uint32_t visualId = plane != nullptr ? plane->GetFormat() : DRM_FORMAT_XRGB2101010; + + // prefer alpha visual id, fallback to non-alpha visual id + if (!m_eglContext.ChooseConfig(renderableType, CDRMUtils::FourCCWithAlpha(visualId)) && + !m_eglContext.ChooseConfig(renderableType, CDRMUtils::FourCCWithoutAlpha(visualId))) + { + // fallback to 8bit format if no EGL config was found for 10bit + if (plane) + plane->SetFormat(DRM_FORMAT_XRGB8888); + + visualId = plane != nullptr ? plane->GetFormat() : DRM_FORMAT_XRGB8888; + + if (!m_eglContext.ChooseConfig(renderableType, CDRMUtils::FourCCWithAlpha(visualId)) && + !m_eglContext.ChooseConfig(renderableType, CDRMUtils::FourCCWithoutAlpha(visualId))) + { + return false; + } + } + + if (!CreateContext()) + { + return false; + } + + return true; +} + +bool CWinSystemGbmEGLContext::CreateNewWindow(const std::string& name, + bool fullScreen, + RESOLUTION_INFO& res) +{ + //Notify other subsystems that we change resolution + OnLostDevice(); + + if (!DestroyWindow()) + { + return false; + } + + if (!m_DRM->SetMode(res)) + { + CLog::Log(LOGERROR, "CWinSystemGbmEGLContext::{} - failed to set DRM mode", __FUNCTION__); + return false; + } + + uint32_t format = m_eglContext.GetConfigAttrib(EGL_NATIVE_VISUAL_ID); + + std::vector<uint64_t> modifiers; + + auto plane = m_DRM->GetGuiPlane(); + if (plane) + modifiers = plane->GetModifiersForFormat(format); + + if (!m_GBM->GetDevice()->CreateSurface(res.iWidth, res.iHeight, format, modifiers.data(), + modifiers.size())) + { + CLog::Log(LOGERROR, "CWinSystemGbmEGLContext::{} - failed to initialize GBM", __FUNCTION__); + return false; + } + + // This check + the reinterpret cast is for security reason, if the user has outdated platform header files which often is the case + static_assert(sizeof(EGLNativeWindowType) == sizeof(gbm_surface*), "Declaration specifier differs in size"); + + if (!m_eglContext.CreatePlatformSurface( + m_GBM->GetDevice()->GetSurface()->Get(), + reinterpret_cast<khronos_uintptr_t>(m_GBM->GetDevice()->GetSurface()->Get()))) + { + return false; + } + + if (!m_eglContext.BindContext()) + { + return false; + } + + m_bFullScreen = fullScreen; + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_fRefreshRate = res.fRefreshRate; + + CLog::Log(LOGDEBUG, "CWinSystemGbmEGLContext::{} - initialized GBM", __FUNCTION__); + return true; +} + +bool CWinSystemGbmEGLContext::DestroyWindow() +{ + m_eglContext.DestroySurface(); + + CLog::Log(LOGDEBUG, "CWinSystemGbmEGLContext::{} - deinitialized GBM", __FUNCTION__); + return true; +} + +bool CWinSystemGbmEGLContext::DestroyWindowSystem() +{ + CDVDFactoryCodec::ClearHWAccels(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + m_eglContext.Destroy(); + + return CWinSystemGbm::DestroyWindowSystem(); +} + +void CWinSystemGbmEGLContext::delete_CVaapiProxy::operator()(CVaapiProxy *p) const +{ + VaapiProxyDelete(p); +} diff --git a/xbmc/windowing/gbm/WinSystemGbmEGLContext.h b/xbmc/windowing/gbm/WinSystemGbmEGLContext.h new file mode 100644 index 0000000..84f863d --- /dev/null +++ b/xbmc/windowing/gbm/WinSystemGbmEGLContext.h @@ -0,0 +1,58 @@ +/* + * 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 + +#include "WinSystemGbm.h" +#include "utils/EGLUtils.h" +#include "windowing/linux/WinSystemEGL.h" + +#include <memory> + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CVaapiProxy; + +class CWinSystemGbmEGLContext : public KODI::WINDOWING::LINUX::CWinSystemEGL, public CWinSystemGbm +{ +public: + ~CWinSystemGbmEGLContext() override = default; + + bool DestroyWindowSystem() override; + bool CreateNewWindow(const std::string& name, + bool fullScreen, + RESOLUTION_INFO& res) override; + bool DestroyWindow() override; + +protected: + CWinSystemGbmEGLContext(EGLenum platform, std::string const& platformExtension) + : CWinSystemEGL{platform, platformExtension} + {} + + /** + * Inheriting classes should override InitWindowSystem() without parameters + * and call this function there with appropriate parameters + */ + bool InitWindowSystemEGL(EGLint renderableType, EGLint apiType); + virtual bool CreateContext() = 0; + + struct delete_CVaapiProxy + { + void operator()(CVaapiProxy *p) const; + }; + std::unique_ptr<CVaapiProxy, delete_CVaapiProxy> m_vaapiProxy; +}; + +} +} +} diff --git a/xbmc/windowing/gbm/WinSystemGbmGLContext.cpp b/xbmc/windowing/gbm/WinSystemGbmGLContext.cpp new file mode 100644 index 0000000..e4ff49c --- /dev/null +++ b/xbmc/windowing/gbm/WinSystemGbmGLContext.cpp @@ -0,0 +1,174 @@ +/* + * 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 "WinSystemGbmGLContext.h" + +#include "OptionalsReg.h" +#include "cores/RetroPlayer/process/gbm/RPProcessInfoGbm.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "rendering/gl/ScreenshotSurfaceGL.h" +#include "utils/BufferObjectFactory.h" +#include "utils/DMAHeapBufferObject.h" +#include "utils/DumbBufferObject.h" +#include "utils/GBMBufferObject.h" +#include "utils/UDMABufferObject.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "windowing/WindowSystemFactory.h" + +#include <mutex> + +#include <EGL/eglext.h> + +using namespace KODI::WINDOWING::GBM; + +using namespace std::chrono_literals; + +CWinSystemGbmGLContext::CWinSystemGbmGLContext() +: CWinSystemGbmEGLContext(EGL_PLATFORM_GBM_MESA, "EGL_MESA_platform_gbm") +{} + +void CWinSystemGbmGLContext::Register() +{ + CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "gbm"); +} + +std::unique_ptr<CWinSystemBase> CWinSystemGbmGLContext::CreateWinSystem() +{ + return std::make_unique<CWinSystemGbmGLContext>(); +} + +bool CWinSystemGbmGLContext::InitWindowSystem() +{ + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CDVDFactoryCodec::ClearHWAccels(); + CLinuxRendererGL::Register(); + RETRO::CRPProcessInfoGbm::Register(); + RETRO::CRPProcessInfoGbm::RegisterRendererFactory(new RETRO::CRendererFactoryDMA); + RETRO::CRPProcessInfoGbm::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL); + + if (!CWinSystemGbmEGLContext::InitWindowSystemEGL(EGL_OPENGL_BIT, EGL_OPENGL_API)) + { + return false; + } + + bool general, deepColor; + m_vaapiProxy.reset(VaapiProxyCreate(m_DRM->GetRenderNodeFileDescriptor())); + VaapiProxyConfig(m_vaapiProxy.get(), m_eglContext.GetEGLDisplay()); + VAAPIRegisterRenderGL(m_vaapiProxy.get(), general, deepColor); + + if (general) + { + VAAPIRegister(m_vaapiProxy.get(), deepColor); + } + + CScreenshotSurfaceGL::Register(); + + CBufferObjectFactory::ClearBufferObjects(); + CDumbBufferObject::Register(); +#if defined(HAS_GBM_BO_MAP) + CGBMBufferObject::Register(); +#endif +#if defined(HAVE_LINUX_MEMFD) && defined(HAVE_LINUX_UDMABUF) + CUDMABufferObject::Register(); +#endif +#if defined(HAVE_LINUX_DMA_HEAP) + CDMAHeapBufferObject::Register(); +#endif + + return true; +} + +bool CWinSystemGbmGLContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + if (res.iWidth != m_nWidth || + res.iHeight != m_nHeight) + { + CLog::Log(LOGDEBUG, "CWinSystemGbmGLContext::{} - resolution changed, creating a new window", + __FUNCTION__); + CreateNewWindow("", fullScreen, res); + } + + if (!m_eglContext.TrySwapBuffers()) + { + CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed"); + throw std::runtime_error("eglSwapBuffers failed"); + } + + CWinSystemGbm::SetFullScreen(fullScreen, res, blankOtherDisplays); + CRenderSystemGL::ResetRenderSystem(res.iWidth, res.iHeight); + + return true; +} + +void CWinSystemGbmGLContext::PresentRender(bool rendered, bool videoLayer) +{ + if (!m_bRenderCreated) + return; + + if (rendered || videoLayer) + { + if (rendered) + { + if (!m_eglContext.TrySwapBuffers()) + { + CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed"); + throw std::runtime_error("eglSwapBuffers failed"); + } + } + CWinSystemGbm::FlipPage(rendered, videoLayer); + + if (m_dispReset && m_dispResetTimer.IsTimePast()) + { + CLog::Log(LOGDEBUG, "CWinSystemGbmGLContext::{} - Sending display reset to all clients", + __FUNCTION__); + m_dispReset = false; + std::unique_lock<CCriticalSection> lock(m_resourceSection); + + for (auto resource : m_resources) + resource->OnResetDisplay(); + } + } + else + { + KODI::TIME::Sleep(10ms); + } +} + +bool CWinSystemGbmGLContext::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; +} diff --git a/xbmc/windowing/gbm/WinSystemGbmGLContext.h b/xbmc/windowing/gbm/WinSystemGbmGLContext.h new file mode 100644 index 0000000..8994ce6 --- /dev/null +++ b/xbmc/windowing/gbm/WinSystemGbmGLContext.h @@ -0,0 +1,48 @@ +/* + * 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 + +#include "WinSystemGbmEGLContext.h" +#include "rendering/gl/RenderSystemGL.h" +#include "utils/EGLUtils.h" + +#include <memory> + +class CVaapiProxy; + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CWinSystemGbmGLContext : public CWinSystemGbmEGLContext, public CRenderSystemGL +{ +public: + CWinSystemGbmGLContext(); + ~CWinSystemGbmGLContext() override = default; + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystemBase via CWinSystemGbm + CRenderSystemBase *GetRenderSystem() override { return this; } + bool InitWindowSystem() override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + void PresentRender(bool rendered, bool videoLayer) override; +protected: + void SetVSyncImpl(bool enable) override {} + void PresentRenderImpl(bool rendered) override {}; + bool CreateContext() override; +}; + +} +} +} diff --git a/xbmc/windowing/gbm/WinSystemGbmGLESContext.cpp b/xbmc/windowing/gbm/WinSystemGbmGLESContext.cpp new file mode 100644 index 0000000..0d071c3 --- /dev/null +++ b/xbmc/windowing/gbm/WinSystemGbmGLESContext.cpp @@ -0,0 +1,167 @@ +/* + * 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 "WinSystemGbmGLESContext.h" + +#include "OptionalsReg.h" +#include "cores/RetroPlayer/process/gbm/RPProcessInfoGbm.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.h" +#include "cores/VideoPlayer/Process/gbm/ProcessInfoGBM.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.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/DumbBufferObject.h" +#include "utils/GBMBufferObject.h" +#include "utils/UDMABufferObject.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "windowing/WindowSystemFactory.h" + +#include <mutex> + +#include <gbm.h> + +using namespace KODI::WINDOWING::GBM; + +using namespace std::chrono_literals; + +CWinSystemGbmGLESContext::CWinSystemGbmGLESContext() +: CWinSystemGbmEGLContext(EGL_PLATFORM_GBM_MESA, "EGL_MESA_platform_gbm") +{} + +void CWinSystemGbmGLESContext::Register() +{ + CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "gbm"); +} + +std::unique_ptr<CWinSystemBase> CWinSystemGbmGLESContext::CreateWinSystem() +{ + return std::make_unique<CWinSystemGbmGLESContext>(); +} + +bool CWinSystemGbmGLESContext::InitWindowSystem() +{ + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CDVDFactoryCodec::ClearHWAccels(); + CLinuxRendererGLES::Register(); + RETRO::CRPProcessInfoGbm::Register(); + RETRO::CRPProcessInfoGbm::RegisterRendererFactory(new RETRO::CRendererFactoryDMA); + RETRO::CRPProcessInfoGbm::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES); + + if (!CWinSystemGbmEGLContext::InitWindowSystemEGL(EGL_OPENGL_ES2_BIT, EGL_OPENGL_ES_API)) + { + return false; + } + + bool general, deepColor; + m_vaapiProxy.reset(GBM::VaapiProxyCreate(m_DRM->GetRenderNodeFileDescriptor())); + GBM::VaapiProxyConfig(m_vaapiProxy.get(), m_eglContext.GetEGLDisplay()); + GBM::VAAPIRegisterRenderGLES(m_vaapiProxy.get(), general, deepColor); + + if (general) + { + GBM::VAAPIRegister(m_vaapiProxy.get(), deepColor); + } + + CRendererDRMPRIMEGLES::Register(); + CRendererDRMPRIME::Register(); + CDVDVideoCodecDRMPRIME::Register(); + VIDEOPLAYER::CProcessInfoGBM::Register(); + + CScreenshotSurfaceGLES::Register(); + + CBufferObjectFactory::ClearBufferObjects(); + CDumbBufferObject::Register(); +#if defined(HAS_GBM_BO_MAP) + CGBMBufferObject::Register(); +#endif +#if defined(HAVE_LINUX_MEMFD) && defined(HAVE_LINUX_UDMABUF) + CUDMABufferObject::Register(); +#endif +#if defined(HAVE_LINUX_DMA_HEAP) + CDMAHeapBufferObject::Register(); +#endif + + return true; +} + +bool CWinSystemGbmGLESContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + if (res.iWidth != m_nWidth || + res.iHeight != m_nHeight) + { + CLog::Log(LOGDEBUG, "CWinSystemGbmGLESContext::{} - resolution changed, creating a new window", + __FUNCTION__); + CreateNewWindow("", fullScreen, res); + } + + if (!m_eglContext.TrySwapBuffers()) + { + CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed"); + throw std::runtime_error("eglSwapBuffers failed"); + } + + CWinSystemGbm::SetFullScreen(fullScreen, res, blankOtherDisplays); + CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight); + + return true; +} + +void CWinSystemGbmGLESContext::PresentRender(bool rendered, bool videoLayer) +{ + if (!m_bRenderCreated) + return; + + if (rendered || videoLayer) + { + if (rendered) + { + if (!m_eglContext.TrySwapBuffers()) + { + CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed"); + throw std::runtime_error("eglSwapBuffers failed"); + } + } + CWinSystemGbm::FlipPage(rendered, videoLayer); + + if (m_dispReset && m_dispResetTimer.IsTimePast()) + { + CLog::Log(LOGDEBUG, "CWinSystemGbmGLESContext::{} - Sending display reset to all clients", + __FUNCTION__); + m_dispReset = false; + std::unique_lock<CCriticalSection> lock(m_resourceSection); + + for (auto resource : m_resources) + resource->OnResetDisplay(); + } + } + else + { + KODI::TIME::Sleep(10ms); + } +} + +bool CWinSystemGbmGLESContext::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; +} diff --git a/xbmc/windowing/gbm/WinSystemGbmGLESContext.h b/xbmc/windowing/gbm/WinSystemGbmGLESContext.h new file mode 100644 index 0000000..8b9de77 --- /dev/null +++ b/xbmc/windowing/gbm/WinSystemGbmGLESContext.h @@ -0,0 +1,48 @@ +/* + * 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 + +#include "WinSystemGbmEGLContext.h" +#include "rendering/gles/RenderSystemGLES.h" +#include "utils/EGLUtils.h" + +#include <memory> + +class CVaapiProxy; + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CWinSystemGbmGLESContext : public CWinSystemGbmEGLContext, public CRenderSystemGLES +{ +public: + CWinSystemGbmGLESContext(); + ~CWinSystemGbmGLESContext() override = default; + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystemBase via CWinSystemGbm + CRenderSystemBase *GetRenderSystem() override { return this; } + bool InitWindowSystem() override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + void PresentRender(bool rendered, bool videoLayer) override; +protected: + void SetVSyncImpl(bool enable) override {} + void PresentRenderImpl(bool rendered) override {}; + bool CreateContext() override; +}; + +} +} +} diff --git a/xbmc/windowing/gbm/drm/CMakeLists.txt b/xbmc/windowing/gbm/drm/CMakeLists.txt new file mode 100644 index 0000000..8bd07ea --- /dev/null +++ b/xbmc/windowing/gbm/drm/CMakeLists.txt @@ -0,0 +1,21 @@ +set(SOURCES DRMAtomic.cpp + DRMConnector.cpp + DRMCrtc.cpp + DRMEncoder.cpp + DRMLegacy.cpp + DRMObject.cpp + DRMPlane.cpp + DRMUtils.cpp + OffScreenModeSetting.cpp) + +set(HEADERS DRMAtomic.h + DRMConnector.h + DRMCrtc.h + DRMEncoder.h + DRMLegacy.h + DRMObject.h + DRMPlane.h + DRMUtils.h + OffScreenModeSetting.h) + +core_add_library(windowing_gbm_drm) diff --git a/xbmc/windowing/gbm/drm/DRMAtomic.cpp b/xbmc/windowing/gbm/drm/DRMAtomic.cpp new file mode 100644 index 0000000..5d61a69 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMAtomic.cpp @@ -0,0 +1,344 @@ +/* + * 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 "DRMAtomic.h" + +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" +#include "utils/log.h" + +#include <errno.h> +#include <string.h> + +#include <drm_fourcc.h> +#include <drm_mode.h> +#include <unistd.h> + +using namespace KODI::WINDOWING::GBM; + +namespace +{ + +const auto SETTING_VIDEOSCREEN_HW_SCALING_FILTER = "videoscreen.hwscalingfilter"; + +uint32_t GetScalingFactor(uint32_t srcWidth, + uint32_t srcHeight, + uint32_t destWidth, + uint32_t destHeight) +{ + uint32_t factor_W = destWidth / srcWidth; + uint32_t factor_H = destHeight / srcHeight; + if (factor_W != factor_H) + return (factor_W < factor_H) ? factor_W : factor_H; + return factor_W; +} + +} // namespace + +bool CDRMAtomic::SetScalingFilter(CDRMObject* object, const char* name, const char* type) +{ + bool result; + uint64_t value; + std::tie(result, value) = m_gui_plane->GetPropertyValue(name, type); + if (!result) + return false; + + if (!AddProperty(object, name, value)) + return false; + + uint32_t mar_scale_factor = + GetScalingFactor(m_width, m_height, m_mode->hdisplay, m_mode->vdisplay); + AddProperty(object, "CRTC_W", (mar_scale_factor * m_width)); + AddProperty(object, "CRTC_H", (mar_scale_factor * m_height)); + + return true; +} + +void CDRMAtomic::DrmAtomicCommit(int fb_id, int flags, bool rendered, bool videoLayer) +{ + uint32_t blob_id; + + if (flags & DRM_MODE_ATOMIC_ALLOW_MODESET) + { + if (!AddProperty(m_connector, "CRTC_ID", m_crtc->GetCrtcId())) + return; + + if (drmModeCreatePropertyBlob(m_fd, m_mode, sizeof(*m_mode), &blob_id) != 0) + return; + + if (m_active && m_orig_crtc && m_orig_crtc->GetCrtcId() != m_crtc->GetCrtcId()) + { + // if using a different CRTC than the original, disable original to avoid EINVAL + if (!AddProperty(m_orig_crtc, "MODE_ID", 0)) + return; + + if (!AddProperty(m_orig_crtc, "ACTIVE", 0)) + return; + } + + if (!AddProperty(m_crtc, "MODE_ID", blob_id)) + return; + + if (!AddProperty(m_crtc, "ACTIVE", m_active ? 1 : 0)) + return; + } + + if (rendered) + { + AddProperty(m_gui_plane, "FB_ID", fb_id); + AddProperty(m_gui_plane, "CRTC_ID", m_crtc->GetCrtcId()); + AddProperty(m_gui_plane, "SRC_X", 0); + AddProperty(m_gui_plane, "SRC_Y", 0); + AddProperty(m_gui_plane, "SRC_W", m_width << 16); + AddProperty(m_gui_plane, "SRC_H", m_height << 16); + AddProperty(m_gui_plane, "CRTC_X", 0); + AddProperty(m_gui_plane, "CRTC_Y", 0); + //! @todo: disabled until upstream kernel changes are merged + // if (DisplayHardwareScalingEnabled()) + // { + // SetScalingFilter(m_gui_plane, "SCALING_FILTER", "Nearest Neighbor"); + // } + // else + { + AddProperty(m_gui_plane, "CRTC_W", m_mode->hdisplay); + AddProperty(m_gui_plane, "CRTC_H", m_mode->vdisplay); + } + + } + else if (videoLayer && !CServiceBroker::GetGUI()->GetWindowManager().HasVisibleControls()) + { + // disable gui plane when video layer is active and gui has no visible controls + AddProperty(m_gui_plane, "FB_ID", 0); + AddProperty(m_gui_plane, "CRTC_ID", 0); + } + + if (CServiceBroker::GetLogging().CanLogComponent(LOGWINDOWING)) + m_req->LogAtomicRequest(); + + auto ret = drmModeAtomicCommit(m_fd, m_req->Get(), flags | DRM_MODE_ATOMIC_TEST_ONLY, nullptr); + if (ret < 0) + { + CLog::Log(LOGERROR, + "CDRMAtomic::{} - test commit failed: ({}) - falling back to last successful atomic " + "request", + __FUNCTION__, strerror(errno)); + + auto oldRequest = m_atomicRequestQueue.front().get(); + CDRMAtomicRequest::LogAtomicDiff(m_req, oldRequest); + m_req = oldRequest; + + // update the old atomic request with the new fb id to avoid tearing + if (rendered) + AddProperty(m_gui_plane, "FB_ID", fb_id); + } + + ret = drmModeAtomicCommit(m_fd, m_req->Get(), flags, nullptr); + if (ret < 0) + { + CLog::Log(LOGERROR, "CDRMAtomic::{} - atomic commit failed: {}", __FUNCTION__, + strerror(errno)); + } + + if (flags & DRM_MODE_ATOMIC_ALLOW_MODESET) + { + if (drmModeDestroyPropertyBlob(m_fd, blob_id) != 0) + CLog::Log(LOGERROR, "CDRMAtomic::{} - failed to destroy property blob: {}", __FUNCTION__, + strerror(errno)); + } + + if (m_atomicRequestQueue.size() > 1) + m_atomicRequestQueue.pop_back(); + + m_atomicRequestQueue.emplace_back(std::make_unique<CDRMAtomicRequest>()); + m_req = m_atomicRequestQueue.back().get(); +} + +void CDRMAtomic::FlipPage(struct gbm_bo *bo, bool rendered, bool videoLayer) +{ + struct drm_fb *drm_fb = nullptr; + + if (rendered) + { + if (videoLayer) + m_gui_plane->SetFormat(CDRMUtils::FourCCWithAlpha(m_gui_plane->GetFormat())); + else + m_gui_plane->SetFormat(CDRMUtils::FourCCWithoutAlpha(m_gui_plane->GetFormat())); + + drm_fb = CDRMUtils::DrmFbGetFromBo(bo); + if (!drm_fb) + { + CLog::Log(LOGERROR, "CDRMAtomic::{} - Failed to get a new FBO", __FUNCTION__); + return; + } + } + + uint32_t flags = 0; + + if (m_need_modeset) + { + flags |= DRM_MODE_ATOMIC_ALLOW_MODESET; + m_need_modeset = false; + CLog::Log(LOGDEBUG, "CDRMAtomic::{} - Execute modeset at next commit", __FUNCTION__); + } + + DrmAtomicCommit(!drm_fb ? 0 : drm_fb->fb_id, flags, rendered, videoLayer); +} + +bool CDRMAtomic::InitDrm() +{ + if (!CDRMUtils::OpenDrm(true)) + return false; + + auto ret = drmSetClientCap(m_fd, DRM_CLIENT_CAP_ATOMIC, 1); + if (ret) + { + CLog::Log(LOGERROR, "CDRMAtomic::{} - no atomic modesetting support: {}", __FUNCTION__, + strerror(errno)); + return false; + } + + m_atomicRequestQueue.emplace_back(std::make_unique<CDRMAtomicRequest>()); + m_req = m_atomicRequestQueue.back().get(); + + if (!CDRMUtils::InitDrm()) + return false; + + for (auto& plane : m_planes) + { + AddProperty(plane.get(), "FB_ID", 0); + AddProperty(plane.get(), "CRTC_ID", 0); + } + + CLog::Log(LOGDEBUG, "CDRMAtomic::{} - initialized atomic DRM", __FUNCTION__); + + //! @todo: disabled until upstream kernel changes are merged + // if (m_gui_plane->SupportsProperty("SCALING_FILTER")) + // { + // const std::shared_ptr<CSettings> settings = + // CServiceBroker::GetSettingsComponent()->GetSettings(); + // settings->GetSetting(SETTING_VIDEOSCREEN_HW_SCALING_FILTER)->SetVisible(true); + // } + + return true; +} + +void CDRMAtomic::DestroyDrm() +{ + CDRMUtils::DestroyDrm(); +} + +bool CDRMAtomic::SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo *bo) +{ + m_need_modeset = true; + + return true; +} + +bool CDRMAtomic::SetActive(bool active) +{ + m_need_modeset = true; + m_active = active; + + return true; +} + +bool CDRMAtomic::AddProperty(CDRMObject* object, const char* name, uint64_t value) +{ + return m_req->AddProperty(object, name, value); +} + +bool CDRMAtomic::DisplayHardwareScalingEnabled() +{ + auto settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + + if (settings && settings->GetBool(SETTING_VIDEOSCREEN_HW_SCALING_FILTER)) + return true; + + return false; +} + +CDRMAtomic::CDRMAtomicRequest::CDRMAtomicRequest() : m_atomicRequest(drmModeAtomicAlloc()) +{ +} + +bool CDRMAtomic::CDRMAtomicRequest::AddProperty(CDRMObject* object, const char* name, uint64_t value) +{ + uint32_t propertyId = object->GetPropertyId(name); + if (propertyId == 0) + return false; + + int ret = drmModeAtomicAddProperty(m_atomicRequest.get(), object->GetId(), propertyId, value); + if (ret < 0) + return false; + + m_atomicRequestItems[object][propertyId] = value; + return true; +} + +void CDRMAtomic::CDRMAtomicRequest::LogAtomicDiff(CDRMAtomicRequest* current, + CDRMAtomicRequest* old) +{ + std::map<CDRMObject*, std::map<uint32_t, uint64_t>> atomicDiff; + + for (const auto& object : current->m_atomicRequestItems) + { + auto sameObject = old->m_atomicRequestItems.find(object.first); + if (sameObject != old->m_atomicRequestItems.end()) + { + std::map<uint32_t, uint64_t> propertyDiff; + + std::set_difference(current->m_atomicRequestItems[object.first].begin(), + current->m_atomicRequestItems[object.first].end(), + old->m_atomicRequestItems[object.first].begin(), + old->m_atomicRequestItems[object.first].end(), + std::inserter(propertyDiff, propertyDiff.begin())); + + atomicDiff[object.first] = propertyDiff; + } + else + { + atomicDiff[object.first] = current->m_atomicRequestItems[object.first]; + } + } + + CLog::Log(LOGDEBUG, "CDRMAtomicRequest::{} - DRM Atomic Request Diff:", __FUNCTION__); + + LogAtomicRequest(LOGERROR, atomicDiff); +} + +void CDRMAtomic::CDRMAtomicRequest::LogAtomicRequest() +{ + CLog::Log(LOGDEBUG, "CDRMAtomicRequest::{} - DRM Atomic Request:", __FUNCTION__); + LogAtomicRequest(LOGDEBUG, m_atomicRequestItems); +} + +void CDRMAtomic::CDRMAtomicRequest::LogAtomicRequest( + uint8_t logLevel, std::map<CDRMObject*, std::map<uint32_t, uint64_t>>& atomicRequestItems) +{ + std::string message; + for (const auto& object : atomicRequestItems) + { + message.append("\nObject: " + object.first->GetTypeName() + + "\tID: " + std::to_string(object.first->GetId())); + for (const auto& property : object.second) + message.append("\n Property: " + object.first->GetPropertyName(property.first) + + "\tID: " + std::to_string(property.first) + + "\tValue: " + std::to_string(property.second)); + } + + CLog::Log(logLevel, "{}", message); +} + +void CDRMAtomic::CDRMAtomicRequest::DrmModeAtomicReqDeleter::operator()(drmModeAtomicReqPtr p) const +{ + drmModeAtomicFree(p); +}; diff --git a/xbmc/windowing/gbm/drm/DRMAtomic.h b/xbmc/windowing/gbm/drm/DRMAtomic.h new file mode 100644 index 0000000..ca2cd9a --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMAtomic.h @@ -0,0 +1,81 @@ +/* + * 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 + +#include "DRMUtils.h" + +#include <cstdint> +#include <deque> +#include <map> +#include <memory> + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CDRMAtomic : public CDRMUtils +{ +public: + CDRMAtomic() = default; + ~CDRMAtomic() override = default; + void FlipPage(struct gbm_bo* bo, bool rendered, bool videoLayer) override; + bool SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo* bo) override; + bool SetActive(bool active) override; + bool InitDrm() override; + void DestroyDrm() override; + bool AddProperty(CDRMObject* object, const char* name, uint64_t value); + + bool DisplayHardwareScalingEnabled(); + +private: + void DrmAtomicCommit(int fb_id, int flags, bool rendered, bool videoLayer); + + bool SetScalingFilter(CDRMObject* object, const char* name, const char* type); + + bool m_need_modeset; + bool m_active = true; + + class CDRMAtomicRequest + { + public: + CDRMAtomicRequest(); + ~CDRMAtomicRequest() = default; + CDRMAtomicRequest(const CDRMAtomicRequest& right) = delete; + + drmModeAtomicReqPtr Get() const { return m_atomicRequest.get(); } + + bool AddProperty(CDRMObject* object, const char* name, uint64_t value); + void LogAtomicRequest(); + + static void LogAtomicDiff(CDRMAtomicRequest* current, CDRMAtomicRequest* old); + + private: + static void LogAtomicRequest( + uint8_t logLevel, std::map<CDRMObject*, std::map<uint32_t, uint64_t>>& atomicRequestItems); + + std::map<CDRMObject*, std::map<uint32_t, uint64_t>> m_atomicRequestItems; + + struct DrmModeAtomicReqDeleter + { + void operator()(drmModeAtomicReqPtr p) const; + }; + + std::unique_ptr<drmModeAtomicReq, DrmModeAtomicReqDeleter> m_atomicRequest; + }; + + CDRMAtomicRequest* m_req = nullptr; + std::deque<std::unique_ptr<CDRMAtomicRequest>> m_atomicRequestQueue; +}; + +} +} +} diff --git a/xbmc/windowing/gbm/drm/DRMConnector.cpp b/xbmc/windowing/gbm/drm/DRMConnector.cpp new file mode 100644 index 0000000..fc76b90 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMConnector.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2005-2020 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 "DRMConnector.h" + +#include "utils/XTimeUtils.h" +#include "utils/log.h" + +#include <map> + +using namespace KODI::WINDOWING::GBM; + +using namespace std::chrono_literals; + +namespace +{ + +std::map<int, std::string> connectorTypeNames = { + {DRM_MODE_CONNECTOR_Unknown, "unknown"}, + {DRM_MODE_CONNECTOR_VGA, "VGA"}, + {DRM_MODE_CONNECTOR_DVII, "DVI-I"}, + {DRM_MODE_CONNECTOR_DVID, "DVI-D"}, + {DRM_MODE_CONNECTOR_DVIA, "DVI-A"}, + {DRM_MODE_CONNECTOR_Composite, "composite"}, + {DRM_MODE_CONNECTOR_SVIDEO, "s-video"}, + {DRM_MODE_CONNECTOR_LVDS, "LVDS"}, + {DRM_MODE_CONNECTOR_Component, "component"}, + {DRM_MODE_CONNECTOR_9PinDIN, "9-pin DIN"}, + {DRM_MODE_CONNECTOR_DisplayPort, "DP"}, + {DRM_MODE_CONNECTOR_HDMIA, "HDMI-A"}, + {DRM_MODE_CONNECTOR_HDMIB, "HDMI-B"}, + {DRM_MODE_CONNECTOR_TV, "TV"}, + {DRM_MODE_CONNECTOR_eDP, "eDP"}, + {DRM_MODE_CONNECTOR_VIRTUAL, "Virtual"}, + {DRM_MODE_CONNECTOR_DSI, "DSI"}, + {DRM_MODE_CONNECTOR_DPI, "DPI"}, +}; + +std::map<drmModeConnection, std::string> connectorStatusNames = { + {DRM_MODE_CONNECTED, "connected"}, + {DRM_MODE_DISCONNECTED, "disconnected"}, + {DRM_MODE_UNKNOWNCONNECTION, "unknown"}, +}; + +} // namespace + +CDRMConnector::CDRMConnector(int fd, uint32_t connector) + : CDRMObject(fd), m_connector(drmModeGetConnector(m_fd, connector)) +{ + if (!m_connector) + throw std::runtime_error("drmModeGetConnector failed: " + std::string{strerror(errno)}); + + if (!GetProperties(m_connector->connector_id, DRM_MODE_OBJECT_CONNECTOR)) + throw std::runtime_error("failed to get properties for connector: " + + std::to_string(m_connector->connector_id)); +} + +bool CDRMConnector::CheckConnector() +{ + unsigned retryCnt = 7; + while (!IsConnected() && retryCnt > 0) + { + CLog::Log(LOGDEBUG, "CDRMConnector::{} - connector is disconnected", __FUNCTION__); + retryCnt--; + KODI::TIME::Sleep(1s); + + m_connector.reset(drmModeGetConnector(m_fd, m_connector->connector_id)); + } + + return m_connector->connection == DRM_MODE_CONNECTED; +} + +std::string CDRMConnector::GetType() +{ + auto typeName = connectorTypeNames.find(m_connector->connector_type); + if (typeName == connectorTypeNames.end()) + return connectorTypeNames[DRM_MODE_CONNECTOR_Unknown]; + + return typeName->second; +} + +std::string CDRMConnector::GetStatus() +{ + auto statusName = connectorStatusNames.find(m_connector->connection); + if (statusName == connectorStatusNames.end()) + return connectorStatusNames[DRM_MODE_UNKNOWNCONNECTION]; + + return statusName->second; +} + +std::string CDRMConnector::GetName() +{ + return GetType() + "-" + std::to_string(m_connector->connector_type_id); +} diff --git a/xbmc/windowing/gbm/drm/DRMConnector.h b/xbmc/windowing/gbm/drm/DRMConnector.h new file mode 100644 index 0000000..5a29222 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMConnector.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2020 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 "DRMObject.h" + +#include <string> + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CDRMConnector : public CDRMObject +{ +public: + explicit CDRMConnector(int fd, uint32_t connector); + CDRMConnector(const CDRMConnector&) = delete; + CDRMConnector& operator=(const CDRMConnector&) = delete; + ~CDRMConnector() = default; + + std::string GetType(); + std::string GetStatus(); + std::string GetName(); + + uint32_t GetEncoderId() const { return m_connector->encoder_id; } + uint32_t* GetConnectorId() const { return &m_connector->connector_id; } + int GetModesCount() const { return m_connector->count_modes; } + drmModeModeInfoPtr GetModeForIndex(int index) const { return &m_connector->modes[index]; } + + bool IsConnected() { return m_connector->connection == DRM_MODE_CONNECTED; } + bool CheckConnector(); + +private: + struct DrmModeConnectorDeleter + { + void operator()(drmModeConnector* p) { drmModeFreeConnector(p); } + }; + + std::unique_ptr<drmModeConnector, DrmModeConnectorDeleter> m_connector; +}; + +} // namespace GBM +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/gbm/drm/DRMCrtc.cpp b/xbmc/windowing/gbm/drm/DRMCrtc.cpp new file mode 100644 index 0000000..426fd95 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMCrtc.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2005-2020 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 "DRMCrtc.h" + +#include <cstring> +#include <errno.h> +#include <stdexcept> +#include <string> + +using namespace KODI::WINDOWING::GBM; + +CDRMCrtc::CDRMCrtc(int fd, uint32_t crtc) : CDRMObject(fd), m_crtc(drmModeGetCrtc(m_fd, crtc)) +{ + if (!m_crtc) + throw std::runtime_error("drmModeGetCrtc failed: " + std::string{strerror(errno)}); + + if (!GetProperties(m_crtc->crtc_id, DRM_MODE_OBJECT_CRTC)) + throw std::runtime_error("failed to get properties for crtc: " + + std::to_string(m_crtc->crtc_id)); +} diff --git a/xbmc/windowing/gbm/drm/DRMCrtc.h b/xbmc/windowing/gbm/drm/DRMCrtc.h new file mode 100644 index 0000000..a1aadc2 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMCrtc.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005-2020 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 "DRMObject.h" + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CDRMCrtc : public CDRMObject +{ +public: + explicit CDRMCrtc(int fd, uint32_t crtc); + CDRMCrtc(const CDRMCrtc&) = delete; + CDRMCrtc& operator=(const CDRMCrtc&) = delete; + ~CDRMCrtc() = default; + + uint32_t GetCrtcId() const { return m_crtc->crtc_id; } + uint32_t GetBufferId() const { return m_crtc->buffer_id; } + uint32_t GetX() const { return m_crtc->x; } + uint32_t GetY() const { return m_crtc->y; } + drmModeModeInfoPtr GetMode() const { return &m_crtc->mode; } + bool GetModeValid() const { return m_crtc->mode_valid != 0; } + +private: + struct DrmModeCrtcDeleter + { + void operator()(drmModeCrtc* p) { drmModeFreeCrtc(p); } + }; + + std::unique_ptr<drmModeCrtc, DrmModeCrtcDeleter> m_crtc; +}; + +} // namespace GBM +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/gbm/drm/DRMEncoder.cpp b/xbmc/windowing/gbm/drm/DRMEncoder.cpp new file mode 100644 index 0000000..e4289c6 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMEncoder.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2005-2020 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 "DRMEncoder.h" + +#include <cstring> +#include <errno.h> +#include <stdexcept> +#include <string> + +using namespace KODI::WINDOWING::GBM; + +CDRMEncoder::CDRMEncoder(int fd, uint32_t encoder) + : CDRMObject(fd), m_encoder(drmModeGetEncoder(m_fd, encoder)) +{ + if (!m_encoder) + throw std::runtime_error("drmModeGetEncoder failed: " + std::string{strerror(errno)}); +} diff --git a/xbmc/windowing/gbm/drm/DRMEncoder.h b/xbmc/windowing/gbm/drm/DRMEncoder.h new file mode 100644 index 0000000..aceb276 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMEncoder.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2020 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 "DRMObject.h" + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CDRMEncoder : public CDRMObject +{ +public: + explicit CDRMEncoder(int fd, uint32_t encoder); + CDRMEncoder(const CDRMEncoder&) = delete; + CDRMEncoder& operator=(const CDRMEncoder&) = delete; + ~CDRMEncoder() = default; + + uint32_t GetEncoderId() const { return m_encoder->encoder_id; } + uint32_t GetCrtcId() const { return m_encoder->crtc_id; } + uint32_t GetPossibleCrtcs() const { return m_encoder->possible_crtcs; } + +private: + struct DrmModeEncoderDeleter + { + void operator()(drmModeEncoder* p) { drmModeFreeEncoder(p); } + }; + + std::unique_ptr<drmModeEncoder, DrmModeEncoderDeleter> m_encoder; +}; + +} // namespace GBM +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/gbm/drm/DRMLegacy.cpp b/xbmc/windowing/gbm/drm/DRMLegacy.cpp new file mode 100644 index 0000000..418d067 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMLegacy.cpp @@ -0,0 +1,141 @@ +/* + * 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 "DRMLegacy.h" + +#include "guilib/gui3d.h" +#include "settings/Settings.h" +#include "utils/log.h" + +#include <errno.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <drm_mode.h> +#include <fcntl.h> +#include <poll.h> +#include <unistd.h> + +using namespace KODI::WINDOWING::GBM; + +static int flip_happening = 0; + +bool CDRMLegacy::SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo *bo) +{ + struct drm_fb *drm_fb = DrmFbGetFromBo(bo); + + auto ret = drmModeSetCrtc(m_fd, m_crtc->GetCrtcId(), drm_fb->fb_id, 0, 0, + m_connector->GetConnectorId(), 1, m_mode); + + if(ret < 0) + { + CLog::Log(LOGERROR, "CDRMLegacy::{} - failed to set crtc mode: {}x{}{} @ {} Hz", __FUNCTION__, + m_mode->hdisplay, m_mode->vdisplay, + m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "", m_mode->vrefresh); + + return false; + } + + CLog::Log(LOGDEBUG, "CDRMLegacy::{} - set crtc mode: {}x{}{} @ {} Hz", __FUNCTION__, + m_mode->hdisplay, m_mode->vdisplay, m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "", + m_mode->vrefresh); + + return true; +} + +void CDRMLegacy::PageFlipHandler(int fd, unsigned int frame, unsigned int sec, + unsigned int usec, void *data) +{ + (void) fd, (void) frame, (void) sec, (void) usec; + + int *flip_happening = static_cast<int *>(data); + *flip_happening = 0; +} + +bool CDRMLegacy::WaitingForFlip() +{ + if (!flip_happening) + return false; + + struct pollfd drm_fds = + { + m_fd, + POLLIN, + 0, + }; + + drmEventContext drm_evctx{}; + drm_evctx.version = DRM_EVENT_CONTEXT_VERSION; + drm_evctx.page_flip_handler = PageFlipHandler; + + while(flip_happening) + { + auto ret = poll(&drm_fds, 1, -1); + + if(ret < 0) + return true; + + if(drm_fds.revents & (POLLHUP | POLLERR)) + return true; + + if(drm_fds.revents & POLLIN) + drmHandleEvent(m_fd, &drm_evctx); + } + + return false; +} + +bool CDRMLegacy::QueueFlip(struct gbm_bo *bo) +{ + struct drm_fb *drm_fb = DrmFbGetFromBo(bo); + + auto ret = drmModePageFlip(m_fd, m_crtc->GetCrtcId(), drm_fb->fb_id, DRM_MODE_PAGE_FLIP_EVENT, + &flip_happening); + + if(ret) + { + CLog::Log(LOGDEBUG, "CDRMLegacy::{} - failed to queue DRM page flip", __FUNCTION__); + return false; + } + + return true; +} + +void CDRMLegacy::FlipPage(struct gbm_bo *bo, bool rendered, bool videoLayer) +{ + if (rendered || videoLayer) + { + flip_happening = QueueFlip(bo); + WaitingForFlip(); + } +} + +bool CDRMLegacy::InitDrm() +{ + if (!CDRMUtils::OpenDrm(true)) + return false; + + if (!CDRMUtils::InitDrm()) + return false; + + CLog::Log(LOGDEBUG, "CDRMLegacy::{} - initialized legacy DRM", __FUNCTION__); + return true; +} + +bool CDRMLegacy::SetActive(bool active) +{ + if (!m_connector->SetProperty("DPMS", active ? DRM_MODE_DPMS_ON : DRM_MODE_DPMS_OFF)) + { + CLog::Log(LOGDEBUG, "CDRMLegacy::{} - failed to set DPMS property", __FUNCTION__); + return false; + } + + return true; +} diff --git a/xbmc/windowing/gbm/drm/DRMLegacy.h b/xbmc/windowing/gbm/drm/DRMLegacy.h new file mode 100644 index 0000000..2b7ff45 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMLegacy.h @@ -0,0 +1,39 @@ +/* + * 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 + +#include "DRMUtils.h" + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CDRMLegacy : public CDRMUtils +{ +public: + CDRMLegacy() = default; + ~CDRMLegacy() override = default; + void FlipPage(struct gbm_bo* bo, bool rendered, bool videoLayer) override; + bool SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo* bo) override; + bool SetActive(bool active) override; + bool InitDrm() override; + +private: + bool WaitingForFlip(); + bool QueueFlip(struct gbm_bo *bo); + static void PageFlipHandler(int fd, unsigned int frame, unsigned int sec, + unsigned int usec, void *data); +}; + +} +} +} diff --git a/xbmc/windowing/gbm/drm/DRMObject.cpp b/xbmc/windowing/gbm/drm/DRMObject.cpp new file mode 100644 index 0000000..599bb61 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMObject.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2005-2020 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 "DRMObject.h" + +#include "utils/log.h" + +#include <algorithm> +#include <array> + +using namespace KODI::WINDOWING::GBM; + +namespace +{ + +constexpr std::array<std::pair<uint32_t, const char*>, 8> DrmModeObjectTypes = { + {{DRM_MODE_OBJECT_CRTC, "crtc"}, + {DRM_MODE_OBJECT_CONNECTOR, "connector"}, + {DRM_MODE_OBJECT_ENCODER, "encoder"}, + {DRM_MODE_OBJECT_MODE, "mode"}, + {DRM_MODE_OBJECT_PROPERTY, "property"}, + {DRM_MODE_OBJECT_FB, "framebuffer"}, + {DRM_MODE_OBJECT_BLOB, "blob"}, + {DRM_MODE_OBJECT_PLANE, "plane"}}}; +} + +CDRMObject::CDRMObject(int fd) : m_fd(fd) +{ +} + +std::string CDRMObject::GetTypeName() const +{ + auto name = std::find_if(DrmModeObjectTypes.begin(), DrmModeObjectTypes.end(), + [this](const auto& p) { return p.first == m_type; }); + if (name != DrmModeObjectTypes.end()) + return name->second; + + return "invalid type"; +} + +std::string CDRMObject::GetPropertyName(uint32_t propertyId) const +{ + auto prop = std::find_if(m_propsInfo.begin(), m_propsInfo.end(), + [&propertyId](const auto& p) { return p->prop_id == propertyId; }); + if (prop != m_propsInfo.end()) + return prop->get()->name; + + return "invalid property"; +} + +uint32_t CDRMObject::GetPropertyId(const std::string& name) const +{ + auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(), + [&name](const auto& prop) { return prop->name == name; }); + + if (property != m_propsInfo.end()) + return property->get()->prop_id; + + return 0; +} + +bool CDRMObject::GetProperties(uint32_t id, uint32_t type) +{ + m_props.reset(drmModeObjectGetProperties(m_fd, id, type)); + if (!m_props) + return false; + + m_id = id; + m_type = type; + + for (uint32_t i = 0; i < m_props->count_props; i++) + m_propsInfo.emplace_back(std::unique_ptr<drmModePropertyRes, DrmModePropertyResDeleter>( + drmModeGetProperty(m_fd, m_props->props[i]))); + + return true; +} + +//! @todo: improve with c++17 +std::tuple<bool, uint64_t> CDRMObject::GetPropertyValue(const std::string& name, + const std::string& valueName) const +{ + auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(), + [&name](const auto& prop) { return prop->name == name; }); + + if (property == m_propsInfo.end()) + return std::make_tuple(false, 0); + + auto prop = property->get(); + + if (!static_cast<bool>(drm_property_type_is(prop, DRM_MODE_PROP_ENUM))) + return std::make_tuple(false, 0); + + for (int j = 0; j < prop->count_enums; j++) + { + if (prop->enums[j].name != valueName) + continue; + + return std::make_tuple(true, prop->enums[j].value); + } + + return std::make_tuple(false, 0); +} + +bool CDRMObject::SetProperty(const std::string& name, uint64_t value) +{ + auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(), + [&name](const auto& prop) { return prop->name == name; }); + + if (property != m_propsInfo.end()) + { + int ret = drmModeObjectSetProperty(m_fd, m_id, m_type, property->get()->prop_id, value); + if (ret == 0) + return true; + } + + return false; +} + +bool CDRMObject::SupportsProperty(const std::string& name) +{ + auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(), + [&name](const auto& prop) { return prop->name == name; }); + + if (property != m_propsInfo.end()) + return true; + + return false; +} diff --git a/xbmc/windowing/gbm/drm/DRMObject.h b/xbmc/windowing/gbm/drm/DRMObject.h new file mode 100644 index 0000000..e2ae326 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMObject.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2005-2020 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 <cstddef> +#include <cstdint> +#include <memory> +#include <vector> + +#include <xf86drmMode.h> + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CDRMObject +{ +public: + CDRMObject(const CDRMObject&) = delete; + CDRMObject& operator=(const CDRMObject&) = delete; + virtual ~CDRMObject() = default; + + std::string GetTypeName() const; + std::string GetPropertyName(uint32_t propertyId) const; + + uint32_t GetId() const { return m_id; } + uint32_t GetPropertyId(const std::string& name) const; + std::tuple<bool, uint64_t> GetPropertyValue(const std::string& name, + const std::string& valueName) const; + + bool SetProperty(const std::string& name, uint64_t value); + bool SupportsProperty(const std::string& name); + +protected: + explicit CDRMObject(int fd); + + bool GetProperties(uint32_t id, uint32_t type); + + struct DrmModeObjectPropertiesDeleter + { + void operator()(drmModeObjectProperties* p) { drmModeFreeObjectProperties(p); } + }; + + std::unique_ptr<drmModeObjectProperties, DrmModeObjectPropertiesDeleter> m_props; + + struct DrmModePropertyResDeleter + { + void operator()(drmModePropertyRes* p) { drmModeFreeProperty(p); } + }; + + std::vector<std::unique_ptr<drmModePropertyRes, DrmModePropertyResDeleter>> m_propsInfo; + + int m_fd{-1}; + +private: + uint32_t m_id{0}; + uint32_t m_type{0}; +}; + +} // namespace GBM +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/gbm/drm/DRMPlane.cpp b/xbmc/windowing/gbm/drm/DRMPlane.cpp new file mode 100644 index 0000000..90b5660 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMPlane.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2005-2020 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 "DRMPlane.h" + +#include "DRMUtils.h" +#include "utils/DRMHelpers.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +using namespace KODI::WINDOWING::GBM; + +CDRMPlane::CDRMPlane(int fd, uint32_t plane) : CDRMObject(fd), m_plane(drmModeGetPlane(m_fd, plane)) +{ + if (!m_plane) + throw std::runtime_error("drmModeGetPlane failed: " + std::string{strerror(errno)}); + + if (!GetProperties(m_plane->plane_id, DRM_MODE_OBJECT_PLANE)) + throw std::runtime_error("failed to get properties for plane: " + + std::to_string(m_plane->plane_id)); +} + +bool CDRMPlane::SupportsFormat(uint32_t format) +{ + for (uint32_t i = 0; i < m_plane->count_formats; i++) + if (m_plane->formats[i] == format) + return true; + + return false; +} + +bool CDRMPlane::SupportsFormatAndModifier(uint32_t format, uint64_t modifier) +{ + /* + * Some broadcom modifiers have parameters encoded which need to be + * masked out before comparing with reported modifiers. + */ + if (modifier >> 56 == DRM_FORMAT_MOD_VENDOR_BROADCOM) + modifier = fourcc_mod_broadcom_mod(modifier); + + if (modifier == DRM_FORMAT_MOD_LINEAR) + { + if (!SupportsFormat(format)) + { + CLog::Log(LOGDEBUG, "CDRMPlane::{} - format not supported: {}", __FUNCTION__, + DRMHELPERS::FourCCToString(format)); + return false; + } + } + else + { + auto formatModifiers = &m_modifiers_map[format]; + if (formatModifiers->empty()) + { + CLog::Log(LOGDEBUG, "CDRMPlane::{} - format not supported: {}", __FUNCTION__, + DRMHELPERS::FourCCToString(format)); + return false; + } + + auto formatModifier = std::find(formatModifiers->begin(), formatModifiers->end(), modifier); + if (formatModifier == formatModifiers->end()) + { + CLog::Log(LOGDEBUG, "CDRMPlane::{} - modifier ({}) not supported for format ({})", + __FUNCTION__, DRMHELPERS::ModifierToString(modifier), + DRMHELPERS::FourCCToString(format)); + return false; + } + } + + CLog::Log(LOGDEBUG, "CDRMPlane::{} - found plane format ({}) and modifier ({})", __FUNCTION__, + DRMHELPERS::FourCCToString(format), DRMHELPERS::ModifierToString(modifier)); + + return true; +} + +void CDRMPlane::FindModifiers() +{ + auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(), [](auto& prop) { + return StringUtils::EqualsNoCase(prop->name, "IN_FORMATS"); + }); + + uint64_t blob_id = 0; + if (property != m_propsInfo.end()) + blob_id = m_props->prop_values[std::distance(m_propsInfo.begin(), property)]; + + if (blob_id == 0) + return; + + drmModePropertyBlobPtr blob = drmModeGetPropertyBlob(m_fd, blob_id); + if (!blob) + return; + + drm_format_modifier_blob* header = static_cast<drm_format_modifier_blob*>(blob->data); + uint32_t* formats = + reinterpret_cast<uint32_t*>(reinterpret_cast<char*>(header) + header->formats_offset); + drm_format_modifier* mod = reinterpret_cast<drm_format_modifier*>( + reinterpret_cast<char*>(header) + header->modifiers_offset); + + for (uint32_t i = 0; i < header->count_formats; i++) + { + std::vector<uint64_t> modifiers; + for (uint32_t j = 0; j < header->count_modifiers; j++) + { + if (mod[j].formats & 1ULL << i) + modifiers.emplace_back(mod[j].modifier); + } + + m_modifiers_map.emplace(formats[i], modifiers); + } + + if (blob) + drmModeFreePropertyBlob(blob); +} diff --git a/xbmc/windowing/gbm/drm/DRMPlane.h b/xbmc/windowing/gbm/drm/DRMPlane.h new file mode 100644 index 0000000..e077fc3 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMPlane.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2005-2020 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 "DRMObject.h" + +#include <map> + +#include <drm_fourcc.h> + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class CDRMPlane : public CDRMObject +{ +public: + explicit CDRMPlane(int fd, uint32_t plane); + CDRMPlane(const CDRMPlane&) = delete; + CDRMPlane& operator=(const CDRMPlane&) = delete; + ~CDRMPlane() = default; + + uint32_t GetPlaneId() const { return m_plane->plane_id; } + uint32_t GetPossibleCrtcs() const { return m_plane->possible_crtcs; } + + void FindModifiers(); + + void SetFormat(const uint32_t newFormat) { m_format = newFormat; } + uint32_t GetFormat() const { return m_format; } + std::vector<uint64_t>& GetModifiersForFormat(uint32_t format) { return m_modifiers_map[format]; } + + bool SupportsFormat(uint32_t format); + bool SupportsFormatAndModifier(uint32_t format, uint64_t modifier); + +private: + struct DrmModePlaneDeleter + { + void operator()(drmModePlane* p) { drmModeFreePlane(p); } + }; + + std::unique_ptr<drmModePlane, DrmModePlaneDeleter> m_plane; + + std::map<uint32_t, std::vector<uint64_t>> m_modifiers_map; + uint32_t m_format{DRM_FORMAT_XRGB8888}; +}; + +} // namespace GBM +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/gbm/drm/DRMUtils.cpp b/xbmc/windowing/gbm/drm/DRMUtils.cpp new file mode 100644 index 0000000..6b61403 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMUtils.cpp @@ -0,0 +1,740 @@ +/* + * 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 "DRMUtils.h" + +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/DRMHelpers.h" +#include "utils/StringUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +#include "PlatformDefs.h" + +using namespace KODI::WINDOWING::GBM; + +namespace +{ +const std::string SETTING_VIDEOSCREEN_LIMITGUISIZE = "videoscreen.limitguisize"; + +void DrmFbDestroyCallback(gbm_bo* bo, void* data) +{ + drm_fb* fb = static_cast<drm_fb*>(data); + + if (fb->fb_id > 0) + { + CLog::Log(LOGDEBUG, "CDRMUtils::{} - removing framebuffer: {}", __FUNCTION__, fb->fb_id); + int drm_fd = gbm_device_get_fd(gbm_bo_get_device(bo)); + drmModeRmFB(drm_fd, fb->fb_id); + } + + delete fb; +} +} + +CDRMUtils::~CDRMUtils() +{ + DestroyDrm(); +} + +bool CDRMUtils::SetMode(const RESOLUTION_INFO& res) +{ + if (!m_connector->CheckConnector()) + return false; + + m_mode = m_connector->GetModeForIndex(std::atoi(res.strId.c_str())); + m_width = res.iWidth; + m_height = res.iHeight; + + CLog::Log(LOGDEBUG, "CDRMUtils::{} - found crtc mode: {}x{}{} @ {} Hz", __FUNCTION__, + m_mode->hdisplay, m_mode->vdisplay, m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "", + m_mode->vrefresh); + + return true; +} + +drm_fb * CDRMUtils::DrmFbGetFromBo(struct gbm_bo *bo) +{ + { + struct drm_fb *fb = static_cast<drm_fb *>(gbm_bo_get_user_data(bo)); + if(fb) + { + if (m_gui_plane->GetFormat() == fb->format) + return fb; + else + DrmFbDestroyCallback(bo, gbm_bo_get_user_data(bo)); + } + } + + struct drm_fb *fb = new drm_fb; + fb->bo = bo; + fb->format = m_gui_plane->GetFormat(); + + uint32_t width, + height, + handles[4] = {0}, + strides[4] = {0}, + offsets[4] = {0}; + + uint64_t modifiers[4] = {0}; + + width = gbm_bo_get_width(bo); + height = gbm_bo_get_height(bo); + +#if defined(HAS_GBM_MODIFIERS) + for (int i = 0; i < gbm_bo_get_plane_count(bo); i++) + { + handles[i] = gbm_bo_get_handle_for_plane(bo, i).u32; + strides[i] = gbm_bo_get_stride_for_plane(bo, i); + offsets[i] = gbm_bo_get_offset(bo, i); + modifiers[i] = gbm_bo_get_modifier(bo); + } +#else + handles[0] = gbm_bo_get_handle(bo).u32; + strides[0] = gbm_bo_get_stride(bo); + memset(offsets, 0, 16); +#endif + + uint32_t flags = 0; + + if (modifiers[0] && modifiers[0] != DRM_FORMAT_MOD_INVALID) + { + flags |= DRM_MODE_FB_MODIFIERS; + CLog::Log(LOGDEBUG, "CDRMUtils::{} - using modifier: {}", __FUNCTION__, + DRMHELPERS::ModifierToString(modifiers[0])); + } + + int ret = drmModeAddFB2WithModifiers(m_fd, + width, + height, + fb->format, + handles, + strides, + offsets, + modifiers, + &fb->fb_id, + flags); + + if(ret < 0) + { + ret = drmModeAddFB2(m_fd, + width, + height, + fb->format, + handles, + strides, + offsets, + &fb->fb_id, + flags); + + if (ret < 0) + { + delete (fb); + CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to add framebuffer: {} ({})", __FUNCTION__, strerror(errno), errno); + return nullptr; + } + } + + gbm_bo_set_user_data(bo, fb, DrmFbDestroyCallback); + + return fb; +} + +bool CDRMUtils::FindPreferredMode() +{ + if (m_mode) + return true; + + for (int i = 0, area = 0; i < m_connector->GetModesCount(); i++) + { + drmModeModeInfo* current_mode = m_connector->GetModeForIndex(i); + + if(current_mode->type & DRM_MODE_TYPE_PREFERRED) + { + m_mode = current_mode; + CLog::Log(LOGDEBUG, "CDRMUtils::{} - found preferred mode: {}x{}{} @ {} Hz", __FUNCTION__, + m_mode->hdisplay, m_mode->vdisplay, + m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "", m_mode->vrefresh); + break; + } + + auto current_area = current_mode->hdisplay * current_mode->vdisplay; + if (current_area > area) + { + m_mode = current_mode; + area = current_area; + } + } + + if(!m_mode) + { + CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to find preferred mode", __FUNCTION__); + return false; + } + + return true; +} + +bool CDRMUtils::FindPlanes() +{ + for (size_t i = 0; i < m_crtcs.size(); i++) + { + if (!(m_encoder->GetPossibleCrtcs() & (1 << i))) + continue; + + auto videoPlane = std::find_if(m_planes.begin(), m_planes.end(), [&i](auto& plane) { + if (plane->GetPossibleCrtcs() & (1 << i)) + { + return plane->SupportsFormat(DRM_FORMAT_NV12); + } + return false; + }); + + uint32_t videoPlaneId{0}; + + if (videoPlane != m_planes.end()) + videoPlaneId = videoPlane->get()->GetPlaneId(); + + auto guiPlane = + std::find_if(m_planes.begin(), m_planes.end(), [&i, &videoPlaneId](auto& plane) { + if (plane->GetPossibleCrtcs() & (1 << i)) + { + return (plane->GetPlaneId() != videoPlaneId && + (videoPlaneId == 0 || plane->SupportsFormat(DRM_FORMAT_ARGB8888)) && + (plane->SupportsFormat(DRM_FORMAT_XRGB2101010) || + plane->SupportsFormat(DRM_FORMAT_XRGB8888))); + } + return false; + }); + + if (videoPlane != m_planes.end() && guiPlane != m_planes.end()) + { + m_crtc = m_crtcs[i].get(); + m_video_plane = videoPlane->get(); + m_gui_plane = guiPlane->get(); + break; + } + + if (guiPlane != m_planes.end()) + { + if (!m_crtc && m_encoder->GetCrtcId() == m_crtcs[i]->GetCrtcId()) + { + m_crtc = m_crtcs[i].get(); + m_gui_plane = guiPlane->get(); + m_video_plane = nullptr; + } + } + } + + CLog::Log(LOGINFO, "CDRMUtils::{} - using crtc: {}", __FUNCTION__, m_crtc->GetCrtcId()); + + // video plane may not be available + if (m_video_plane) + CLog::Log(LOGDEBUG, "CDRMUtils::{} - using video plane {}", __FUNCTION__, + m_video_plane->GetPlaneId()); + + if (m_gui_plane->SupportsFormat(DRM_FORMAT_XRGB2101010)) + { + m_gui_plane->SetFormat(DRM_FORMAT_XRGB2101010); + CLog::Log(LOGDEBUG, "CDRMUtils::{} - using 10bit gui plane {}", __FUNCTION__, + m_gui_plane->GetPlaneId()); + } + else + { + m_gui_plane->SetFormat(DRM_FORMAT_XRGB8888); + CLog::Log(LOGDEBUG, "CDRMUtils::{} - using gui plane {}", __FUNCTION__, + m_gui_plane->GetPlaneId()); + } + + return true; +} + +void CDRMUtils::PrintDrmDeviceInfo(drmDevicePtr device) +{ + std::string message; + + // clang-format off + message.append(fmt::format("CDRMUtils::{} - DRM Device Info:", __FUNCTION__)); + message.append(fmt::format("\n available_nodes: {:#04x}", device->available_nodes)); + message.append("\n nodes:"); + + for (int i = 0; i < DRM_NODE_MAX; i++) + { + if (device->available_nodes & 1 << i) + message.append(fmt::format("\n nodes[{}]: {}", i, device->nodes[i])); + } + + message.append(fmt::format("\n bustype: {:#04x}", device->bustype)); + + if (device->bustype == DRM_BUS_PCI) + { + message.append("\n pci:"); + message.append(fmt::format("\n domain: {:#04x}", device->businfo.pci->domain)); + message.append(fmt::format("\n bus: {:#02x}", device->businfo.pci->bus)); + message.append(fmt::format("\n dev: {:#02x}", device->businfo.pci->dev)); + message.append(fmt::format("\n func: {:#1}", device->businfo.pci->func)); + + message.append("\n deviceinfo:"); + message.append("\n pci:"); + message.append(fmt::format("\n vendor_id: {:#04x}", device->deviceinfo.pci->vendor_id)); + message.append(fmt::format("\n device_id: {:#04x}", device->deviceinfo.pci->device_id)); + message.append(fmt::format("\n subvendor_id: {:#04x}", device->deviceinfo.pci->subvendor_id)); + message.append(fmt::format("\n subdevice_id: {:#04x}", device->deviceinfo.pci->subdevice_id)); + } + else if (device->bustype == DRM_BUS_USB) + { + message.append("\n usb:"); + message.append(fmt::format("\n bus: {:#03x}", device->businfo.usb->bus)); + message.append(fmt::format("\n dev: {:#03x}", device->businfo.usb->dev)); + + message.append("\n deviceinfo:"); + message.append("\n usb:"); + message.append(fmt::format("\n vendor: {:#04x}", device->deviceinfo.usb->vendor)); + message.append(fmt::format("\n product: {:#04x}", device->deviceinfo.usb->product)); + } + else if (device->bustype == DRM_BUS_PLATFORM) + { + message.append("\n platform:"); + message.append(fmt::format("\n fullname: {}", device->businfo.platform->fullname)); + } + else if (device->bustype == DRM_BUS_HOST1X) + { + message.append("\n host1x:"); + message.append(fmt::format("\n fullname: {}", device->businfo.host1x->fullname)); + } + else + message.append("\n unhandled bus type"); + // clang-format on + + CLog::Log(LOGDEBUG, "{}", message); +} + +bool CDRMUtils::OpenDrm(bool needConnector) +{ + int numDevices = drmGetDevices2(0, nullptr, 0); + if (numDevices <= 0) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - no drm devices found: ({})", __FUNCTION__, + strerror(errno)); + return false; + } + + CLog::Log(LOGDEBUG, "CDRMUtils::{} - drm devices found: {}", __FUNCTION__, numDevices); + + std::vector<drmDevicePtr> devices(numDevices); + + int ret = drmGetDevices2(0, devices.data(), devices.size()); + if (ret < 0) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - drmGetDevices2 return an error: ({})", __FUNCTION__, + strerror(errno)); + return false; + } + + for (const auto device : devices) + { + if (!(device->available_nodes & 1 << DRM_NODE_PRIMARY)) + continue; + + close(m_fd); + m_fd = open(device->nodes[DRM_NODE_PRIMARY], O_RDWR | O_CLOEXEC); + if (m_fd < 0) + continue; + + if (needConnector) + { + auto resources = drmModeGetResources(m_fd); + if (!resources) + continue; + + m_connectors.clear(); + for (int i = 0; i < resources->count_connectors; i++) + m_connectors.emplace_back(std::make_unique<CDRMConnector>(m_fd, resources->connectors[i])); + + drmModeFreeResources(resources); + + if (!FindConnector()) + continue; + } + + CLog::Log(LOGDEBUG, "CDRMUtils::{} - opened device: {}", __FUNCTION__, + device->nodes[DRM_NODE_PRIMARY]); + + PrintDrmDeviceInfo(device); + + const char* renderPath = drmGetRenderDeviceNameFromFd(m_fd); + + if (!renderPath) + renderPath = drmGetDeviceNameFromFd2(m_fd); + + if (!renderPath) + renderPath = drmGetDeviceNameFromFd(m_fd); + + if (renderPath) + { + m_renderDevicePath = renderPath; + m_renderFd = open(renderPath, O_RDWR | O_CLOEXEC); + if (m_renderFd != 0) + CLog::Log(LOGDEBUG, "CDRMUtils::{} - opened render node: {}", __FUNCTION__, renderPath); + } + + drmFreeDevices(devices.data(), devices.size()); + return true; + } + + drmFreeDevices(devices.data(), devices.size()); + return false; +} + +bool CDRMUtils::InitDrm() +{ + if (m_fd < 0) + return false; + + /* caps need to be set before allocating connectors, encoders, crtcs, and planes */ + int ret = drmSetClientCap(m_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); + if (ret) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - failed to set universal planes capability: {}", + __FUNCTION__, strerror(errno)); + return false; + } + + ret = drmSetClientCap(m_fd, DRM_CLIENT_CAP_STEREO_3D, 1); + if (ret) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - failed to set stereo 3d capability: {}", __FUNCTION__, + strerror(errno)); + return false; + } + +#if defined(DRM_CLIENT_CAP_ASPECT_RATIO) + ret = drmSetClientCap(m_fd, DRM_CLIENT_CAP_ASPECT_RATIO, 0); + if (ret != 0) + CLog::Log(LOGERROR, "CDRMUtils::{} - aspect ratio capability is not supported: {}", + __FUNCTION__, strerror(errno)); +#endif + + auto resources = drmModeGetResources(m_fd); + if (!resources) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - failed to get drm resources: {}", __FUNCTION__, strerror(errno)); + return false; + } + + m_connectors.clear(); + for (int i = 0; i < resources->count_connectors; i++) + m_connectors.emplace_back(std::make_unique<CDRMConnector>(m_fd, resources->connectors[i])); + + m_encoders.clear(); + for (int i = 0; i < resources->count_encoders; i++) + m_encoders.emplace_back(std::make_unique<CDRMEncoder>(m_fd, resources->encoders[i])); + + m_crtcs.clear(); + for (int i = 0; i < resources->count_crtcs; i++) + m_crtcs.emplace_back(std::make_unique<CDRMCrtc>(m_fd, resources->crtcs[i])); + + drmModeFreeResources(resources); + + auto planeResources = drmModeGetPlaneResources(m_fd); + if (!planeResources) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - failed to get drm plane resources: {}", __FUNCTION__, strerror(errno)); + return false; + } + + m_planes.clear(); + for (uint32_t i = 0; i < planeResources->count_planes; i++) + { + m_planes.emplace_back(std::make_unique<CDRMPlane>(m_fd, planeResources->planes[i])); + m_planes[i]->FindModifiers(); + } + + drmModeFreePlaneResources(planeResources); + + if (!FindConnector()) + return false; + + if (!FindEncoder()) + return false; + + if (!FindCrtc()) + return false; + + if (!FindPlanes()) + return false; + + if (!FindPreferredMode()) + return false; + + ret = drmSetMaster(m_fd); + if (ret < 0) + { + CLog::Log(LOGDEBUG, + "CDRMUtils::{} - failed to set drm master, will try to authorize instead: {}", + __FUNCTION__, strerror(errno)); + + drm_magic_t magic; + + ret = drmGetMagic(m_fd, &magic); + if (ret < 0) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - failed to get drm magic: {}", __FUNCTION__, + strerror(errno)); + return false; + } + + ret = drmAuthMagic(m_fd, magic); + if (ret < 0) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - failed to authorize drm magic: {}", __FUNCTION__, + strerror(errno)); + return false; + } + + CLog::Log(LOGINFO, "CDRMUtils::{} - successfully authorized drm magic", __FUNCTION__); + } + + return true; +} + +bool CDRMUtils::FindConnector() +{ + auto settingsComponent = CServiceBroker::GetSettingsComponent(); + if (!settingsComponent) + return false; + + auto settings = settingsComponent->GetSettings(); + if (!settings) + return false; + + std::vector<std::unique_ptr<CDRMConnector>>::iterator connector; + + std::string connectorName = settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR); + if (connectorName != "Default") + { + connector = std::find_if(m_connectors.begin(), m_connectors.end(), + [&connectorName](auto& connector) + { + return connector->GetEncoderId() > 0 && connector->IsConnected() && + connector->GetName() == connectorName; + }); + } + + if (connector == m_connectors.end()) + { + CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to find specified connector: {}, trying default", + __FUNCTION__, connectorName); + connectorName = "Default"; + } + + if (connectorName == "Default") + { + connector = std::find_if(m_connectors.begin(), m_connectors.end(), + [](auto& connector) + { return connector->GetEncoderId() > 0 && connector->IsConnected(); }); + } + + if (connector == m_connectors.end()) + { + CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to find connected connector", __FUNCTION__); + return false; + } + + CLog::Log(LOGINFO, "CDRMUtils::{} - using connector: {}", __FUNCTION__, + connector->get()->GetName()); + + m_connector = connector->get(); + return true; +} + +bool CDRMUtils::FindEncoder() +{ + auto encoder = std::find_if(m_encoders.begin(), m_encoders.end(), [this](auto& encoder) { + return encoder->GetEncoderId() == m_connector->GetEncoderId(); + }); + + if (encoder == m_encoders.end()) + { + CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to find encoder for connector id: {}", __FUNCTION__, + *m_connector->GetConnectorId()); + return false; + } + + CLog::Log(LOGINFO, "CDRMUtils::{} - using encoder: {}", __FUNCTION__, + encoder->get()->GetEncoderId()); + + m_encoder = encoder->get(); + return true; +} + +bool CDRMUtils::FindCrtc() +{ + for (size_t i = 0; i < m_crtcs.size(); i++) + { + if (m_encoder->GetPossibleCrtcs() & (1 << i)) + { + if (m_crtcs[i]->GetCrtcId() == m_encoder->GetCrtcId()) + { + m_orig_crtc = m_crtcs[i].get(); + if (m_orig_crtc->GetModeValid()) + { + m_mode = m_orig_crtc->GetMode(); + CLog::Log(LOGDEBUG, "CDRMUtils::{} - original crtc mode: {}x{}{} @ {} Hz", __FUNCTION__, + m_mode->hdisplay, m_mode->vdisplay, + m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "", m_mode->vrefresh); + } + return true; + } + } + } + + return false; +} + +bool CDRMUtils::RestoreOriginalMode() +{ + if(!m_orig_crtc) + { + return false; + } + + auto ret = drmModeSetCrtc(m_fd, m_orig_crtc->GetCrtcId(), m_orig_crtc->GetBufferId(), + m_orig_crtc->GetX(), m_orig_crtc->GetY(), m_connector->GetConnectorId(), + 1, m_orig_crtc->GetMode()); + + if(ret) + { + CLog::Log(LOGERROR, "CDRMUtils::{} - failed to set original crtc mode", __FUNCTION__); + return false; + } + + CLog::Log(LOGDEBUG, "CDRMUtils::{} - set original crtc mode", __FUNCTION__); + + return true; +} + +void CDRMUtils::DestroyDrm() +{ + RestoreOriginalMode(); + + if (drmAuthMagic(m_fd, 0) == EINVAL) + drmDropMaster(m_fd); + + close(m_renderFd); + close(m_fd); + + m_connector = nullptr; + m_encoder = nullptr; + m_crtc = nullptr; + m_orig_crtc = nullptr; + m_video_plane = nullptr; + m_gui_plane = nullptr; +} + +RESOLUTION_INFO CDRMUtils::GetResolutionInfo(drmModeModeInfoPtr mode) +{ + RESOLUTION_INFO res; + res.iScreenWidth = mode->hdisplay; + res.iScreenHeight = mode->vdisplay; + res.iWidth = res.iScreenWidth; + res.iHeight = res.iScreenHeight; + + int limit = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + SETTING_VIDEOSCREEN_LIMITGUISIZE); + if (limit > 0 && res.iScreenWidth > 1920 && res.iScreenHeight > 1080) + { + switch (limit) + { + case 1: // 720p + res.iWidth = 1280; + res.iHeight = 720; + break; + case 2: // 1080p / 720p (>30hz) + res.iWidth = mode->vrefresh > 30 ? 1280 : 1920; + res.iHeight = mode->vrefresh > 30 ? 720 : 1080; + break; + case 3: // 1080p + res.iWidth = 1920; + res.iHeight = 1080; + break; + case 4: // Unlimited / 1080p (>30hz) + res.iWidth = mode->vrefresh > 30 ? 1920 : res.iScreenWidth; + res.iHeight = mode->vrefresh > 30 ? 1080 : res.iScreenHeight; + break; + } + } + + if (mode->clock % 5 != 0) + res.fRefreshRate = static_cast<float>(mode->vrefresh) * (1000.0f/1001.0f); + else + res.fRefreshRate = mode->vrefresh; + res.iSubtitles = res.iHeight; + res.fPixelRatio = 1.0f; + res.bFullScreen = true; + + if (mode->flags & DRM_MODE_FLAG_3D_MASK) + { + if (mode->flags & DRM_MODE_FLAG_3D_TOP_AND_BOTTOM) + res.dwFlags = D3DPRESENTFLAG_MODE3DTB; + else if (mode->flags & DRM_MODE_FLAG_3D_SIDE_BY_SIDE_HALF) + res.dwFlags = D3DPRESENTFLAG_MODE3DSBS; + } + else if (mode->flags & DRM_MODE_FLAG_INTERLACE) + res.dwFlags = D3DPRESENTFLAG_INTERLACED; + else + res.dwFlags = D3DPRESENTFLAG_PROGRESSIVE; + + res.strMode = + StringUtils::Format("{}x{}{} @ {:.6f} Hz", res.iScreenWidth, res.iScreenHeight, + res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "", res.fRefreshRate); + return res; +} + +RESOLUTION_INFO CDRMUtils::GetCurrentMode() +{ + return GetResolutionInfo(m_mode); +} + +std::vector<RESOLUTION_INFO> CDRMUtils::GetModes() +{ + std::vector<RESOLUTION_INFO> resolutions; + resolutions.reserve(m_connector->GetModesCount()); + + for (auto i = 0; i < m_connector->GetModesCount(); i++) + { + RESOLUTION_INFO res = GetResolutionInfo(m_connector->GetModeForIndex(i)); + res.strId = std::to_string(i); + resolutions.push_back(res); + } + + return resolutions; +} + +std::vector<std::string> CDRMUtils::GetConnectedConnectorNames() +{ + std::vector<std::string> connectorNames; + for (const auto& connector : m_connectors) + { + if (connector->IsConnected()) + connectorNames.emplace_back(connector->GetName()); + } + + return connectorNames; +} + +uint32_t CDRMUtils::FourCCWithAlpha(uint32_t fourcc) +{ + return (fourcc & 0xFFFFFF00) | static_cast<uint32_t>('A'); +} + +uint32_t CDRMUtils::FourCCWithoutAlpha(uint32_t fourcc) +{ + return (fourcc & 0xFFFFFF00) | static_cast<uint32_t>('X'); +} diff --git a/xbmc/windowing/gbm/drm/DRMUtils.h b/xbmc/windowing/gbm/drm/DRMUtils.h new file mode 100644 index 0000000..5327e35 --- /dev/null +++ b/xbmc/windowing/gbm/drm/DRMUtils.h @@ -0,0 +1,103 @@ +/* + * 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 + +#include "DRMConnector.h" +#include "DRMCrtc.h" +#include "DRMEncoder.h" +#include "DRMPlane.h" +#include "windowing/Resolution.h" +#include "windowing/gbm/GBMUtils.h" + +#include <vector> + +#include <gbm.h> +#include <xf86drm.h> + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +struct drm_fb +{ + struct gbm_bo *bo = nullptr; + uint32_t fb_id; + uint32_t format; +}; + +class CDRMUtils +{ +public: + CDRMUtils() = default; + virtual ~CDRMUtils(); + virtual void FlipPage(struct gbm_bo* bo, bool rendered, bool videoLayer) {} + virtual bool SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo* bo) { return false; } + virtual bool SetActive(bool active) { return false; } + virtual bool InitDrm(); + virtual void DestroyDrm(); + + int GetFileDescriptor() const { return m_fd; } + int GetRenderNodeFileDescriptor() const { return m_renderFd; } + const char* GetRenderDevicePath() const { return m_renderDevicePath; } + CDRMPlane* GetVideoPlane() const { return m_video_plane; } + CDRMPlane* GetGuiPlane() const { return m_gui_plane; } + CDRMCrtc* GetCrtc() const { return m_crtc; } + CDRMConnector* GetConnector() const { return m_connector; } + + std::vector<std::string> GetConnectedConnectorNames(); + + virtual RESOLUTION_INFO GetCurrentMode(); + virtual std::vector<RESOLUTION_INFO> GetModes(); + virtual bool SetMode(const RESOLUTION_INFO& res); + + static uint32_t FourCCWithAlpha(uint32_t fourcc); + static uint32_t FourCCWithoutAlpha(uint32_t fourcc); + +protected: + bool OpenDrm(bool needConnector); + drm_fb* DrmFbGetFromBo(struct gbm_bo *bo); + + int m_fd; + CDRMConnector* m_connector{nullptr}; + CDRMEncoder* m_encoder{nullptr}; + CDRMCrtc* m_crtc{nullptr}; + CDRMCrtc* m_orig_crtc{nullptr}; + CDRMPlane* m_video_plane{nullptr}; + CDRMPlane* m_gui_plane{nullptr}; + drmModeModeInfo *m_mode = nullptr; + + int m_width = 0; + int m_height = 0; + + std::vector<std::unique_ptr<CDRMPlane>> m_planes; + +private: + bool FindConnector(); + bool FindEncoder(); + bool FindCrtc(); + bool FindPlanes(); + bool FindPreferredMode(); + bool RestoreOriginalMode(); + RESOLUTION_INFO GetResolutionInfo(drmModeModeInfoPtr mode); + void PrintDrmDeviceInfo(drmDevicePtr device); + + int m_renderFd; + const char* m_renderDevicePath{nullptr}; + + std::vector<std::unique_ptr<CDRMConnector>> m_connectors; + std::vector<std::unique_ptr<CDRMEncoder>> m_encoders; + std::vector<std::unique_ptr<CDRMCrtc>> m_crtcs; +}; + +} +} +} diff --git a/xbmc/windowing/gbm/drm/OffScreenModeSetting.cpp b/xbmc/windowing/gbm/drm/OffScreenModeSetting.cpp new file mode 100644 index 0000000..dc69fdc --- /dev/null +++ b/xbmc/windowing/gbm/drm/OffScreenModeSetting.cpp @@ -0,0 +1,52 @@ +/* + * 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 "OffScreenModeSetting.h" + +#include "utils/log.h" + +using namespace KODI::WINDOWING::GBM; + +namespace +{ +constexpr int DISPLAY_WIDTH = 1280; +constexpr int DISPLAY_HEIGHT = 720; +constexpr float DISPLAY_REFRESH = 50.0f; +} // namespace + +bool COffScreenModeSetting::InitDrm() +{ + if (!CDRMUtils::OpenDrm(false)) + return false; + + CLog::Log(LOGDEBUG, "COffScreenModeSetting::{} - initialized offscreen DRM", __FUNCTION__); + return true; +} + +std::vector<RESOLUTION_INFO> COffScreenModeSetting::GetModes() +{ + std::vector<RESOLUTION_INFO> resolutions; + resolutions.push_back(GetCurrentMode()); + return resolutions; +} + +RESOLUTION_INFO COffScreenModeSetting::GetCurrentMode() +{ + RESOLUTION_INFO res; + res.iScreenWidth = DISPLAY_WIDTH; + res.iWidth = DISPLAY_WIDTH; + res.iScreenHeight = DISPLAY_HEIGHT; + res.iHeight = DISPLAY_HEIGHT; + res.fRefreshRate = DISPLAY_REFRESH; + res.iSubtitles = res.iHeight; + res.fPixelRatio = 1.0f; + res.bFullScreen = true; + res.strId = "0"; + + return res; +} diff --git a/xbmc/windowing/gbm/drm/OffScreenModeSetting.h b/xbmc/windowing/gbm/drm/OffScreenModeSetting.h new file mode 100644 index 0000000..4270d4e --- /dev/null +++ b/xbmc/windowing/gbm/drm/OffScreenModeSetting.h @@ -0,0 +1,38 @@ +/* + * 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 + +#include "DRMUtils.h" + +namespace KODI +{ +namespace WINDOWING +{ +namespace GBM +{ + +class COffScreenModeSetting : public CDRMUtils +{ +public: + COffScreenModeSetting() = default; + ~COffScreenModeSetting() override = default; + void FlipPage(struct gbm_bo *bo, bool rendered, bool videoLayer) override {} + bool SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo *bo) override { return false; } + bool SetActive(bool active) override { return false; } + bool InitDrm() override; + void DestroyDrm() override {} + + RESOLUTION_INFO GetCurrentMode() override; + std::vector<RESOLUTION_INFO> GetModes() override; + bool SetMode(const RESOLUTION_INFO& res) override { return true; } +}; + +} +} +} diff --git a/xbmc/windowing/ios/CMakeLists.txt b/xbmc/windowing/ios/CMakeLists.txt new file mode 100644 index 0000000..0cdd0d6 --- /dev/null +++ b/xbmc/windowing/ios/CMakeLists.txt @@ -0,0 +1,8 @@ +set(SOURCES WinEventsIOS.mm + WinSystemIOS.mm + VideoSyncIos.cpp) +set(HEADERS WinEventsIOS.h + WinSystemIOS.h + VideoSyncIos.h) + +core_add_library(windowing_ios) diff --git a/xbmc/windowing/ios/VideoSyncIos.cpp b/xbmc/windowing/ios/VideoSyncIos.cpp new file mode 100644 index 0000000..a3cac13 --- /dev/null +++ b/xbmc/windowing/ios/VideoSyncIos.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2015-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 "VideoSyncIos.h" + +#include "cores/VideoPlayer/VideoReferenceClock.h" +#include "utils/MathUtils.h" +#include "utils/TimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/ios/WinSystemIOS.h" + +bool CVideoSyncIos::Setup(PUPDATECLOCK func) +{ + CLog::Log(LOGDEBUG, "CVideoSyncIos::{} setting up OSX", __FUNCTION__); + + //init the vblank timestamp + m_LastVBlankTime = CurrentHostCounter(); + UpdateClock = func; + m_abortEvent.Reset(); + + bool setupOk = InitDisplayLink(); + if (setupOk) + { + m_winSystem.Register(this); + } + + return setupOk; +} + +void CVideoSyncIos::Run(CEvent& stopEvent) +{ + //because cocoa has a vblank callback, we just keep sleeping until we're asked to stop the thread + XbmcThreads::CEventGroup waitGroup{&stopEvent, &m_abortEvent}; + waitGroup.wait(); +} + +void CVideoSyncIos::Cleanup() +{ + CLog::Log(LOGDEBUG, "CVideoSyncIos::{} cleaning up OSX", __FUNCTION__); + DeinitDisplayLink(); + m_winSystem.Unregister(this); +} + +float CVideoSyncIos::GetFps() +{ + m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS(); + CLog::Log(LOGDEBUG, "CVideoSyncIos::{} Detected refreshrate: {:f} hertz", __FUNCTION__, m_fps); + return m_fps; +} + +void CVideoSyncIos::OnResetDisplay() +{ + m_abortEvent.Set(); +} + +void CVideoSyncIos::IosVblankHandler() +{ + int NrVBlanks; + double VBlankTime; + int64_t nowtime = CurrentHostCounter(); + + //calculate how many vblanks happened + VBlankTime = (double)(nowtime - m_LastVBlankTime) / (double)CurrentHostFrequency(); + NrVBlanks = MathUtils::round_int(VBlankTime * static_cast<double>(m_fps)); + + //save the timestamp of this vblank so we can calculate how many happened next time + m_LastVBlankTime = nowtime; + + //update the vblank timestamp, update the clock and send a signal that we got a vblank + UpdateClock(NrVBlanks, nowtime, m_refClock); +} + +bool CVideoSyncIos::InitDisplayLink() +{ + bool ret = true; + CLog::Log(LOGDEBUG, "CVideoSyncIos: setting up displaylink"); + if (!m_winSystem.InitDisplayLink(this)) + { + CLog::Log(LOGDEBUG, "CVideoSyncIos: InitDisplayLink failed"); + ret = false; + } + return ret; +} + +void CVideoSyncIos::DeinitDisplayLink() +{ + m_winSystem.DeinitDisplayLink(); +} + diff --git a/xbmc/windowing/ios/VideoSyncIos.h b/xbmc/windowing/ios/VideoSyncIos.h new file mode 100644 index 0000000..0828e34 --- /dev/null +++ b/xbmc/windowing/ios/VideoSyncIos.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015-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 "guilib/DispResource.h" +#include "windowing/VideoSync.h" + +class CWinSystemIOS; + +class CVideoSyncIos : public CVideoSync, IDispResource +{ +public: + CVideoSyncIos(void *clock, CWinSystemIOS &winSystem) : + CVideoSync(clock), m_winSystem(winSystem) {} + + // CVideoSync interface + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stopEvent) override; + void Cleanup() override; + float GetFps() override; + + // IDispResource interface + void OnResetDisplay() override; + + // used in the displaylink callback + void IosVblankHandler(); + +private: + // CVideoSyncDarwin interface + virtual bool InitDisplayLink(); + virtual void DeinitDisplayLink(); + + int64_t m_LastVBlankTime = 0; //timestamp of the last vblank, used for calculating how many vblanks happened + CEvent m_abortEvent; + CWinSystemIOS &m_winSystem; +}; + diff --git a/xbmc/windowing/ios/WinEventsIOS.h b/xbmc/windowing/ios/WinEventsIOS.h new file mode 100644 index 0000000..98ec4dc --- /dev/null +++ b/xbmc/windowing/ios/WinEventsIOS.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2012-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 "windowing/WinEvents.h" + +class CWinEventsIOS : public IWinEvents +{ +public: + bool MessagePump() override; +private: + size_t GetQueueSize(); +}; + diff --git a/xbmc/windowing/ios/WinEventsIOS.mm b/xbmc/windowing/ios/WinEventsIOS.mm new file mode 100644 index 0000000..ec416a6 --- /dev/null +++ b/xbmc/windowing/ios/WinEventsIOS.mm @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012-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 "WinEventsIOS.h" + +#include "application/AppInboundProtocol.h" +#include "guilib/GUIWindowManager.h" +#include "input/InputManager.h" +#include "input/XBMC_vkeys.h" +#include "threads/CriticalSection.h" +#include "utils/log.h" + +#include <list> +#include <mutex> + +static CCriticalSection g_inputCond; + +static std::list<XBMC_Event> events; + +bool CWinEventsIOS::MessagePump() +{ + bool ret = false; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + + // Do not always loop, only pump the initial queued count events. else if ui keep pushing + // events the loop won't finish then it will block xbmc main message loop. + for (size_t pumpEventCount = GetQueueSize(); pumpEventCount > 0; --pumpEventCount) + { + // Pop up only one event per time since in App::OnEvent it may init modal dialog which init + // deeper message loop and call the deeper MessagePump from there. + XBMC_Event pumpEvent; + { + std::unique_lock<CCriticalSection> lock(g_inputCond); + if (events.empty()) + return ret; + pumpEvent = events.front(); + events.pop_front(); + } + + if (appPort) + ret = appPort->OnEvent(pumpEvent); + } + return ret; +} + +size_t CWinEventsIOS::GetQueueSize() +{ + std::unique_lock<CCriticalSection> lock(g_inputCond); + return events.size(); +} diff --git a/xbmc/windowing/ios/WinSystemIOS.h b/xbmc/windowing/ios/WinSystemIOS.h new file mode 100644 index 0000000..8320b60 --- /dev/null +++ b/xbmc/windowing/ios/WinSystemIOS.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010-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 "rendering/gles/RenderSystemGLES.h" +#include "threads/CriticalSection.h" +#include "windowing/WinSystem.h" + +#include <string> +#include <vector> + +#include <CoreVideo/CVOpenGLESTextureCache.h> + +class IDispResource; +class CVideoSyncIos; +struct CADisplayLinkWrapper; + +class CWinSystemIOS : public CWinSystemBase, public CRenderSystemGLES +{ +public: + CWinSystemIOS(); + ~CWinSystemIOS() override; + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + int GetDisplayIndexFromSettings(); + // Implementation of CWinSystemBase + CRenderSystemBase *GetRenderSystem() override { return this; } + 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 UpdateResolutions() override; + bool CanDoWindowed() override { return false; } + + void ShowOSMouse(bool show) override {} + bool HasCursor() override; + + void NotifyAppActiveChange(bool bActivated) override; + + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + + bool IsExtSupported(const char* extension) const override; + + bool BeginRender() override; + bool EndRender() override; + + void Register(IDispResource *resource) override; + void Unregister(IDispResource *resource) override; + + std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override; + + std::vector<std::string> GetConnectedOutputs() override; + + bool InitDisplayLink(CVideoSyncIos *syncImpl); + void DeinitDisplayLink(void); + void OnAppFocusChange(bool focus); + bool IsBackgrounded() const { return m_bIsBackgrounded; } + CVEAGLContext GetEAGLContextObj(); + void MoveToTouchscreen(); + + // winevents override + bool MessagePump() override; + +protected: + void PresentRenderImpl(bool rendered) override; + void SetVSyncImpl(bool enable) override {} + + void *m_glView; // EAGLView opaque + void *m_WorkingContext; // shared EAGLContext opaque + bool m_bWasFullScreenBeforeMinimize; + std::string m_eglext; + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + bool m_bIsBackgrounded; + +private: + bool GetScreenResolution(int* w, int* h, double* fps, int screenIdx); + void FillInVideoModes(int screenIdx); + bool SwitchToVideoMode(int width, int height, double refreshrate); + CADisplayLinkWrapper *m_pDisplayLink; + int m_internalTouchscreenResolutionWidth = -1; + int m_internalTouchscreenResolutionHeight = -1; +}; + diff --git a/xbmc/windowing/ios/WinSystemIOS.mm b/xbmc/windowing/ios/WinSystemIOS.mm new file mode 100644 index 0000000..805c573 --- /dev/null +++ b/xbmc/windowing/ios/WinSystemIOS.mm @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2010-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 "WinSystemIOS.h" + +#include "ServiceBroker.h" +#include "VideoSyncIos.h" +#include "WinEventsIOS.h" +#include "cores/AudioEngine/Sinks/AESinkDARWINIOS.h" +#include "cores/RetroPlayer/process/ios/RPProcessInfoIOS.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h" +#include "cores/VideoPlayer/Process/ios/ProcessInfoIOS.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "filesystem/SpecialProtocol.h" +#include "guilib/DispResource.h" +#include "guilib/Texture.h" +#include "messaging/ApplicationMessenger.h" +#include "rendering/gles/ScreenshotSurfaceGLES.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/WindowSystemFactory.h" + +#import "platform/darwin/ios/IOSScreenManager.h" +#import "platform/darwin/ios/XBMCController.h" + +#include <mutex> +#include <vector> + +#import <Foundation/Foundation.h> +#import <OpenGLES/ES2/gl.h> +#import <OpenGLES/ES2/glext.h> +#import <QuartzCore/CADisplayLink.h> +#import <dlfcn.h> + +#define CONST_TOUCHSCREEN "Touchscreen" +#define CONST_EXTERNAL "External" + +// IOSDisplayLinkCallback is declared in the lower part of the file +@interface IOSDisplayLinkCallback : NSObject +{ +@private CVideoSyncIos *_videoSyncImpl; +} +@property (nonatomic, setter=SetVideoSyncImpl:) CVideoSyncIos *_videoSyncImpl; +- (void) runDisplayLink; +@end + +using namespace KODI; +using namespace MESSAGING; + +struct CADisplayLinkWrapper +{ + CADisplayLink* impl; + IOSDisplayLinkCallback *callbackClass; +}; + +void CWinSystemIOS::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem); +} + +std::unique_ptr<CWinSystemBase> CWinSystemIOS::CreateWinSystem() +{ + return std::make_unique<CWinSystemIOS>(); +} + +int CWinSystemIOS::GetDisplayIndexFromSettings() +{ + std::string currentScreen = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR); + + int screenIdx = 0; + if (currentScreen == CONST_EXTERNAL) + { + if ([[UIScreen screens] count] > 1) + { + screenIdx = 1; + } + else// screen 1 is setup but not connected + { + // force internal screen + MoveToTouchscreen(); + } + } + + return screenIdx; +} + +CWinSystemIOS::CWinSystemIOS() : CWinSystemBase() +{ + m_bIsBackgrounded = false; + m_pDisplayLink = new CADisplayLinkWrapper; + m_pDisplayLink->callbackClass = [[IOSDisplayLinkCallback alloc] init]; + m_winEvents.reset(new CWinEventsIOS()); + + CAESinkDARWINIOS::Register(); +} + +CWinSystemIOS::~CWinSystemIOS() +{ + delete m_pDisplayLink; +} + +bool CWinSystemIOS::InitWindowSystem() +{ + return CWinSystemBase::InitWindowSystem(); +} + +bool CWinSystemIOS::DestroyWindowSystem() +{ + return true; +} + +bool CWinSystemIOS::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + //NSLog(@"%s", __PRETTY_FUNCTION__); + + if(!SetFullScreen(fullScreen, res, false)) + return false; + + [g_xbmcController setFramebuffer]; + + m_bWindowCreated = true; + + m_eglext = " "; + + const char *tmpExtensions = (const char*) glGetString(GL_EXTENSIONS); + if (tmpExtensions != NULL) + { + m_eglext += tmpExtensions; + } + + m_eglext += " "; + + CLog::Log(LOGDEBUG, "EGL_EXTENSIONS:{}", m_eglext); + + // register platform dependent objects + CDVDFactoryCodec::ClearHWAccels(); + VTB::CDecoder::Register(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CLinuxRendererGLES::Register(); + CRendererVTB::Register(); + VIDEOPLAYER::CProcessInfoIOS::Register(); + RETRO::CRPProcessInfoIOS::Register(); + RETRO::CRPProcessInfoIOS::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES); + CScreenshotSurfaceGLES::Register(); + + return true; +} + +bool CWinSystemIOS::DestroyWindow() +{ + return true; +} + +bool CWinSystemIOS::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + //NSLog(@"%s", __PRETTY_FUNCTION__); + + if (m_nWidth != newWidth || m_nHeight != newHeight) + { + m_nWidth = newWidth; + m_nHeight = newHeight; + } + + CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight); + + return true; +} + +bool CWinSystemIOS::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + //NSLog(@"%s", __PRETTY_FUNCTION__); + + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + + CLog::Log(LOGDEBUG, "About to switch to {} x {}", m_nWidth, m_nHeight); + SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate)); + CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight); + + return true; +} + +UIScreenMode *getModeForResolution(int width, int height, unsigned int screenIdx) +{ + auto screen = UIScreen.screens[screenIdx]; + for (UIScreenMode* mode in screen.availableModes) + { + //for main screen also find modes where width and height are + //exchanged (because of the 90°degree rotated buildinscreens) + auto modeSize = mode.size; + if ((modeSize.width == width && modeSize.height == height) || + (screenIdx == 0 && modeSize.width == height && modeSize.height == width)) + { + CLog::Log(LOGDEBUG, "Found matching mode: {} x {}", modeSize.width, modeSize.height); + return mode; + } + } + CLog::Log(LOGERROR,"No matching mode found!"); + return nil; +} + +bool CWinSystemIOS::SwitchToVideoMode(int width, int height, double refreshrate) +{ + bool ret = false; + int screenIdx = GetDisplayIndexFromSettings(); + + //get the mode to pass to the controller + UIScreenMode *newMode = getModeForResolution(width, height, screenIdx); + + if(newMode) + { + ret = [g_xbmcController changeScreen:screenIdx withMode:newMode]; + } + return ret; +} + +bool CWinSystemIOS::GetScreenResolution(int* w, int* h, double* fps, int screenIdx) +{ + UIScreen *screen = [[UIScreen screens] objectAtIndex:screenIdx]; + CGSize screenSize = [screen currentMode].size; + *w = screenSize.width; + *h = screenSize.height; + *fps = 0.0; + + //if current mode is 0x0 (happens with external screens which aren't active) + //then use the preferred mode + if(*h == 0 || *w ==0) + { + UIScreenMode *firstMode = [screen preferredMode]; + *w = firstMode.size.width; + *h = firstMode.size.height; + } + + // for mainscreen use the eagl bounds from xbmcController + // because mainscreen is might be 90° rotate dependend on + // the device and eagl gives the correct values in all cases. + if(screenIdx == 0) + { + // at very first start up we cache the internal screen resolution + // because when using external screens and need to go back + // to internal we are not able to determine the eagl bounds + // before we really switched back to internal + // but display settings ask for the internal resolution before + // switching. So we give the cached values back in that case. + if (m_internalTouchscreenResolutionWidth == -1 && + m_internalTouchscreenResolutionHeight == -1) + { + m_internalTouchscreenResolutionWidth = [g_xbmcController getScreenSize].width; + m_internalTouchscreenResolutionHeight = [g_xbmcController getScreenSize].height; + } + + *w = m_internalTouchscreenResolutionWidth; + *h = m_internalTouchscreenResolutionHeight; + } + CLog::Log(LOGDEBUG, "Current resolution Screen: {} with {} x {}", screenIdx, *w, *h); + return true; +} + +void CWinSystemIOS::UpdateResolutions() +{ + // Add display resolution + int w, h; + double fps; + CWinSystemBase::UpdateResolutions(); + + int screenIdx = GetDisplayIndexFromSettings(); + + //first screen goes into the current desktop mode + if(GetScreenResolution(&w, &h, &fps, screenIdx)) + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), screenIdx == 0 ? CONST_TOUCHSCREEN : CONST_EXTERNAL, w, h, fps, 0); + + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + //now just fill in the possible resolutions for the attached screens + //and push to the resolution info vector + FillInVideoModes(screenIdx); +} + +void CWinSystemIOS::FillInVideoModes(int screenIdx) +{ + // Add full screen settings for additional monitors + RESOLUTION_INFO res; + int w, h; + // atm we don't get refreshrate info from iOS + // but this may change in the future. In that case + // we will adapt this code for filling some + // useful info into this local var :) + double refreshrate = 0.0; + //screen 0 is mainscreen - 1 has to be the external one... + UIScreen *aScreen = [[UIScreen screens]objectAtIndex:screenIdx]; + //found external screen + for ( UIScreenMode *mode in [aScreen availableModes] ) + { + w = mode.size.width; + h = mode.size.height; + + UpdateDesktopResolution(res, screenIdx == 0 ? CONST_TOUCHSCREEN : CONST_EXTERNAL, w, h, refreshrate, 0); + CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {}", screenIdx, w, h); + + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + } +} + +bool CWinSystemIOS::IsExtSupported(const char* extension) const +{ + if(strncmp(extension, "EGL_", 4) != 0) + return CRenderSystemGLES::IsExtSupported(extension); + + std::string name; + + name = " "; + name += extension; + name += " "; + + return m_eglext.find(name) != std::string::npos; +} + +bool CWinSystemIOS::BeginRender() +{ + bool rtn; + + [g_xbmcController setFramebuffer]; + + rtn = CRenderSystemGLES::BeginRender(); + return rtn; +} + +bool CWinSystemIOS::EndRender() +{ + bool rtn; + + rtn = CRenderSystemGLES::EndRender(); + return rtn; +} + +void CWinSystemIOS::Register(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemIOS::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +void CWinSystemIOS::OnAppFocusChange(bool focus) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_bIsBackgrounded = !focus; + CLog::Log(LOGDEBUG, "CWinSystemIOS::OnAppFocusChange: {}", focus ? 1 : 0); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); i++) + (*i)->OnAppFocusChange(focus); +} + +//-------------------------------------------------------------- +//-------------------DisplayLink stuff +@implementation IOSDisplayLinkCallback +@synthesize _videoSyncImpl; +//-------------------------------------------------------------- +- (void) runDisplayLink +{ + @autoreleasepool + { + if (_videoSyncImpl != nil) + { + _videoSyncImpl->IosVblankHandler(); + } + } +} +@end + +bool CWinSystemIOS::InitDisplayLink(CVideoSyncIos *syncImpl) +{ + //init with the appropriate display link for the + //used screen + if([[IOSScreenManager sharedInstance] isExternalScreen]) + { + fprintf(stderr,"InitDisplayLink on external"); + } + else + { + fprintf(stderr,"InitDisplayLink on internal"); + } + + unsigned int currentScreenIdx = [[IOSScreenManager sharedInstance] GetScreenIdx]; + UIScreen * currentScreen = [[UIScreen screens] objectAtIndex:currentScreenIdx]; + [m_pDisplayLink->callbackClass SetVideoSyncImpl:syncImpl]; + m_pDisplayLink->impl = [currentScreen displayLinkWithTarget:m_pDisplayLink->callbackClass selector:@selector(runDisplayLink)]; + + [m_pDisplayLink->impl setPreferredFramesPerSecond:0]; + [m_pDisplayLink->impl addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + return m_pDisplayLink->impl != nil; +} + +void CWinSystemIOS::DeinitDisplayLink(void) +{ + if (m_pDisplayLink->impl) + { + [m_pDisplayLink->impl invalidate]; + m_pDisplayLink->impl = nil; + [m_pDisplayLink->callbackClass SetVideoSyncImpl:nil]; + } +} +//------------DisplayLink stuff end +//-------------------------------------------------------------- + +void CWinSystemIOS::PresentRenderImpl(bool rendered) +{ + //glFlush; + if (rendered) + [g_xbmcController presentFramebuffer]; +} + +bool CWinSystemIOS::HasCursor() +{ + // apple touch devices + return false; +} + +void CWinSystemIOS::NotifyAppActiveChange(bool bActivated) +{ + if (bActivated && m_bWasFullScreenBeforeMinimize && !CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot()) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); +} + +bool CWinSystemIOS::Minimize() +{ + m_bWasFullScreenBeforeMinimize = CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot(); + if (m_bWasFullScreenBeforeMinimize) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + + return true; +} + +bool CWinSystemIOS::Restore() +{ + return false; +} + +bool CWinSystemIOS::Hide() +{ + return true; +} + +bool CWinSystemIOS::Show(bool raise) +{ + return true; +} + +CVEAGLContext CWinSystemIOS::GetEAGLContextObj() +{ + return [g_xbmcController getEAGLContextObj]; +} + +std::vector<std::string> CWinSystemIOS::GetConnectedOutputs() +{ + std::vector<std::string> outputs; + outputs.emplace_back("Default"); + outputs.emplace_back(CONST_TOUCHSCREEN); + if ([[UIScreen screens] count] > 1) + { + outputs.emplace_back(CONST_EXTERNAL); + } + + return outputs; +} + +void CWinSystemIOS::MoveToTouchscreen() +{ + CDisplaySettings::GetInstance().SetMonitor(CONST_TOUCHSCREEN); +} + +std::unique_ptr<CVideoSync> CWinSystemIOS::GetVideoSync(void *clock) +{ + std::unique_ptr<CVideoSync> pVSync(new CVideoSyncIos(clock, *this)); + return pVSync; +} + +bool CWinSystemIOS::MessagePump() +{ + return m_winEvents->MessagePump(); +} diff --git a/xbmc/windowing/linux/CMakeLists.txt b/xbmc/windowing/linux/CMakeLists.txt new file mode 100644 index 0000000..2a36d63 --- /dev/null +++ b/xbmc/windowing/linux/CMakeLists.txt @@ -0,0 +1,16 @@ +set(SOURCES "") +set(HEADERS "") + +if(DBUS_FOUND) + list(APPEND SOURCES OSScreenSaverFreedesktop.cpp) + list(APPEND HEADERS OSScreenSaverFreedesktop.h) +endif() + +if(EGL_FOUND) + list(APPEND SOURCES WinSystemEGL.cpp) + list(APPEND HEADERS WinSystemEGL.h) +endif() + +if(SOURCES) + core_add_library(windowing_linux) +endif() diff --git a/xbmc/windowing/linux/OSScreenSaverFreedesktop.cpp b/xbmc/windowing/linux/OSScreenSaverFreedesktop.cpp new file mode 100644 index 0000000..167930d --- /dev/null +++ b/xbmc/windowing/linux/OSScreenSaverFreedesktop.cpp @@ -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. + */ + +#include "OSScreenSaverFreedesktop.h" + +#include "CompileInfo.h" +#include "guilib/LocalizeStrings.h" +#include "utils/log.h" + +#include "platform/linux/DBusMessage.h" +#include "platform/linux/DBusUtil.h" + +using namespace KODI::WINDOWING::LINUX; + +namespace +{ +const std::string SCREENSAVER_OBJECT = "/org/freedesktop/ScreenSaver"; +const std::string SCREENSAVER_INTERFACE = "org.freedesktop.ScreenSaver"; +} + +bool COSScreenSaverFreedesktop::IsAvailable() +{ + // Test by making a call to a function without side-effects + CDBusMessage dummyMessage(SCREENSAVER_INTERFACE, SCREENSAVER_OBJECT, SCREENSAVER_INTERFACE, "GetActive"); + CDBusError error; + dummyMessage.SendSession(error); + // We do not care whether GetActive() is actually supported, we're just checking for the name to be there + // (GNOME for example does not support GetActive) + return !error || error.Name() == DBUS_ERROR_NOT_SUPPORTED; +} + +void COSScreenSaverFreedesktop::Inhibit() +{ + if (m_inhibited) + { + return; + } + + CDBusMessage inhibitMessage(SCREENSAVER_INTERFACE, SCREENSAVER_OBJECT, SCREENSAVER_INTERFACE, "Inhibit"); + inhibitMessage.AppendArguments(std::string(CCompileInfo::GetAppName()), + g_localizeStrings.Get(14086)); + if (!inhibitMessage.SendSession()) + { + // DBus call failed + CLog::Log(LOGERROR, "Inhibiting freedesktop screen saver failed"); + return; + } + + if (!inhibitMessage.GetReplyArguments(&m_cookie)) + { + CLog::Log(LOGERROR, "Could not retrieve cookie from org.freedesktop.ScreenSaver Inhibit response"); + return; + } + + m_inhibited = true; +} + +void COSScreenSaverFreedesktop::Uninhibit() +{ + if (!m_inhibited) + { + return; + } + + CDBusMessage uninhibitMessage(SCREENSAVER_INTERFACE, SCREENSAVER_OBJECT, SCREENSAVER_INTERFACE, "UnInhibit"); + uninhibitMessage.AppendArgument(m_cookie); + if (!uninhibitMessage.SendSession()) + { + CLog::Log(LOGERROR, "Uninhibiting freedesktop screen saver failed"); + } + + m_inhibited = false; +} diff --git a/xbmc/windowing/linux/OSScreenSaverFreedesktop.h b/xbmc/windowing/linux/OSScreenSaverFreedesktop.h new file mode 100644 index 0000000..041e8ab --- /dev/null +++ b/xbmc/windowing/linux/OSScreenSaverFreedesktop.h @@ -0,0 +1,40 @@ +/* + * 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 <cstdint> + +namespace KODI +{ +namespace WINDOWING +{ +namespace LINUX +{ + +// FIXME This is not really linux-specific, BSD could also have this. Better directory name? + +class COSScreenSaverFreedesktop : public IOSScreenSaver +{ +public: + COSScreenSaverFreedesktop() = default; + + static bool IsAvailable(); + void Inhibit() override; + void Uninhibit() override; + +private: + bool m_inhibited{false}; + std::uint32_t m_cookie; +}; + +} +} +} diff --git a/xbmc/windowing/linux/WinSystemEGL.cpp b/xbmc/windowing/linux/WinSystemEGL.cpp new file mode 100644 index 0000000..f791b91 --- /dev/null +++ b/xbmc/windowing/linux/WinSystemEGL.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2005-2020 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 "WinSystemEGL.h" + +using namespace KODI::WINDOWING::LINUX; + +CWinSystemEGL::CWinSystemEGL(EGLenum platform, std::string const& platformExtension) + : m_eglContext{platform, platformExtension} +{ +} + +EGLDisplay CWinSystemEGL::GetEGLDisplay() const +{ + return m_eglContext.GetEGLDisplay(); +} + +EGLSurface CWinSystemEGL::GetEGLSurface() const +{ + return m_eglContext.GetEGLSurface(); +} + +EGLContext CWinSystemEGL::GetEGLContext() const +{ + return m_eglContext.GetEGLContext(); +} + +EGLConfig CWinSystemEGL::GetEGLConfig() const +{ + return m_eglContext.GetEGLConfig(); +} diff --git a/xbmc/windowing/linux/WinSystemEGL.h b/xbmc/windowing/linux/WinSystemEGL.h new file mode 100644 index 0000000..44bb5e4 --- /dev/null +++ b/xbmc/windowing/linux/WinSystemEGL.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2005-2020 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/EGLUtils.h" + +namespace KODI +{ +namespace WINDOWING +{ +namespace LINUX +{ + +class CWinSystemEGL +{ +public: + CWinSystemEGL(EGLenum platform, std::string const& platformExtension); + ~CWinSystemEGL() = default; + + EGLDisplay GetEGLDisplay() const; + EGLSurface GetEGLSurface() const; + EGLContext GetEGLContext() const; + EGLConfig GetEGLConfig() const; + +protected: + CEGLContextUtils m_eglContext; +}; + +} // namespace LINUX +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/osx/CMakeLists.txt b/xbmc/windowing/osx/CMakeLists.txt new file mode 100644 index 0000000..433f5cf --- /dev/null +++ b/xbmc/windowing/osx/CMakeLists.txt @@ -0,0 +1,17 @@ +set(SOURCES CocoaDPMSSupport.cpp + OSScreenSaverOSX.cpp + VideoSyncOsx.mm) +set(HEADERS CocoaDPMSSupport.h + OSScreenSaverOSX.h + VideoSyncOsx.h) + +if(NOT SDL_FOUND) + list(APPEND SOURCES WinEventsOSX.mm + WinEventsOSXImpl.mm + WinSystemOSX.mm) + list(APPEND HEADERS WinEventsOSX.h + WinEventsOSXImpl.h + WinSystemOSX.h) +endif() + +core_add_library(windowing_osx) diff --git a/xbmc/windowing/osx/CocoaDPMSSupport.cpp b/xbmc/windowing/osx/CocoaDPMSSupport.cpp new file mode 100644 index 0000000..bc86220 --- /dev/null +++ b/xbmc/windowing/osx/CocoaDPMSSupport.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2009-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 "CocoaDPMSSupport.h" + +#include "ServiceBroker.h" +#include "utils/log.h" + +#include <CoreFoundation/CFNumber.h> +#include <IOKit/IOKitLib.h> + +CCocoaDPMSSupport::CCocoaDPMSSupport() +{ + m_supportedModes.push_back(OFF); + m_supportedModes.push_back(STANDBY); +} + +bool CCocoaDPMSSupport::EnablePowerSaving(PowerSavingMode mode) +{ + // http://lists.apple.com/archives/Cocoa-dev/2007/Nov/msg00267.html + // This is an unsupported system call that might kernel panic on PPC boxes + // The reported OSX-PPC panic is unverified so we are going to enable this until + // we find out which OSX-PPC boxes have problems, then add detect/disable for those boxes. + io_registry_entry_t r = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/IOResources/IODisplayWrangler"); + if (!r) + return false; + + switch (mode) + { + case OFF: + case STANDBY: + return (IORegistryEntrySetCFProperty(r, CFSTR("IORequestIdle"), kCFBooleanTrue) == 0); + default: + return false; + } +} + +bool CCocoaDPMSSupport::DisablePowerSaving() +{ + // http://lists.apple.com/archives/Cocoa-dev/2007/Nov/msg00267.html + // This is an unsupported system call that might kernel panic on PPC boxes + // The reported OSX-PPC panic is unverified so we are going to enable this until + // we find out which OSX-PPC boxes have problems, then add detect/disable for those boxes. + io_registry_entry_t r = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/IOResources/IODisplayWrangler"); + if (!r) + return false; + + // Turn display on + return (IORegistryEntrySetCFProperty(r, CFSTR("IORequestIdle"), kCFBooleanFalse) == 0); +} diff --git a/xbmc/windowing/osx/CocoaDPMSSupport.h b/xbmc/windowing/osx/CocoaDPMSSupport.h new file mode 100644 index 0000000..6a54f42 --- /dev/null +++ b/xbmc/windowing/osx/CocoaDPMSSupport.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009-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 "xbmc/powermanagement/DPMSSupport.h" + +class CCocoaDPMSSupport : public CDPMSSupport +{ +public: + CCocoaDPMSSupport(); + ~CCocoaDPMSSupport() override = default; + +protected: + bool EnablePowerSaving(PowerSavingMode mode) override; + bool DisablePowerSaving() override; +}; diff --git a/xbmc/windowing/osx/OSScreenSaverOSX.cpp b/xbmc/windowing/osx/OSScreenSaverOSX.cpp new file mode 100644 index 0000000..019f86c --- /dev/null +++ b/xbmc/windowing/osx/OSScreenSaverOSX.cpp @@ -0,0 +1,31 @@ +/* + * 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 "OSScreenSaverOSX.h" + +#include <CoreFoundation/CoreFoundation.h> + +void COSScreenSaverOSX::Inhibit() +{ + // see Technical Q&A QA1340 + if (m_assertionID == 0) + { + CFStringRef reasonForActivity= CFSTR("XBMC requested disable system screen saver"); + IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep, + kIOPMAssertionLevelOn, reasonForActivity, &m_assertionID); + } +} + +void COSScreenSaverOSX::Uninhibit() +{ + if (m_assertionID != 0) + { + IOPMAssertionRelease(m_assertionID); + m_assertionID = 0; + } +}
\ No newline at end of file diff --git a/xbmc/windowing/osx/OSScreenSaverOSX.h b/xbmc/windowing/osx/OSScreenSaverOSX.h new file mode 100644 index 0000000..c79367a --- /dev/null +++ b/xbmc/windowing/osx/OSScreenSaverOSX.h @@ -0,0 +1,24 @@ +/* + * 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" + +#import <IOKit/pwr_mgt/IOPMLib.h> + +class COSScreenSaverOSX : public KODI::WINDOWING::IOSScreenSaver +{ +public: + COSScreenSaverOSX() = default; + void Inhibit() override; + void Uninhibit() override; + +private: + IOPMAssertionID m_assertionID = 0; +}; diff --git a/xbmc/windowing/osx/OpenGL/CMakeLists.txt b/xbmc/windowing/osx/OpenGL/CMakeLists.txt new file mode 100644 index 0000000..fd250d7 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/CMakeLists.txt @@ -0,0 +1,14 @@ +if(OPENGL_FOUND) + set(SOURCES WinSystemOSXGL.mm) + set(HEADERS WinSystemOSXGL.h) + + if(NOT SDL_FOUND) + list(APPEND SOURCES OSXGLView.mm + OSXGLWindow.mm) + list(APPEND HEADERS OSXGLView.h + OSXGLWindow.h) + endif() + + core_add_library(windowing_osx_opengl) + +endif() diff --git a/xbmc/windowing/osx/OpenGL/OSXGLView.h b/xbmc/windowing/osx/OpenGL/OSXGLView.h new file mode 100644 index 0000000..577642d --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/OSXGLView.h @@ -0,0 +1,18 @@ +#pragma once + +/* + * Copyright (C) 2021- 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. + */ + +#import <Cocoa/Cocoa.h> + +@interface OSXGLView : NSOpenGLView + +- (id)initWithFrame:(NSRect)frameRect; +- (NSOpenGLContext*)getGLContext; + +@end diff --git a/xbmc/windowing/osx/OpenGL/OSXGLView.mm b/xbmc/windowing/osx/OpenGL/OSXGLView.mm new file mode 100644 index 0000000..6c0a8b5 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/OSXGLView.mm @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021- 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. + */ + +#import "OSXGLView.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/AppParamParser.h" +#include "application/Application.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/log.h" + +#include "system_gl.h" + +@implementation OSXGLView +{ + NSOpenGLContext* m_glcontext; + NSOpenGLPixelFormat* m_pixFmt; + NSTrackingArea* m_trackingArea; + BOOL pause; +} + +- (id)initWithFrame:(NSRect)frameRect +{ + NSOpenGLPixelFormatAttribute wattrs[] = { + NSOpenGLPFANoRecovery, NSOpenGLPFAAccelerated, + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, (NSOpenGLPixelFormatAttribute)32, + NSOpenGLPFAAlphaSize, (NSOpenGLPixelFormatAttribute)8, + NSOpenGLPFADepthSize, (NSOpenGLPixelFormatAttribute)24, + NSOpenGLPFADoubleBuffer, (NSOpenGLPixelFormatAttribute)0}; + + self = [super initWithFrame:frameRect]; + if (self) + { + m_pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs]; + m_glcontext = [[NSOpenGLContext alloc] initWithFormat:m_pixFmt shareContext:nil]; + } + + [self updateTrackingAreas]; + + GLint swapInterval = 1; + [m_glcontext setValues:&swapInterval forParameter:NSOpenGLCPSwapInterval]; + [m_glcontext makeCurrentContext]; + + return self; +} + +- (void)dealloc +{ + [NSOpenGLContext clearCurrentContext]; + [m_glcontext clearDrawable]; +} + +- (void)drawRect:(NSRect)rect +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [m_glcontext setView:self]; + + // clear screen on first render + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + glClearColor(0, 0, 0, 0); + + [m_glcontext update]; + }); +} + +- (void)updateTrackingAreas +{ + if (m_trackingArea != nil) + { + [self removeTrackingArea:m_trackingArea]; + } + + const int opts = + (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways); + m_trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds + options:opts + owner:self + userInfo:nil]; + [self addTrackingArea:m_trackingArea]; +} + +- (void)mouseEntered:(NSEvent*)theEvent +{ + [NSCursor hide]; +} + +- (void)mouseMoved:(NSEvent*)theEvent +{ +} + +- (void)mouseExited:(NSEvent*)theEvent +{ + [NSCursor unhide]; +} + +- (NSOpenGLContext*)getGLContext +{ + return m_glcontext; +} +@end diff --git a/xbmc/windowing/osx/OpenGL/OSXGLWindow.h b/xbmc/windowing/osx/OpenGL/OSXGLWindow.h new file mode 100644 index 0000000..a722804 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/OSXGLWindow.h @@ -0,0 +1,19 @@ +#pragma once + +/* + * Copyright (C) 2021- 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. + */ + +#import <Cocoa/Cocoa.h> + +@interface OSXGLWindow : NSWindow <NSWindowDelegate> + +@property(atomic) bool resizeState; + +- (id)initWithContentRect:(NSRect)box styleMask:(uint)style; + +@end diff --git a/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm b/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm new file mode 100644 index 0000000..8ee26fc --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2021- 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. + */ + +#import "OSXGLWindow.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/AppParamParser.h" +#include "application/Application.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#import "windowing/osx/OpenGL/OSXGLView.h" +#include "windowing/osx/WinEventsOSX.h" +#import "windowing/osx/WinSystemOSX.h" + +#include "platform/darwin/osx/CocoaInterface.h" + +//------------------------------------------------------------------------------------------ +@implementation OSXGLWindow + +@synthesize resizeState = m_resizeState; + +- (id)initWithContentRect:(NSRect)box styleMask:(uint)style +{ + self = [super initWithContentRect:box styleMask:style backing:NSBackingStoreBuffered defer:YES]; + [self setDelegate:self]; + [self setAcceptsMouseMovedEvents:YES]; + // autosave the window position/size + // Tell the controller to not cascade its windows. + [self.windowController setShouldCascadeWindows:NO]; + [self setFrameAutosaveName:@"OSXGLWindowPositionHeightWidth"]; + + g_application.m_AppFocused = true; + + return self; +} + +- (void)dealloc +{ + [self setDelegate:nil]; +} + +- (BOOL)windowShouldClose:(id)sender +{ + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + + return NO; +} + +- (void)windowDidExpose:(NSNotification*)aNotification +{ + g_application.m_AppFocused = true; +} + +- (void)windowDidMove:(NSNotification*)aNotification +{ + NSOpenGLContext* context = NSOpenGLContext.currentContext; + if (context) + { + if (context.view) + { + NSPoint window_origin = [[[context view] window] frame].origin; + XBMC_Event newEvent = {}; + newEvent.type = XBMC_VIDEOMOVE; + newEvent.move.x = window_origin.x; + newEvent.move.y = window_origin.y; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(newEvent); + } + } +} + +- (void)windowWillStartLiveResize:(NSNotification*)notification +{ + m_resizeState = true; +} + +- (void)windowDidEndLiveResize:(NSNotification*)notification +{ + m_resizeState = false; + [self windowDidResize:notification]; +} + +- (void)windowDidResize:(NSNotification*)aNotification +{ + if (!m_resizeState) + { + NSRect rect = [self contentRectForFrameRect:self.frame]; + int width = static_cast<int>(rect.size.width); + int height = static_cast<int>(rect.size.height); + + XBMC_Event newEvent = {}; + + if (!CServiceBroker::GetWinSystem()->IsFullScreen()) + { + RESOLUTION res_index = RES_DESKTOP; + if ((width == CDisplaySettings::GetInstance().GetResolutionInfo(res_index).iWidth) && + (height == CDisplaySettings::GetInstance().GetResolutionInfo(res_index).iHeight)) + return; + + newEvent.type = XBMC_VIDEORESIZE; + } + else + { + // macos may trigger a resize/rescale event just after Kodi has entered fullscreen + // (from windowDidEndLiveResize). Kodi needs to rescale the UI - use a different event + // type since XBMC_VIDEORESIZE is supposed to only be used in windowed mode + newEvent.type = XBMC_FULLSCREEN_UPDATE; + newEvent.move.x = -1; + newEvent.move.y = -1; + } + + newEvent.resize.w = width; + newEvent.resize.h = height; + + // check for valid sizes cause in some cases + // we are hit during fullscreen transition from macos + // and might be technically "zero" sized + if (newEvent.resize.w != 0 && newEvent.resize.h != 0) + { + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(newEvent); + } + } +} + +- (void)windowDidChangeScreen:(NSNotification*)notification +{ + // user has moved the window to a + // different screen + // if (CServiceBroker::GetWinSystem()->IsFullScreen()) + // CServiceBroker::GetWinSystem()->SetMovedToOtherScreen(true); +} + +- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)frameSize +{ + return frameSize; +} + +- (void)windowWillEnterFullScreen:(NSNotification*)pNotification +{ + CWinSystemOSX* winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return; + + // if osx is the issuer of the toggle + // call Kodi's toggle function + if (!winSystem->GetFullscreenWillToggle()) + { + // indicate that we are toggling + // flag will be reset in SetFullscreen once its + // called from Kodi's gui thread + winSystem->SetFullscreenWillToggle(true); + + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + } + else + { + // in this case we are just called because + // of Kodi did a toggle - just reset the flag + // we don't need to do anything else + winSystem->SetFullscreenWillToggle(false); + } +} + +- (void)windowDidExitFullScreen:(NSNotification*)pNotification +{ + auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return; + + // if osx is the issuer of the toggle + // call Kodi's toggle function + if (!winSystem->GetFullscreenWillToggle()) + { + // indicate that we are toggling + // flag will be reset in SetFullscreen once its + // called from Kodi's gui thread + winSystem->SetFullscreenWillToggle(true); + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + } + else + { + // in this case we are just called because + // of Kodi did a toggle - just reset the flag + // we don't need to do anything else + winSystem->SetFullscreenWillToggle(false); + } +} + +- (NSApplicationPresentationOptions)window:(NSWindow*)window + willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions +{ + // customize our appearance when entering full screen: + // we don't want the dock to appear but we want the menubar to hide/show automatically + // + return (NSApplicationPresentationFullScreen | // support full screen for this window (required) + NSApplicationPresentationHideDock | // completely hide the dock + NSApplicationPresentationAutoHideMenuBar); // yes we want the menu bar to show/hide +} + +- (void)windowDidMiniaturize:(NSNotification*)aNotification +{ + g_application.m_AppFocused = false; +} + +- (void)windowDidDeminiaturize:(NSNotification*)aNotification +{ + g_application.m_AppFocused = true; +} + +- (void)windowDidBecomeKey:(NSNotification*)aNotification +{ + g_application.m_AppFocused = true; + + auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (winSystem) + { + winSystem->enableInputEvents(); + } +} + +- (void)windowDidResignKey:(NSNotification*)aNotification +{ + g_application.m_AppFocused = false; + + auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (winSystem) + { + winSystem->disableInputEvents(); + } +} +@end diff --git a/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h new file mode 100644 index 0000000..c38e5e3 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h @@ -0,0 +1,35 @@ +/* + * 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 + +#if defined(HAS_SDL) +#include "windowing/osx/SDL/WinSystemOSXSDL.h" +#else +#include "windowing/osx/WinSystemOSX.h" +#endif +#include "rendering/gl/RenderSystemGL.h" + +class CWinSystemOSXGL : public CWinSystemOSX, public CRenderSystemGL +{ +public: + CWinSystemOSXGL() = default; + ~CWinSystemOSXGL() override = default; + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystemBase via CWinSystemOSX + CRenderSystemBase *GetRenderSystem() override { return this; } + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + +protected: + void PresentRenderImpl(bool rendered) override; + void SetVSyncImpl(bool enable) override; +}; diff --git a/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm new file mode 100644 index 0000000..30e44f2 --- /dev/null +++ b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm @@ -0,0 +1,79 @@ +/* + * 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 "WinSystemOSXGL.h" + +#include "guilib/Texture.h" +#include "rendering/gl/RenderSystemGL.h" +#include "windowing/WindowSystemFactory.h" + +#include <unistd.h> + +void CWinSystemOSXGL::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem); +} + +std::unique_ptr<CWinSystemBase> CWinSystemOSXGL::CreateWinSystem() +{ + return std::make_unique<CWinSystemOSXGL>(); +} + +void CWinSystemOSXGL::PresentRenderImpl(bool rendered) +{ + if (rendered) + FlushBuffer(); + + // FlushBuffer does not block if window is obscured + // in this case we need to throttle the render loop + if (IsObscured()) + usleep(10000); + + if (m_delayDispReset && m_dispResetTimer.IsTimePast()) + { + m_delayDispReset = false; + AnnounceOnResetDevice(); + } +} + +void CWinSystemOSXGL::SetVSyncImpl(bool enable) +{ + EnableVSync(false); + + if (enable) + { + EnableVSync(true); + } +} + +bool CWinSystemOSXGL::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + CWinSystemOSX::ResizeWindow(newWidth, newHeight, newLeft, newTop); + CRenderSystemGL::ResetRenderSystem(newWidth, newHeight); + + if (m_bVSync) + { + EnableVSync(m_bVSync); + } + + return true; +} + +bool CWinSystemOSXGL::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + CWinSystemOSX::SetFullScreen(fullScreen, res, blankOtherDisplays); + CRenderSystemGL::ResetRenderSystem(res.iWidth, res.iHeight); + + if (m_bVSync) + { + EnableVSync(m_bVSync); + } + + return true; +} + diff --git a/xbmc/windowing/osx/SDL/CMakeLists.txt b/xbmc/windowing/osx/SDL/CMakeLists.txt new file mode 100644 index 0000000..33e25d8 --- /dev/null +++ b/xbmc/windowing/osx/SDL/CMakeLists.txt @@ -0,0 +1,10 @@ +if(SDL_FOUND) + set(SOURCES WinEventsSDL.cpp + WinSystemOSXSDL.mm) + set(HEADERS WinEventsSDL.h + WinSystemOSXSDL.h) +endif() + +if(SOURCES) + core_add_library(windowing_osx_SDL) +endif() diff --git a/xbmc/windowing/osx/SDL/WinEventsSDL.cpp b/xbmc/windowing/osx/SDL/WinEventsSDL.cpp new file mode 100644 index 0000000..31b2506 --- /dev/null +++ b/xbmc/windowing/osx/SDL/WinEventsSDL.cpp @@ -0,0 +1,242 @@ +/* + * 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 "WinEventsSDL.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/Application.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "input/InputManager.h" +#include "input/Key.h" +#include "input/mouse/MouseStat.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/DisplaySettings.h" +#include "windowing/WinSystem.h" + +#include "platform/darwin/osx/CocoaInterface.h" + +bool CWinEventsOSX::MessagePump() +{ + SDL_Event event; + bool ret = false; + + while (SDL_PollEvent(&event)) + { + switch(event.type) + { + case SDL_QUIT: + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + break; + + case SDL_ACTIVEEVENT: + //If the window was inconified or restored + if( event.active.state & SDL_APPACTIVE ) + { + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->SetRenderGUI(event.active.gain != 0); + CServiceBroker::GetWinSystem()->NotifyAppActiveChange(g_application.GetRenderGUI()); + } + else if (event.active.state & SDL_APPINPUTFOCUS) + { + g_application.m_AppFocused = event.active.gain != 0; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort && g_application.m_AppFocused) + appPort->SetRenderGUI(g_application.m_AppFocused); + CServiceBroker::GetWinSystem()->NotifyAppFocusChange(g_application.m_AppFocused); + } + break; + + case SDL_KEYDOWN: + { + // process any platform specific shortcuts before handing off to XBMC + if (ProcessOSXShortcuts(event)) + { + ret = true; + break; + } + + XBMC_Event newEvent = {}; + newEvent.type = XBMC_KEYDOWN; + newEvent.key.keysym.scancode = event.key.keysym.scancode; + newEvent.key.keysym.sym = (XBMCKey) event.key.keysym.sym; + newEvent.key.keysym.unicode = event.key.keysym.unicode; + + // Check if the Windows keys are down because SDL doesn't flag this. + uint16_t mod = event.key.keysym.mod; + uint8_t* keystate = SDL_GetKeyState(NULL); + if (keystate[SDLK_LSUPER] || keystate[SDLK_RSUPER]) + mod |= XBMCKMOD_LSUPER; + newEvent.key.keysym.mod = (XBMCMod) mod; + + // don't handle any more messages in the queue until we've handled keydown, + // if a keyup is in the queue it will reset the keypress before it is handled. + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case SDL_KEYUP: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_KEYUP; + newEvent.key.keysym.scancode = event.key.keysym.scancode; + newEvent.key.keysym.sym = (XBMCKey) event.key.keysym.sym; + newEvent.key.keysym.mod =(XBMCMod) event.key.keysym.mod; + newEvent.key.keysym.unicode = event.key.keysym.unicode; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case SDL_MOUSEBUTTONDOWN: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = event.button.button; + newEvent.button.x = event.button.x; + newEvent.button.y = event.button.y; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case SDL_MOUSEBUTTONUP: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = event.button.button; + newEvent.button.x = event.button.x; + newEvent.button.y = event.button.y; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + + case SDL_MOUSEMOTION: + { + if (0 == (SDL_GetAppState() & SDL_APPMOUSEFOCUS)) + { + CServiceBroker::GetInputManager().SetMouseActive(false); + // See CApplication::ProcessSlow() for a description as to why we call Cocoa_HideMouse. + // this is here to restore the pointer when toggling back to window mode from fullscreen. + Cocoa_ShowMouse(); + break; + } + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEMOTION; + newEvent.motion.x = event.motion.x; + newEvent.motion.y = event.motion.y; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + case SDL_VIDEORESIZE: + { + // Under newer osx versions sdl is so fucked up that it even fires resize events + // that exceed the screen size (maybe some HiDP incompatibility in old SDL?) + // ensure to ignore those events because it will mess with windowed size + if((event.resize.w > CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iWidth) || + (event.resize.h > CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iHeight)) + { + break; + } + XBMC_Event newEvent = {}; + newEvent.type = XBMC_VIDEORESIZE; + newEvent.resize.w = event.resize.w; + newEvent.resize.h = event.resize.h; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + CServiceBroker::GetGUI()->GetWindowManager().MarkDirty(); + break; + } + case SDL_USEREVENT: + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_USEREVENT; + newEvent.user.code = event.user.code; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(newEvent); + break; + } + case SDL_VIDEOEXPOSE: + CServiceBroker::GetGUI()->GetWindowManager().MarkDirty(); + break; + } + memset(&event, 0, sizeof(SDL_Event)); + } + + return ret; +} + +bool CWinEventsOSX::ProcessOSXShortcuts(SDL_Event& event) +{ + static bool shift = false, cmd = false; + + cmd = !!(SDL_GetModState() & (KMOD_LMETA | KMOD_RMETA )); + shift = !!(SDL_GetModState() & (KMOD_LSHIFT | KMOD_RSHIFT)); + + if (cmd && event.key.type == SDL_KEYDOWN) + { + char keysymbol = event.key.keysym.sym; + + // if the unicode is in the ascii range + // use this instead for getting the real + // character based on the used keyboard layout + // see http://lists.libsdl.org/pipermail/sdl-libsdl.org/2004-May/043716.html + bool isControl = (event.key.keysym.mod & KMOD_CTRL) != 0; + if (!isControl && !(event.key.keysym.unicode & 0xff80)) + keysymbol = event.key.keysym.unicode; + + switch(keysymbol) + { + case SDLK_q: // CMD-q to quit + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + return true; + + case SDLK_f: // CMD-Ctrl-f to toggle fullscreen + if (!isControl) + return false; + g_application.OnAction(CAction(ACTION_TOGGLE_FULLSCREEN)); + return true; + + case SDLK_s: // CMD-3 to take a screenshot + g_application.OnAction(CAction(ACTION_TAKE_SCREENSHOT)); + return true; + + case SDLK_h: // CMD-h to hide + CServiceBroker::GetWinSystem()->Hide(); + return true; + + case SDLK_m: // CMD-m to minimize + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE); + return true; + + default: + return false; + } + } + + return false; +} diff --git a/xbmc/windowing/osx/SDL/WinEventsSDL.h b/xbmc/windowing/osx/SDL/WinEventsSDL.h new file mode 100644 index 0000000..06e6325 --- /dev/null +++ b/xbmc/windowing/osx/SDL/WinEventsSDL.h @@ -0,0 +1,24 @@ +/* + * 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 + +#include "windowing/WinEvents.h" + +#include <SDL/SDL_events.h> + +class CWinEventsOSX : public IWinEvents +{ +public: + CWinEventsOSX() = default; + ~CWinEventsOSX() override = default; + bool MessagePump() override; + +private: + static bool ProcessOSXShortcuts(SDL_Event& event); +}; diff --git a/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h new file mode 100644 index 0000000..ab19301 --- /dev/null +++ b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h @@ -0,0 +1,111 @@ +/* + * 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 + +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "threads/Timer.h" +#include "windowing/WinSystem.h" + +#include <chrono> +#include <string> +#include <vector> + +typedef struct SDL_Surface SDL_Surface; + +class IDispResource; +class CWinEventsOSX; +class CWinSystemOSXImpl; +#ifdef __OBJC__ +@class NSOpenGLContext; +#endif + +class CWinSystemOSX : public CWinSystemBase, public ITimerCallback +{ +public: + + CWinSystemOSX(); + ~CWinSystemOSX() override; + + // ITimerCallback interface + void OnTimeout() override; + + // CWinSystemBase + 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 UpdateResolutions() override; + void NotifyAppFocusChange(bool bGaining) override; + void ShowOSMouse(bool show) override; + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + void OnMove(int x, int y) override; + + std::string GetClipboardText() override; + + void Register(IDispResource *resource) override; + void Unregister(IDispResource *resource) override; + + std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override; + + std::vector<std::string> GetConnectedOutputs() override; + + void WindowChangedScreen(); + + void AnnounceOnLostDevice(); + void AnnounceOnResetDevice(); + void HandleOnResetDevice(); + void StartLostDeviceTimer(); + void StopLostDeviceTimer(); + + void* GetCGLContextObj(); +#ifdef __OBJC__ + NSOpenGLContext* GetNSOpenGLContext(); +#else + void* GetNSOpenGLContext(); +#endif + + // winevents override + bool MessagePump() override; + +protected: + std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override; + + void HandlePossibleRefreshrateChange(); + void GetScreenResolution(int* w, int* h, double* fps, int screenIdx); + void EnableVSync(bool enable); + bool SwitchToVideoMode(int width, int height, double refreshrate); + void FillInVideoModes(); + bool FlushBuffer(void); + bool IsObscured(void); + void StartTextInput(); + void StopTextInput(); + + std::unique_ptr<CWinSystemOSXImpl> m_impl; + SDL_Surface* m_SDLSurface; + CWinEventsOSX *m_osx_events; + bool m_obscured; + std::chrono::time_point<std::chrono::steady_clock> m_obscured_timecheck; + + bool m_movedToOtherScreen; + int m_lastDisplayNr; + double m_refreshRate; + + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + CTimer m_lostDeviceTimer; + bool m_delayDispReset; + XbmcThreads::EndTime<> m_dispResetTimer; + int m_updateGLContext = 0; +}; diff --git a/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm new file mode 100644 index 0000000..7a1b46b --- /dev/null +++ b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm @@ -0,0 +1,1852 @@ +/* + * 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 "WinSystemOSXSDL.h" + +#include "CompileInfo.h" +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "cores/AudioEngine/AESinkFactory.h" +#include "cores/AudioEngine/Sinks/AESinkDARWINOSX.h" +#include "cores/RetroPlayer/process/osx/RPProcessInfoOSX.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h" +#include "cores/VideoPlayer/Process/osx/ProcessInfoOSX.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "guilib/DispResource.h" +#include "guilib/GUIWindowManager.h" +#include "input/KeyboardStat.h" +#include "messaging/ApplicationMessenger.h" +#include "rendering/gl/ScreenshotSurfaceGL.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/SystemInfo.h" +#include "utils/log.h" +#include "windowing/osx/CocoaDPMSSupport.h" +#include "windowing/osx/OSScreenSaverOSX.h" +#include "windowing/osx/SDL/WinEventsSDL.h" +#include "windowing/osx/VideoSyncOsx.h" + +#include "platform/darwin/DarwinUtils.h" +#include "platform/darwin/DictionaryUtils.h" +#include "platform/darwin/osx/CocoaInterface.h" +#import "platform/darwin/osx/SDL/OSXTextInputResponder.h" +#include "platform/darwin/osx/XBMCHelper.h" + +#include <cstdlib> +#include <mutex> +#include <signal.h> + +#import <Cocoa/Cocoa.h> +#import <IOKit/graphics/IOGraphicsLib.h> +#import <IOKit/pwr_mgt/IOPMLib.h> +#import <QuartzCore/QuartzCore.h> +#import <SDL/SDL.h> + +// turn off deprecated warning spew. +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +using namespace KODI; +using namespace WINDOWING; +using namespace std::chrono_literals; + +//------------------------------------------------------------------------------------------ +// special object-c class for handling the NSWindowDidMoveNotification callback. +@interface windowDidMoveNoteClass : NSObject +{ + void *m_userdata; +} ++ (windowDidMoveNoteClass*) initWith: (void*) userdata; +- (void) windowDidMoveNotification:(NSNotification*) note; +@end + +@implementation windowDidMoveNoteClass ++ (windowDidMoveNoteClass*) initWith: (void*) userdata +{ + windowDidMoveNoteClass *windowDidMove = [windowDidMoveNoteClass new]; + windowDidMove->m_userdata = userdata; + return windowDidMove; +} +- (void) windowDidMoveNotification:(NSNotification*) note +{ + CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata; + if (!winsys) + return; + + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + if (context) + { + if ([context view]) + { + NSPoint window_origin = [[[context view] window] frame].origin; + XBMC_Event newEvent = {}; + newEvent.type = XBMC_VIDEOMOVE; + newEvent.move.x = window_origin.x; + newEvent.move.y = window_origin.y; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(newEvent); + } + } +} +@end +//------------------------------------------------------------------------------------------ +// special object-c class for handling the NSWindowDidReSizeNotification callback. +@interface windowDidReSizeNoteClass : NSObject +{ + void *m_userdata; +} ++ (windowDidReSizeNoteClass*) initWith: (void*) userdata; +- (void) windowDidReSizeNotification:(NSNotification*) note; +@end +@implementation windowDidReSizeNoteClass ++ (windowDidReSizeNoteClass*) initWith: (void*) userdata +{ + windowDidReSizeNoteClass *windowDidReSize = [windowDidReSizeNoteClass new]; + windowDidReSize->m_userdata = userdata; + return windowDidReSize; +} +- (void) windowDidReSizeNotification:(NSNotification*) note +{ + CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata; + if (!winsys) + return; + +} +@end + +//------------------------------------------------------------------------------------------ +// special object-c class for handling the NSWindowDidChangeScreenNotification callback. +@interface windowDidChangeScreenNoteClass : NSObject +{ + void *m_userdata; +} ++ (windowDidChangeScreenNoteClass*) initWith: (void*) userdata; +- (void) windowDidChangeScreenNotification:(NSNotification*) note; +@end +@implementation windowDidChangeScreenNoteClass ++ (windowDidChangeScreenNoteClass*) initWith: (void*) userdata +{ + windowDidChangeScreenNoteClass *windowDidChangeScreen = [windowDidChangeScreenNoteClass new]; + windowDidChangeScreen->m_userdata = userdata; + return windowDidChangeScreen; +} +- (void) windowDidChangeScreenNotification:(NSNotification*) note +{ + CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata; + if (!winsys) + return; + winsys->WindowChangedScreen(); +} +@end +//------------------------------------------------------------------------------------------ + +class CWinSystemOSXImpl +{ +public: + NSOpenGLContext* m_glContext; + static NSOpenGLContext* m_lastOwnedContext; + + windowDidMoveNoteClass* m_windowDidMove; + windowDidReSizeNoteClass* m_windowDidReSize; + windowDidChangeScreenNoteClass* m_windowChangedScreen; +}; + +NSOpenGLContext* CWinSystemOSXImpl::m_lastOwnedContext = nil; + + +#define MAX_DISPLAYS 32 +// if there was a devicelost callback +// but no device reset for 3 secs +// a timeout fires the reset callback +// (for ensuring that e.x. AE isn't stuck) +constexpr auto LOST_DEVICE_TIMEOUT_MS = 3000ms; +static NSWindow* blankingWindows[MAX_DISPLAYS]; + +//------------------------------------------------------------------------------------------ +CRect CGRectToCRect(CGRect cgrect) +{ + CRect crect = CRect( + cgrect.origin.x, + cgrect.origin.y, + cgrect.origin.x + cgrect.size.width, + cgrect.origin.y + cgrect.size.height); + return crect; +} +//--------------------------------------------------------------------------------- +void SetMenuBarVisible(bool visible) +{ + if(visible) + { + [[NSApplication sharedApplication] + setPresentationOptions: NSApplicationPresentationDefault]; + } + else + { + [[NSApplication sharedApplication] + setPresentationOptions: NSApplicationPresentationHideMenuBar | + NSApplicationPresentationHideDock]; + } +} +//--------------------------------------------------------------------------------- +CGDirectDisplayID GetDisplayID(int screen_index) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + CGDisplayCount numDisplays; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + if (screen_index >= 0 && screen_index < static_cast<int>(numDisplays)) + return(displayArray[screen_index]); + else + return(displayArray[0]); +} + +size_t DisplayBitsPerPixelForMode(CGDisplayModeRef mode) +{ + size_t bitsPerPixel = 0; + + CFStringRef pixEnc = CGDisplayModeCopyPixelEncoding(mode); + if(CFStringCompare(pixEnc, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) + { + bitsPerPixel = 32; + } + else if(CFStringCompare(pixEnc, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) + { + bitsPerPixel = 16; + } + else if(CFStringCompare(pixEnc, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo) + { + bitsPerPixel = 8; + } + + CFRelease(pixEnc); + + return bitsPerPixel; +} + +CFArrayRef GetAllDisplayModes(CGDirectDisplayID display) +{ + int value = 1; + + CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value); + if (!number) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Number!"); + return NULL; + } + + CFStringRef key = kCGDisplayShowDuplicateLowResolutionModes; + CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, (const void **)&key, (const void **)&number, 1, NULL, NULL); + CFRelease(number); + + if (!options) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Dictionary!"); + return NULL; + } + + CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(display, options); + CFRelease(options); + + if (!displayModes) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - no displaymodes found!"); + return NULL; + } + + return displayModes; +} + +// mimic former behavior of deprecated CGDisplayBestModeForParameters +CGDisplayModeRef BestMatchForMode(CGDirectDisplayID display, size_t bitsPerPixel, size_t width, size_t height, boolean_t &match) +{ + + // Get a copy of the current display mode + CGDisplayModeRef displayMode = CGDisplayCopyDisplayMode(display); + + // Loop through all display modes to determine the closest match. + // CGDisplayBestModeForParameters is deprecated on 10.6 so we will emulate it's behavior + // Try to find a mode with the requested depth and equal or greater dimensions first. + // If no match is found, try to find a mode with greater depth and same or greater dimensions. + // If still no match is found, just use the current mode. + CFArrayRef allModes = GetAllDisplayModes(display); + + for(int i = 0; i < CFArrayGetCount(allModes); i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + + if(DisplayBitsPerPixelForMode(mode) != bitsPerPixel) + continue; + + if((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height)) + { + CGDisplayModeRelease(displayMode); // release the copy we got before ... + displayMode = mode; + match = true; + break; + } + } + + // No depth match was found + if(!match) + { + for(int i = 0; i < CFArrayGetCount(allModes); i++) + { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if(DisplayBitsPerPixelForMode(mode) >= bitsPerPixel) + continue; + + if((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height)) + { + displayMode = mode; + match = true; + break; + } + } + } + + CFRelease(allModes); + + return displayMode; +} + +CGDirectDisplayID GetDisplayIDFromScreen(NSScreen *screen) +{ + NSDictionary* screenInfo = [screen deviceDescription]; + NSNumber* screenID = [screenInfo objectForKey:@"NSScreenNumber"]; + + return (CGDirectDisplayID)[screenID longValue]; +} + +int GetDisplayIndex(CGDirectDisplayID display) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + CGDisplayCount numDisplays; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + while (numDisplays > 0) + { + if (display == displayArray[--numDisplays]) + return numDisplays; + } + return -1; +} + +void BlankOtherDisplays(int screen_index) +{ + int i; + int numDisplays = [[NSScreen screens] count]; + + // zero out blankingWindows for debugging + for (i=0; i<MAX_DISPLAYS; i++) + { + blankingWindows[i] = 0; + } + + // Blank. + for (i=0; i<numDisplays; i++) + { + if (i != screen_index) + { + // Get the size. + NSScreen* pScreen = [[NSScreen screens] objectAtIndex:i]; + NSRect screenRect = [pScreen frame]; + + // Build a blanking window. + screenRect.origin = NSZeroPoint; + blankingWindows[i] = [[NSWindow alloc] initWithContentRect:screenRect + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO + screen:pScreen]; + + [blankingWindows[i] setBackgroundColor:[NSColor blackColor]]; + [blankingWindows[i] setLevel:CGShieldingWindowLevel()]; + [blankingWindows[i] makeKeyAndOrderFront:nil]; + } + } +} + +void UnblankDisplays(void) +{ + int numDisplays = [[NSScreen screens] count]; + int i = 0; + + for (i=0; i<numDisplays; i++) + { + if (blankingWindows[i] != 0) + { + // Get rid of the blanking windows we created. + [blankingWindows[i] close]; + blankingWindows[i] = 0; + } + } +} + +CGDisplayFadeReservationToken DisplayFadeToBlack(bool fade) +{ + // Fade to black to hide resolution-switching flicker and garbage. + CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken; + if (CGAcquireDisplayFadeReservation (5, &fade_token) == kCGErrorSuccess && fade) + CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, TRUE); + + return(fade_token); +} + +void DisplayFadeFromBlack(CGDisplayFadeReservationToken fade_token, bool fade) +{ + if (fade_token != kCGDisplayFadeReservationInvalidToken) + { + if (fade) + CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, 0.0, FALSE); + CGReleaseDisplayFadeReservation(fade_token); + } +} + +NSString* screenNameForDisplay(CGDirectDisplayID displayID) +{ + NSString* screenName; + @autoreleasepool + { + NSDictionary* deviceInfo = (__bridge_transfer NSDictionary*)IODisplayCreateInfoDictionary( + CGDisplayIOServicePort(displayID), kIODisplayOnlyPreferredName); + NSDictionary* localizedNames = + [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]]; + + if ([localizedNames count] > 0) + { + screenName = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]]; + } + } + + if (screenName == nil) + { + screenName = [[NSString alloc] initWithFormat:@"%i", displayID]; + } + else + { + // ensure screen name is unique by appending displayid + screenName = [screenName stringByAppendingFormat:@" (%@)", [@(displayID) stringValue]]; + } + + return screenName; +} + +int GetDisplayIndex(const std::string& dispName) +{ + int ret = 0; + + // Add full screen settings for additional monitors + int numDisplays = [[NSScreen screens] count]; + + for (int disp = 0; disp < numDisplays; disp++) + { + NSString *name = screenNameForDisplay(GetDisplayID(disp)); + if ([name UTF8String] == dispName) + { + ret = disp; + break; + } + } + + return ret; +} + +void ShowHideNSWindow(NSWindow *wind, bool show) +{ + if (show) + [wind orderFront:nil]; + else + [wind orderOut:nil]; +} + +static NSWindow *curtainWindow; +void fadeInDisplay(NSScreen *theScreen, double fadeTime) +{ + int fadeSteps = 100; + double fadeInterval = (fadeTime / (double) fadeSteps); + + if (curtainWindow != nil) + { + for (int step = 0; step < fadeSteps; step++) + { + double fade = 1.0 - (step * fadeInterval); + [curtainWindow setAlphaValue:fade]; + + NSDate *nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:nextDate]; + } + } + [curtainWindow close]; + curtainWindow = nil; + + [NSCursor unhide]; +} + +void fadeOutDisplay(NSScreen *theScreen, double fadeTime) +{ + int fadeSteps = 100; + double fadeInterval = (fadeTime / (double) fadeSteps); + + [NSCursor hide]; + + curtainWindow = [[NSWindow alloc] + initWithContentRect:[theScreen frame] + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:YES + screen:theScreen]; + + [curtainWindow setAlphaValue:0.0]; + [curtainWindow setBackgroundColor:[NSColor blackColor]]; + [curtainWindow setLevel:NSScreenSaverWindowLevel]; + + [curtainWindow makeKeyAndOrderFront:nil]; + [curtainWindow setFrame:[curtainWindow + frameRectForContentRect:[theScreen frame]] + display:YES + animate:NO]; + + for (int step = 0; step < fadeSteps; step++) + { + double fade = step * fadeInterval; + [curtainWindow setAlphaValue:fade]; + + NSDate *nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval]; + [[NSRunLoop currentRunLoop] runUntilDate:nextDate]; + } +} + +// try to find mode that matches the desired size, refreshrate +// non interlaced, nonstretched, safe for hardware +CGDisplayModeRef GetMode(int width, int height, double refreshrate, int screenIdx) +{ + if ( screenIdx >= (signed)[[NSScreen screens] count]) + return NULL; + + Boolean stretched; + Boolean interlaced; + Boolean safeForHardware; + Boolean televisionoutput; + int w, h, bitsperpixel; + double rate; + RESOLUTION_INFO res; + + CLog::Log(LOGDEBUG, "GetMode looking for suitable mode with {} x {} @ {:f} Hz on display {}", + width, height, refreshrate, screenIdx); + + CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(screenIdx)); + + if (!displayModes) + return NULL; + + for (int i=0; i < CFArrayGetCount(displayModes); ++i) + { + CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i); + uint32_t flags = CGDisplayModeGetIOFlags(displayMode); + stretched = flags & kDisplayModeStretchedFlag ? true : false; + interlaced = flags & kDisplayModeInterlacedFlag ? true : false; + bitsperpixel = DisplayBitsPerPixelForMode(displayMode); + safeForHardware = flags & kDisplayModeSafetyFlags ? true : false; + televisionoutput = flags & kDisplayModeTelevisionFlag ? true : false; + w = CGDisplayModeGetWidth(displayMode); + h = CGDisplayModeGetHeight(displayMode); + rate = CGDisplayModeGetRefreshRate(displayMode); + + + if ((bitsperpixel == 32) && + (safeForHardware == YES) && + (stretched == NO) && + (interlaced == NO) && + (w == width) && + (h == height) && + (rate == refreshrate || rate == 0)) + { + CLog::Log(LOGDEBUG, "GetMode found a match!"); + return displayMode; + } + } + + CFRelease(displayModes); + CLog::Log(LOGERROR, "GetMode - no match found!"); + return NULL; +} + +//--------------------------------------------------------------------------------- +static void DisplayReconfigured(CGDirectDisplayID display, + CGDisplayChangeSummaryFlags flags, void* userData) +{ + CWinSystemOSX *winsys = (CWinSystemOSX*)userData; + if (!winsys) + return; + + CLog::Log(LOGDEBUG, "CWinSystemOSX::DisplayReconfigured with flags {}", flags); + + // we fire the callbacks on start of configuration + // or when the mode set was finished + // or when we are called with flags == 0 (which is undocumented but seems to happen + // on some macs - we treat it as device reset) + + // first check if we need to call OnLostDevice + if (flags & kCGDisplayBeginConfigurationFlag) + { + // pre/post-reconfiguration changes + RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); + if (res == RES_INVALID) + return; + + NSScreen* pScreen = nil; + unsigned int screenIdx = 0; + + if ( screenIdx < [[NSScreen screens] count] ) + { + pScreen = [[NSScreen screens] objectAtIndex:screenIdx]; + } + + // kCGDisplayBeginConfigurationFlag is only fired while the screen is still + // valid + if (pScreen) + { + CGDirectDisplayID xbmc_display = GetDisplayIDFromScreen(pScreen); + if (xbmc_display == display) + { + // we only respond to changes on the display we are running on. + winsys->AnnounceOnLostDevice(); + winsys->StartLostDeviceTimer(); + } + } + } + else // the else case checks if we need to call OnResetDevice + { + // we fire if kCGDisplaySetModeFlag is set or if flags == 0 + // (which is undocumented but seems to happen + // on some macs - we treat it as device reset) + // we also don't check the screen here as we might not even have + // one anymore (e.x. when tv is turned off) + if (flags & kCGDisplaySetModeFlag || flags == 0) + { + winsys->StopLostDeviceTimer(); // no need to timeout - we've got the callback + winsys->HandleOnResetDevice(); + } + } + + if ((flags & kCGDisplayAddFlag) || (flags & kCGDisplayRemoveFlag)) + winsys->UpdateResolutions(); +} + +//------------------------------------------------------------------------------ +NSOpenGLContext* CreateWindowedContext(NSOpenGLContext* shareCtx); +void ResizeWindowInternal(int newWidth, int newHeight, int newLeft, int newTop, NSView* last_view); + +//------------------------------------------------------------------------------ +CWinSystemOSX::CWinSystemOSX() + : CWinSystemBase() + , m_impl{new CWinSystemOSXImpl} + , m_lostDeviceTimer(this) +{ + m_SDLSurface = NULL; + m_osx_events = NULL; + m_obscured = false; + m_obscured_timecheck = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000); + m_lastDisplayNr = -1; + m_movedToOtherScreen = false; + m_refreshRate = 0.0; + m_delayDispReset = false; + + m_winEvents.reset(new CWinEventsOSX()); + + AE::CAESinkFactory::ClearSinks(); + CAESinkDARWINOSX::Register(); + m_dpms = std::make_shared<CCocoaDPMSSupport>(); +} + +CWinSystemOSX::~CWinSystemOSX() = default; + +void CWinSystemOSX::StartLostDeviceTimer() +{ + if (m_lostDeviceTimer.IsRunning()) + m_lostDeviceTimer.Restart(); + else + m_lostDeviceTimer.Start(LOST_DEVICE_TIMEOUT_MS, false); +} + +void CWinSystemOSX::StopLostDeviceTimer() +{ + m_lostDeviceTimer.Stop(); +} + +void CWinSystemOSX::OnTimeout() +{ + HandleOnResetDevice(); +} + +bool CWinSystemOSX::InitWindowSystem() +{ + CLog::LogF(LOGINFO, "Setup SDL"); + + /* Clean up on exit, exit on window close and interrupt */ + std::atexit(SDL_Quit); + + if (SDL_Init(SDL_INIT_VIDEO) != 0) + { + CLog::LogF(LOGFATAL, "Unable to initialize SDL: {}", SDL_GetError()); + return false; + } + // SDL_Init will install a handler for segfaults, restore the default handler. + signal(SIGSEGV, SIG_DFL); + + SDL_EnableUNICODE(1); + + // set repeat to 10ms to ensure repeat time < frame time + // so that hold times can be reliably detected + SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, 10); + + if (!CWinSystemBase::InitWindowSystem()) + return false; + + m_osx_events = new CWinEventsOSX(); + + CGDisplayRegisterReconfigurationCallback(DisplayReconfigured, (void*)this); + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + auto windowDidMove = [windowDidMoveNoteClass initWith:this]; + [center addObserver:windowDidMove + selector:@selector(windowDidMoveNotification:) + name:NSWindowDidMoveNotification object:nil]; + m_impl->m_windowDidMove = windowDidMove; + + auto windowDidReSize = [windowDidReSizeNoteClass initWith:this]; + [center addObserver:windowDidReSize + selector:@selector(windowDidReSizeNotification:) + name:NSWindowDidResizeNotification object:nil]; + m_impl->m_windowDidReSize = windowDidReSize; + + auto windowDidChangeScreen = [windowDidChangeScreenNoteClass initWith:this]; + [center addObserver:windowDidChangeScreen + selector:@selector(windowDidChangeScreenNotification:) + name:NSWindowDidChangeScreenNotification object:nil]; + m_impl->m_windowChangedScreen = windowDidChangeScreen; + + return true; +} + +bool CWinSystemOSX::DestroyWindowSystem() +{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center removeObserver:m_impl->m_windowDidMove name:NSWindowDidMoveNotification object:nil]; + [center removeObserver:m_impl->m_windowDidReSize name:NSWindowDidResizeNotification object:nil]; + [center removeObserver:m_impl->m_windowChangedScreen + name:NSWindowDidChangeScreenNotification + object:nil]; + + CGDisplayRemoveReconfigurationCallback(DisplayReconfigured, (void*)this); + + delete m_osx_events; + m_osx_events = NULL; + + UnblankDisplays(); + m_impl->m_glContext = nil; + return true; +} + +bool CWinSystemOSX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + // force initial window creation to be windowed, if fullscreen, it will switch to it below + // fixes the white screen of death if starting fullscreen and switching to windowed. + RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + m_nWidth = resInfo.iWidth; + m_nHeight = resInfo.iHeight; + m_bFullScreen = false; + + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + + // Enable vertical sync to avoid any tearing. + SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1); + + m_SDLSurface = SDL_SetVideoMode(m_nWidth, m_nHeight, 0, SDL_OPENGL | SDL_RESIZABLE); + if (!m_SDLSurface) + return false; + + // the context SDL creates isn't full screen compatible, so we create new one + // first, find the current contect and make sure a view is attached + NSOpenGLContext* cur_context = [NSOpenGLContext currentContext]; + NSView* view = [cur_context view]; + if (!view) + return false; + + if (CDisplaySettings::GetInstance().GetCurrentResolution() != RES_WINDOW) + { + // If we are not starting up windowed, then hide the initial SDL window + // so we do not see it flash before the fade-out and switch to fullscreen. + ShowHideNSWindow([view window], false); + } + + // disassociate view from context + [cur_context clearDrawable]; + + // release the context + if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context) + { + [ NSOpenGLContext clearCurrentContext ]; + [ cur_context clearDrawable ]; + cur_context = nil; + } + + // create a new context + auto new_context = CreateWindowedContext(nil); + if (!new_context) + return false; + + // associate with current view + [new_context setView:view]; + [new_context makeCurrentContext]; + + // set the window title + [[[new_context view] window] + setTitle:[NSString stringWithFormat:@"%s Media Center", CCompileInfo::GetAppName()]]; + + m_impl->m_glContext = new_context; + CWinSystemOSXImpl::m_lastOwnedContext = new_context; + m_bWindowCreated = true; + + // get screen refreshrate - this is needed + // when we startup in windowed mode and don't run through SetFullScreen + int dummy; + GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr); + + // register platform dependent objects + CDVDFactoryCodec::ClearHWAccels(); + VTB::CDecoder::Register(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CLinuxRendererGL::Register(); + CRendererVTB::Register(); + VIDEOPLAYER::CProcessInfoOSX::Register(); + RETRO::CRPProcessInfoOSX::Register(); + RETRO::CRPProcessInfoOSX::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL); + CScreenshotSurfaceGL::Register(); + + return true; +} + +bool CWinSystemOSX::DestroyWindow() +{ + return true; +} + +void ResizeWindowInternal(int newWidth, int newHeight, int newLeft, int newTop, NSView* last_view) +{ + if (last_view && [last_view window]) + { + auto size = NSMakeSize(newWidth, newHeight); + NSWindow* lastWindow = [last_view window]; + [lastWindow setContentSize:size]; + [lastWindow update]; + [last_view setFrameSize:size]; + } +} +bool CWinSystemOSX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + if (!m_impl->m_glContext) + return false; + + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + NSView* view; + NSWindow* window; + + view = [context view]; + + if (view) + { + // It seems, that in macOS 10.15 this defaults to YES, but we currently do not support + // Retina resolutions properly. Ensure that the view uses a 1 pixel per point framebuffer. + view.wantsBestResolutionOpenGLSurface = NO; + } + + if (view && (newWidth > 0) && (newHeight > 0)) + { + window = [view window]; + if (window) + { + [window setContentSize:NSMakeSize(newWidth, newHeight)]; + [window update]; + [view setFrameSize:NSMakeSize(newWidth, newHeight)]; + [context update]; + // this is needed in case we traverse from fullscreen screen 2 + // to windowed on screen 1 directly where in ScreenChangedNotification + // we don't have a window to get the current screen on + // in that case ResizeWindow is called at a later stage from SetFullScreen(false) + // and we can grab the correct display number here then + m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen( [window screen] )); + } + } + + // HACK: resize SDL's view manually so that mouse bounds are correctly updated. + // there are two parts to this, the internal SDL (current_video->screen) and + // the cocoa view ( handled in SetFullScreen). + SDL_SetWidthHeight(newWidth, newHeight); + + [context makeCurrentContext]; + + m_nWidth = newWidth; + m_nHeight = newHeight; + m_impl->m_glContext = context; + CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(m_refreshRate); + + return true; +} + +static bool needtoshowme = true; + +bool CWinSystemOSX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + static NSWindow* windowedFullScreenwindow = nil; + static NSPoint last_window_origin; + static NSView* last_view = nil; + static NSSize last_view_size; + static NSPoint last_view_origin; + static NSInteger last_window_level = NSNormalWindowLevel; + bool was_fullscreen = m_bFullScreen; + NSOpenGLContext* cur_context; + + // Fade to black to hide resolution-switching flicker and garbage. + CGDisplayFadeReservationToken fade_token = DisplayFadeToBlack(needtoshowme); + + // If we're already fullscreen then we must be moving to a different display. + // or if we are still on the same display - it might be only a refreshrate/resolution + // change request. + // Recurse to reset fullscreen mode and then continue. + if (was_fullscreen && fullScreen) + { + needtoshowme = false; + ShowHideNSWindow([last_view window], needtoshowme); + RESOLUTION_INFO& window = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + CWinSystemOSX::SetFullScreen(false, window, blankOtherDisplays); + needtoshowme = true; + } + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + m_lastDisplayNr = GetDisplayIndex(settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + + cur_context = [NSOpenGLContext currentContext]; + + //handle resolution/refreshrate switching early here + if (m_bFullScreen) + { + // switch videomode + SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate)); + } + + //no context? done. + if (!cur_context) + { + DisplayFadeFromBlack(fade_token, needtoshowme); + return false; + } + + if (windowedFullScreenwindow != nil) + { + [windowedFullScreenwindow close]; + windowedFullScreenwindow = nil; + } + + if (m_bFullScreen) + { + // FullScreen Mode + NSOpenGLContext* newContext = nil; + + // Save info about the windowed context so we can restore it when returning to windowed. + last_view = [cur_context view]; + last_view_size = [last_view frame].size; + last_view_origin = [last_view frame].origin; + last_window_origin = [[last_view window] frame].origin; + last_window_level = [[last_view window] level]; + + // This is Cocoa Windowed FullScreen Mode + // Get the screen rect of our current display + NSScreen* pScreen = [[NSScreen screens] objectAtIndex:m_lastDisplayNr]; + NSRect screenRect = [pScreen frame]; + + // remove frame origin offset of original display + screenRect.origin = NSZeroPoint; + + // make a new window to act as the windowedFullScreen + windowedFullScreenwindow = [[NSWindow alloc] initWithContentRect:screenRect + styleMask:NSBorderlessWindowMask + backing:NSBackingStoreBuffered + defer:NO + screen:pScreen]; + windowedFullScreenwindow.releasedWhenClosed = NO; + + [windowedFullScreenwindow setBackgroundColor:[NSColor blackColor]]; + [windowedFullScreenwindow makeKeyAndOrderFront:nil]; + + // make our window the same level as the rest to enable cmd+tab switching + [windowedFullScreenwindow setLevel:NSNormalWindowLevel]; + // this will make our window topmost and hide all system messages + //[windowedFullScreenwindow setLevel:CGShieldingWindowLevel()]; + + // ...and the original one beneath it and on the same screen. + [[last_view window] setLevel:NSNormalWindowLevel-1]; + [[last_view window] setFrameOrigin:[pScreen frame].origin]; + // expand the mouse bounds in SDL view to fullscreen + [ last_view setFrameOrigin:NSMakePoint(0.0, 0.0)]; + [ last_view setFrameSize:NSMakeSize(m_nWidth, m_nHeight) ]; + + NSView* blankView = [[NSView alloc] init]; + [windowedFullScreenwindow setContentView:blankView]; + [windowedFullScreenwindow setContentSize:NSMakeSize(m_nWidth, m_nHeight)]; + [windowedFullScreenwindow update]; + [blankView setFrameSize:NSMakeSize(m_nWidth, m_nHeight)]; + + // force SDL's window origin to be at zero: + // on Monterey, X coordinate becomes non-zero somewhere after this method returns + const auto twoSeconds = dispatch_time(DISPATCH_TIME_NOW, 2LL * NSEC_PER_SEC); + dispatch_after(twoSeconds, dispatch_get_main_queue(), ^{ + for (NSWindow* w in NSApp.windows) + { + if (w == windowedFullScreenwindow) + continue; + [w setFrameOrigin:w.screen.frame.origin]; + break; + } + }); + + // Obtain windowed pixel format and create a new context. + newContext = CreateWindowedContext(cur_context); + [newContext setView:blankView]; + + // Hide the menu bar. + SetMenuBarVisible(false); + + // Blank other displays if requested. + if (blankOtherDisplays) + BlankOtherDisplays(m_lastDisplayNr); + + // Hide the mouse. + [NSCursor hide]; + + // Release old context if we created it. + if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context) + { + [NSOpenGLContext clearCurrentContext]; + [cur_context clearDrawable]; + } + + // activate context + [newContext makeCurrentContext]; + CWinSystemOSXImpl::m_lastOwnedContext = newContext; + } + else + { + // Windowed Mode + // exit fullscreen + [cur_context clearDrawable]; + + [NSCursor unhide]; + + // Show menubar. + SetMenuBarVisible(true); + + // restore the windowed window level + [[last_view window] setLevel:last_window_level]; + + // Unblank. + // Force the unblank when returning from fullscreen, we get called with blankOtherDisplays set false. + //if (blankOtherDisplays) + UnblankDisplays(); + + // create our new context (sharing with the current one) + auto newContext = CreateWindowedContext(cur_context); + if (!newContext) + return false; + + // Assign view from old context, move back to original screen. + [newContext setView:last_view]; + [[last_view window] setFrameOrigin:last_window_origin]; + // return the mouse bounds in SDL view to previous size + [last_view setFrameSize:last_view_size]; + [last_view setFrameOrigin:last_view_origin]; + + // Release the fullscreen context. + if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context) + { + [NSOpenGLContext clearCurrentContext]; + [cur_context clearDrawable]; + } + + // Activate context. + [newContext makeCurrentContext]; + CWinSystemOSXImpl::m_lastOwnedContext = newContext; + } + + DisplayFadeFromBlack(fade_token, needtoshowme); + + ShowHideNSWindow([last_view window], needtoshowme); + // need to make sure SDL tracks any window size changes + ResizeWindow(m_nWidth, m_nHeight, -1, -1); + ResizeWindowInternal(m_nWidth, m_nHeight, -1, -1, last_view); + // restore origin once again when going to windowed mode + if (!fullScreen) + { + [[last_view window] setFrameOrigin:last_window_origin]; + } + HandlePossibleRefreshrateChange(); + + m_updateGLContext = 0; + return true; +} + +void CWinSystemOSX::UpdateResolutions() +{ + CWinSystemBase::UpdateResolutions(); + + // Add desktop resolution + int w, h; + double fps; + + int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + GetScreenResolution(&w, &h, &fps, dispIdx); + NSString* dispName = screenNameForDisplay(GetDisplayID(dispIdx)); + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), [dispName UTF8String], w, h, fps, 0); + + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + // now just fill in the possible resolutions for the attached screens + // and push to the resolution info vector + FillInVideoModes(); + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +/* +void* Cocoa_GL_CreateContext(void* pixFmt, void* shareCtx) +{ + if (!pixFmt) + return nil; + + NSOpenGLContext* newContext = [[NSOpenGLContext alloc] initWithFormat:(NSOpenGLPixelFormat*)pixFmt + shareContext:(NSOpenGLContext*)shareCtx]; + + // snipit from SDL_cocoaopengl.m + // + // Wisdom from Apple engineer in reference to UT2003's OpenGL performance: + // "You are blowing a couple of the internal OpenGL function caches. This + // appears to be happening in the VAO case. You can tell OpenGL to up + // the cache size by issuing the following calls right after you create + // the OpenGL context. The default cache size is 16." --ryan. + // + + #ifndef GLI_ARRAY_FUNC_CACHE_MAX + #define GLI_ARRAY_FUNC_CACHE_MAX 284 + #endif + + #ifndef GLI_SUBMIT_FUNC_CACHE_MAX + #define GLI_SUBMIT_FUNC_CACHE_MAX 280 + #endif + + { + long cache_max = 64; + CGLContextObj ctx = (CGLContextObj)[newContext CGLContextObj]; + CGLSetParameter(ctx, (CGLContextParameter)GLI_SUBMIT_FUNC_CACHE_MAX, &cache_max); + CGLSetParameter(ctx, (CGLContextParameter)GLI_ARRAY_FUNC_CACHE_MAX, &cache_max); + } + + // End Wisdom from Apple Engineer section. --ryan. + return newContext; +} +*/ + +NSOpenGLContext* CreateWindowedContext(NSOpenGLContext* shareCtx) +{ + NSOpenGLPixelFormat* pixFmt; + if (getenv("KODI_GL_PROFILE_LEGACY")) + { + NSOpenGLPixelFormatAttribute wattrs[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFANoRecovery, + NSOpenGLPFAAccelerated, + NSOpenGLPFADepthSize, + static_cast<NSOpenGLPixelFormatAttribute>(8), + static_cast<NSOpenGLPixelFormatAttribute>(0)}; + pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs]; + } + else + { + NSOpenGLPixelFormatAttribute wattrs_gl3[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFAOpenGLProfile, + NSOpenGLProfileVersion3_2Core, + NSOpenGLPFANoRecovery, + NSOpenGLPFAAccelerated, + NSOpenGLPFADepthSize, + static_cast<NSOpenGLPixelFormatAttribute>(24), + static_cast<NSOpenGLPixelFormatAttribute>(0)}; + pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs_gl3]; + } + + auto newContext = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:shareCtx]; + + if (!newContext) + { + // bah, try again for non-accelerated renderer + NSOpenGLPixelFormatAttribute wattrs2[] = + { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFANoRecovery, + NSOpenGLPFADepthSize, (NSOpenGLPixelFormatAttribute)8, + (NSOpenGLPixelFormatAttribute)0 + }; + newContext = [[NSOpenGLContext alloc] + initWithFormat:[[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs2] + shareContext:shareCtx]; + } + + return newContext; +} + +NSOpenGLContext* CreateFullScreenContext(int screen_index, NSOpenGLContext* shareCtx) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + CGDisplayCount numDisplays; + CGDirectDisplayID displayID; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + displayID = displayArray[screen_index]; + + NSOpenGLPixelFormat* pixFmt; + if (getenv("KODI_GL_PROFILE_LEGACY")) + { + NSOpenGLPixelFormatAttribute fsattrs[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFANoRecovery, + NSOpenGLPFAAccelerated, + NSOpenGLPFADepthSize, + static_cast<NSOpenGLPixelFormatAttribute>(8), + NSOpenGLPFAScreenMask, + static_cast<NSOpenGLPixelFormatAttribute>(CGDisplayIDToOpenGLDisplayMask(displayID)), + static_cast<NSOpenGLPixelFormatAttribute>(0)}; + pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:fsattrs]; + } + else + { + NSOpenGLPixelFormatAttribute fsattrs_gl3[] = { + NSOpenGLPFADoubleBuffer, + NSOpenGLPFANoRecovery, + NSOpenGLPFAAccelerated, + NSOpenGLPFADepthSize, + static_cast<NSOpenGLPixelFormatAttribute>(24), + NSOpenGLPFAOpenGLProfile, + NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAScreenMask, + static_cast<NSOpenGLPixelFormatAttribute>(CGDisplayIDToOpenGLDisplayMask(displayID)), + static_cast<NSOpenGLPixelFormatAttribute>(0)}; + pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:fsattrs_gl3]; + } + + if (!pixFmt) + return nil; + + auto newContext = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:shareCtx]; + + return newContext; +} + +void CWinSystemOSX::GetScreenResolution(int* w, int* h, double* fps, int screenIdx) +{ + CGDirectDisplayID display_id = (CGDirectDisplayID)GetDisplayID(screenIdx); + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display_id); + *w = CGDisplayModeGetWidth(mode); + *h = CGDisplayModeGetHeight(mode); + *fps = CGDisplayModeGetRefreshRate(mode); + CGDisplayModeRelease(mode); + if ((int)*fps == 0) + { + // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays. + *fps = 60.0; + } +} + +void CWinSystemOSX::EnableVSync(bool enable) +{ + // OpenGL Flush synchronised with vertical retrace + GLint swapInterval = enable ? 1 : 0; + [[NSOpenGLContext currentContext] setValues:&swapInterval forParameter:NSOpenGLCPSwapInterval]; +} + +bool CWinSystemOSX::SwitchToVideoMode(int width, int height, double refreshrate) +{ + boolean_t match = false; + CGDisplayModeRef dispMode = NULL; + + int screenIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + + // Figure out the screen size. (default to main screen) + CGDirectDisplayID display_id = GetDisplayID(screenIdx); + + // find mode that matches the desired size, refreshrate + // non interlaced, nonstretched, safe for hardware + dispMode = GetMode(width, height, refreshrate, screenIdx); + + //not found - fallback to bestemdeforparameters + if (!dispMode) + { + dispMode = BestMatchForMode(display_id, 32, width, height, match); + + if (!match) + dispMode = BestMatchForMode(display_id, 16, width, height, match); + + // still no match? fallback to current resolution of the display which HAS to work [tm] + if (!match) + { + int tmpWidth; + int tmpHeight; + double tmpRefresh; + + GetScreenResolution(&tmpWidth, &tmpHeight, &tmpRefresh, screenIdx); + dispMode = GetMode(tmpWidth, tmpHeight, tmpRefresh, screenIdx); + + // no way to get a resolution set + if (!dispMode) + return false; + } + + if (!match) + return false; + } + + // switch mode and return success + CGDisplayCapture(display_id); + CGDisplayConfigRef cfg; + CGBeginDisplayConfiguration(&cfg); + CGConfigureDisplayWithDisplayMode(cfg, display_id, dispMode, nullptr); + CGError err = CGCompleteDisplayConfiguration(cfg, kCGConfigureForAppOnly); + CGDisplayRelease(display_id); + + m_refreshRate = CGDisplayModeGetRefreshRate(dispMode); + + Cocoa_CVDisplayLinkUpdate(); + + return (err == kCGErrorSuccess); +} + +void CWinSystemOSX::FillInVideoModes() +{ + int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + + // Add full screen settings for additional monitors + int numDisplays = [[NSScreen screens] count]; + + for (int disp = 0; disp < numDisplays; disp++) + { + Boolean stretched; + Boolean interlaced; + Boolean safeForHardware; + Boolean televisionoutput; + int w, h, bitsperpixel; + double refreshrate; + RESOLUTION_INFO res; + + CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(disp)); + NSString *dispName = screenNameForDisplay(GetDisplayID(disp)); + + CLog::Log(LOGINFO, "Display {} has name {}", disp, [dispName UTF8String]); + + if (NULL == displayModes) + continue; + + for (int i=0; i < CFArrayGetCount(displayModes); ++i) + { + CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i); + + uint32_t flags = CGDisplayModeGetIOFlags(displayMode); + stretched = flags & kDisplayModeStretchedFlag ? true : false; + interlaced = flags & kDisplayModeInterlacedFlag ? true : false; + bitsperpixel = DisplayBitsPerPixelForMode(displayMode); + safeForHardware = flags & kDisplayModeSafetyFlags ? true : false; + televisionoutput = flags & kDisplayModeTelevisionFlag ? true : false; + + if ((bitsperpixel == 32) && + (safeForHardware == YES) && + (stretched == NO) && + (interlaced == NO)) + { + w = CGDisplayModeGetWidth(displayMode); + h = CGDisplayModeGetHeight(displayMode); + refreshrate = CGDisplayModeGetRefreshRate(displayMode); + if ((int)refreshrate == 0) // LCD display? + { + // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays. + refreshrate = 60.0; + } + CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {} @ {:f} Hz", disp, + w, h, refreshrate); + + // only add the resolution if it belongs to "our" screen + // all others are only logged above... + if (disp == dispIdx) + { + UpdateDesktopResolution(res, (dispName != nil) ? [dispName UTF8String] : "Unknown", w, h, refreshrate, 0); + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + } + } + } + CFRelease(displayModes); + } +} + +bool CWinSystemOSX::FlushBuffer(void) +{ + if (m_updateGLContext < 5) + { + [m_impl->m_glContext update]; + m_updateGLContext++; + } + + [m_impl->m_glContext flushBuffer]; + + return true; +} + +bool CWinSystemOSX::IsObscured(void) +{ + // check once a second if we are obscured. + auto now_time = std::chrono::steady_clock::now(); + if (m_obscured_timecheck > now_time) + return m_obscured; + else + m_obscured_timecheck = now_time + std::chrono::milliseconds(1000); + + NSOpenGLContext* cur_context = [NSOpenGLContext currentContext]; + NSView* view = [cur_context view]; + if (!view) + { + // sanity check, we should always have a view + m_obscured = true; + return m_obscured; + } + + NSWindow *window = [view window]; + if (!window) + { + // sanity check, we should always have a window + m_obscured = true; + return m_obscured; + } + + if ([window isVisible] == NO) + { + // not visible means the window is not showing. + // this should never really happen as we are always visible + // even when minimized in dock. + m_obscured = true; + return m_obscured; + } + + // check if we are minimized (to an icon in the Dock). + if ([window isMiniaturized] == YES) + { + m_obscured = true; + return m_obscured; + } + + // check if we are showing on the active workspace. + if ([window isOnActiveSpace] == NO) + { + m_obscured = true; + return m_obscured; + } + + // default to false before we start parsing though the windows. + // if we are are obscured by any windows, then set true. + m_obscured = false; + static bool obscureLogged = false; + + CGWindowListOption opts; + opts = kCGWindowListOptionOnScreenAboveWindow | kCGWindowListExcludeDesktopElements; + CFArrayRef windowIDs =CGWindowListCreate(opts, (CGWindowID)[window windowNumber]); + + if (!windowIDs) + return m_obscured; + + CFArrayRef windowDescs = CGWindowListCreateDescriptionFromArray(windowIDs); + if (!windowDescs) + { + CFRelease(windowIDs); + return m_obscured; + } + + CGRect bounds = NSRectToCGRect([window frame]); + // kCGWindowBounds measures the origin as the top-left corner of the rectangle + // relative to the top-left corner of the screen. + // NSWindow’s frame property measures the origin as the bottom-left corner + // of the rectangle relative to the bottom-left corner of the screen. + // convert bounds from NSWindow to CGWindowBounds here. + bounds.origin.y = [[window screen] frame].size.height - bounds.origin.y - bounds.size.height; + + std::vector<CRect> partialOverlaps; + CRect ourBounds = CGRectToCRect(bounds); + + for (CFIndex idx=0; idx < CFArrayGetCount(windowDescs); idx++) + { + // walk the window list of windows that are above us and are not desktop elements + CFDictionaryRef windowDictionary = (CFDictionaryRef)CFArrayGetValueAtIndex(windowDescs, idx); + + // skip the Dock window, it actually covers the entire screen. + CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(windowDictionary, kCGWindowOwnerName); + if (CFStringCompare(ownerName, CFSTR("Dock"), 0) == kCFCompareEqualTo) + continue; + + // Ignore known brightness tools for dimming the screen. They claim to cover + // the whole XBMC window and therefore would make the framerate limiter + // kicking in. Unfortunately even the alpha of these windows is 1.0 so + // we have to check the ownerName. + if (CFStringCompare(ownerName, CFSTR("Shades"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("SmartSaver"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("Brightness Slider"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("Displaperture"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("Dreamweaver"), 0) == kCFCompareEqualTo || + CFStringCompare(ownerName, CFSTR("Window Server"), 0) == kCFCompareEqualTo) + continue; + + CFDictionaryRef rectDictionary = (CFDictionaryRef)CFDictionaryGetValue(windowDictionary, kCGWindowBounds); + if (!rectDictionary) + continue; + + CGRect windowBounds; + if (CGRectMakeWithDictionaryRepresentation(rectDictionary, &windowBounds)) + { + if (CGRectContainsRect(windowBounds, bounds)) + { + // if the windowBounds completely encloses our bounds, we are obscured. + if (!obscureLogged) + { + std::string appName; + if (CDarwinUtils::CFStringRefToUTF8String(ownerName, appName)) + CLog::Log(LOGDEBUG, "WinSystemOSX: Fullscreen window {} obscures Kodi!", appName); + obscureLogged = true; + } + m_obscured = true; + break; + } + + // handle overlapping windows above us that combine + // to obscure by collecting any partial overlaps, + // then subtract them from our bounds and check + // for any remaining area. + CRect intersection = CGRectToCRect(windowBounds); + intersection.Intersect(ourBounds); + if (!intersection.IsEmpty()) + partialOverlaps.push_back(intersection); + } + } + + if (!m_obscured) + { + // if we are here we are not obscured by any fullscreen window - reset flag + // for allowing the logmessage above to show again if this changes. + if (obscureLogged) + obscureLogged = false; + std::vector<CRect> rects = ourBounds.SubtractRects(partialOverlaps); + // they got us covered + if (rects.empty()) + m_obscured = true; + } + + CFRelease(windowDescs); + CFRelease(windowIDs); + + return m_obscured; +} + +void CWinSystemOSX::NotifyAppFocusChange(bool bGaining) +{ + if (!(m_bFullScreen && bGaining)) + return; + @autoreleasepool + { + // find the window + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + if (context) + { + NSView* view; + + view = [context view]; + if (view) + { + NSWindow* window; + window = [view window]; + if (window) + { + SetMenuBarVisible(false); + [window orderFront:nil]; + } + } + } + } +} + +void CWinSystemOSX::ShowOSMouse(bool show) +{ + SDL_ShowCursor(show ? 1 : 0); +} + +bool CWinSystemOSX::Minimize() +{ + @autoreleasepool + { + [[NSApplication sharedApplication] miniaturizeAll:nil]; + } + return true; +} + +bool CWinSystemOSX::Restore() +{ + @autoreleasepool + { + [[NSApplication sharedApplication] unhide:nil]; + } + return true; +} + +bool CWinSystemOSX::Hide() +{ + @autoreleasepool + { + [[NSApplication sharedApplication] hide:nil]; + } + return true; +} + +void CWinSystemOSX::HandlePossibleRefreshrateChange() +{ + static double oldRefreshRate = m_refreshRate; + Cocoa_CVDisplayLinkUpdate(); + int dummy = 0; + + GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr); + + if (oldRefreshRate != m_refreshRate) + { + oldRefreshRate = m_refreshRate; + // send a message so that videoresolution (and refreshrate) + // is changed + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_VIDEORESIZE, m_SDLSurface->w, m_SDLSurface->h); + } +} + +void CWinSystemOSX::OnMove(int x, int y) +{ + HandlePossibleRefreshrateChange(); +} + +std::unique_ptr<IOSScreenSaver> CWinSystemOSX::GetOSScreenSaverImpl() +{ + return std::unique_ptr<IOSScreenSaver> (new COSScreenSaverOSX); +} + +OSXTextInputResponder *g_textInputResponder = nil; + +void CWinSystemOSX::StartTextInput() +{ + NSView *parentView = [[NSApp keyWindow] contentView]; + + /* We only keep one field editor per process, since only the front most + * window can receive text input events, so it make no sense to keep more + * than one copy. When we switched to another window and requesting for + * text input, simply remove the field editor from its superview then add + * it to the front most window's content view */ + if (!g_textInputResponder) { + g_textInputResponder = + [[OSXTextInputResponder alloc] initWithFrame: NSMakeRect(0.0, 0.0, 0.0, 0.0)]; + } + + if (![[g_textInputResponder superview] isEqual: parentView]) + { +// DLOG(@"add fieldEdit to window contentView"); + [g_textInputResponder removeFromSuperview]; + [parentView addSubview: g_textInputResponder]; + [[NSApp keyWindow] makeFirstResponder: g_textInputResponder]; + } +} +void CWinSystemOSX::StopTextInput() +{ + if (g_textInputResponder) + { + [g_textInputResponder removeFromSuperview]; + g_textInputResponder = nil; + } +} + +void CWinSystemOSX::Register(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemOSX::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +bool CWinSystemOSX::Show(bool raise) +{ + @autoreleasepool + { + auto app = [NSApplication sharedApplication]; + if (raise) + { + [app unhide:nil]; + [app activateIgnoringOtherApps:YES]; + [app arrangeInFront:nil]; + } + else + { + [app unhideWithoutActivation]; + } + } + return true; +} + +void CWinSystemOSX::WindowChangedScreen() +{ + // user has moved the window to a + // different screen + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + m_lastDisplayNr = -1; + + // if we are here the user dragged the window to a different + // screen and we return the screen of the window + if (context) + { + NSView* view; + + view = [context view]; + if (view) + { + NSWindow* window; + window = [view window]; + if (window) + { + m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen([window screen])); + } + } + } + if (m_lastDisplayNr == -1) + m_lastDisplayNr = 0;// default to main screen +} + +void CWinSystemOSX::AnnounceOnLostDevice() +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::Log(LOGDEBUG, "CWinSystemOSX::AnnounceOnLostDevice"); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnLostDisplay(); +} + +void CWinSystemOSX::HandleOnResetDevice() +{ + + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + if (delay > 0ms) + { + m_delayDispReset = true; + m_dispResetTimer.Set(delay); + } + else + { + AnnounceOnResetDevice(); + } +} + +void CWinSystemOSX::AnnounceOnResetDevice() +{ + double currentFps = m_refreshRate; + int w = 0; + int h = 0; + int currentScreenIdx = m_lastDisplayNr; + // ensure that graphics context knows about the current refreshrate before + // doing the callbacks + GetScreenResolution(&w, &h, ¤tFps, currentScreenIdx); + + CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(currentFps); + + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::Log(LOGDEBUG, "CWinSystemOSX::AnnounceOnResetDevice"); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); +} + +void* CWinSystemOSX::GetCGLContextObj() +{ + return [m_impl->m_glContext CGLContextObj]; +} + +NSOpenGLContext* CWinSystemOSX::GetNSOpenGLContext() +{ + return m_impl->m_glContext; +} + +std::string CWinSystemOSX::GetClipboardText(void) +{ + std::string utf8_text; + + const char *szStr = Cocoa_Paste(); + if (szStr) + utf8_text = szStr; + + return utf8_text; +} + +std::unique_ptr<CVideoSync> CWinSystemOSX::GetVideoSync(void *clock) +{ + std::unique_ptr<CVideoSync> pVSync(new CVideoSyncOsx(clock)); + return pVSync; +} + +bool CWinSystemOSX::MessagePump() +{ + return m_winEvents->MessagePump(); +} + +std::vector<std::string> CWinSystemOSX::GetConnectedOutputs() +{ + std::vector<std::string> outputs; + outputs.emplace_back("Default"); + + int numDisplays = [[NSScreen screens] count]; + + for (int disp = 0; disp < numDisplays; disp++) + { + NSString *dispName = screenNameForDisplay(GetDisplayID(disp)); + outputs.emplace_back([dispName UTF8String]); + } + + return outputs; +} diff --git a/xbmc/windowing/osx/VideoSyncOsx.h b/xbmc/windowing/osx/VideoSyncOsx.h new file mode 100644 index 0000000..b3ba145 --- /dev/null +++ b/xbmc/windowing/osx/VideoSyncOsx.h @@ -0,0 +1,46 @@ +/* + * 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 + +#include "guilib/DispResource.h" +#include "threads/Event.h" +#include "windowing/VideoSync.h" + +class CVideoSyncOsx : public CVideoSync, IDispResource +{ +public: + CVideoSyncOsx(void* clock) + : CVideoSync(clock), m_LastVBlankTime(0), m_displayLost(false), m_displayReset(false) + { + } + + // CVideoSync interface + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stopEvent) override; + void Cleanup() override; + float GetFps() override; + void RefreshChanged() override; + + // IDispResource interface + void OnLostDisplay() override; + void OnResetDisplay() override; + + // used in the displaylink callback + void VblankHandler(int64_t nowtime, uint32_t timebase); + +private: + virtual bool InitDisplayLink(); + virtual void DeinitDisplayLink(); + + int64_t m_LastVBlankTime; //timestamp of the last vblank, used for calculating how many vblanks happened + volatile bool m_displayLost; + volatile bool m_displayReset; + CEvent m_lostEvent; +}; + diff --git a/xbmc/windowing/osx/VideoSyncOsx.mm b/xbmc/windowing/osx/VideoSyncOsx.mm new file mode 100644 index 0000000..d19e724 --- /dev/null +++ b/xbmc/windowing/osx/VideoSyncOsx.mm @@ -0,0 +1,148 @@ +/* + * 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 "VideoSyncOsx.h" + +#include "ServiceBroker.h" +#include "utils/MathUtils.h" +#include "utils/TimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/WinSystem.h" + +#include "platform/darwin/osx/CocoaInterface.h" + +#include <CoreVideo/CVHostTime.h> +#include <QuartzCore/CVDisplayLink.h> +#include <unistd.h> + +using namespace std::chrono_literals; + +bool CVideoSyncOsx::Setup(PUPDATECLOCK func) +{ + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} setting up OSX", __FUNCTION__); + + //init the vblank timestamp + m_LastVBlankTime = 0; + UpdateClock = func; + m_displayLost = false; + m_displayReset = false; + m_lostEvent.Reset(); + + CServiceBroker::GetWinSystem()->Register(this); + + return true; +} + +void CVideoSyncOsx::Run(CEvent& stopEvent) +{ + InitDisplayLink(); + + //because cocoa has a vblank callback, we just keep sleeping until we're asked to stop the thread + while(!stopEvent.Signaled() && !m_displayLost && !m_displayReset) + { + usleep(100000); + } + + m_lostEvent.Set(); + + while(!stopEvent.Signaled() && m_displayLost && !m_displayReset) + { + usleep(10000); + } + + DeinitDisplayLink(); +} + +void CVideoSyncOsx::Cleanup() +{ + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} cleaning up OSX", __FUNCTION__); + m_lostEvent.Set(); + m_LastVBlankTime = 0; + CServiceBroker::GetWinSystem()->Unregister(this); +} + +float CVideoSyncOsx::GetFps() +{ + m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS(); + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} Detected refreshrate: {:f} hertz", __FUNCTION__, m_fps); + return m_fps; +} + +void CVideoSyncOsx::RefreshChanged() +{ + m_displayReset = true; +} + +void CVideoSyncOsx::OnLostDisplay() +{ + if (!m_displayLost) + { + m_displayLost = true; + m_lostEvent.Wait(1000ms); + } +} + +void CVideoSyncOsx::OnResetDisplay() +{ + m_displayReset = true; +} + +void CVideoSyncOsx::VblankHandler(int64_t nowtime, uint32_t timebase) +{ + int NrVBlanks; + double VBlankTime; + int64_t Now = CurrentHostCounter(); + + if (m_LastVBlankTime != 0) + { + VBlankTime = (double)(nowtime - m_LastVBlankTime) / (double)timebase; + NrVBlanks = MathUtils::round_int(VBlankTime * static_cast<double>(m_fps)); + + //update the vblank timestamp, update the clock and send a signal that we got a vblank + UpdateClock(NrVBlanks, Now, m_refClock); + } + + //save the timestamp of this vblank so we can calculate how many happened next time + m_LastVBlankTime = nowtime; +} + +// Called by the Core Video Display Link whenever it's appropriate to render a frame. +static CVReturn DisplayLinkCallBack(CVDisplayLinkRef displayLink, const CVTimeStamp* inNow, const CVTimeStamp* inOutputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext) +{ + @autoreleasepool + { + CVideoSyncOsx* VideoSyncOsx = reinterpret_cast<CVideoSyncOsx*>(displayLinkContext); + + if (inOutputTime->flags & kCVTimeStampHostTimeValid) + VideoSyncOsx->VblankHandler(inOutputTime->hostTime, CVGetHostClockFrequency()); + else + VideoSyncOsx->VblankHandler(CVGetCurrentHostTime(), CVGetHostClockFrequency()); + } + + return kCVReturnSuccess; +} + +bool CVideoSyncOsx::InitDisplayLink() +{ + bool ret = true; + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} setting up displaylink", __FUNCTION__); + + if (!Cocoa_CVDisplayLinkCreate((void*)DisplayLinkCallBack, reinterpret_cast<void*>(this))) + { + CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} Cocoa_CVDisplayLinkCreate failed", __FUNCTION__); + ret = false; + } + return ret; +} + +void CVideoSyncOsx::DeinitDisplayLink() +{ + Cocoa_CVDisplayLinkRelease(); +} + diff --git a/xbmc/windowing/osx/WinEventsOSX.h b/xbmc/windowing/osx/WinEventsOSX.h new file mode 100644 index 0000000..7c4644f --- /dev/null +++ b/xbmc/windowing/osx/WinEventsOSX.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011-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/Thread.h" +#include "windowing/WinEvents.h" +#include "windowing/XBMC_events.h" + +#include <memory> + +struct CWinEventsOSXImplWrapper; + +class CWinEventsOSX : public IWinEvents, public CThread +{ +public: + CWinEventsOSX(); + ~CWinEventsOSX(); + + void MessagePush(XBMC_Event* newEvent); + bool MessagePump(); + size_t GetQueueSize(); + + void enableInputEvents(); + void disableInputEvents(); + +private: + std::unique_ptr<CWinEventsOSXImplWrapper> m_eventsImplWrapper; +}; diff --git a/xbmc/windowing/osx/WinEventsOSX.mm b/xbmc/windowing/osx/WinEventsOSX.mm new file mode 100644 index 0000000..b2d07db --- /dev/null +++ b/xbmc/windowing/osx/WinEventsOSX.mm @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2011-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 "WinEventsOSX.h" + +#import "WinEventsOSXImpl.h" + +struct CWinEventsOSXImplWrapper +{ + CWinEventsOSXImpl* callbackClass; +}; + +CWinEventsOSX::CWinEventsOSX() : CThread("CWinEventsOSX") +{ + m_eventsImplWrapper = std::make_unique<CWinEventsOSXImplWrapper>(); + m_eventsImplWrapper->callbackClass = [CWinEventsOSXImpl new]; + Create(); +} + +CWinEventsOSX::~CWinEventsOSX() +{ + m_bStop = true; + StopThread(true); +} + +void CWinEventsOSX::MessagePush(XBMC_Event* newEvent) +{ + [m_eventsImplWrapper->callbackClass MessagePush:newEvent]; +} + +size_t CWinEventsOSX::GetQueueSize() +{ + return [m_eventsImplWrapper->callbackClass GetQueueSize]; +} + +bool CWinEventsOSX::MessagePump() +{ + return [m_eventsImplWrapper->callbackClass MessagePump]; +} + +void CWinEventsOSX::enableInputEvents() +{ + return [m_eventsImplWrapper->callbackClass enableInputEvents]; +} + +void CWinEventsOSX::disableInputEvents() +{ + return [m_eventsImplWrapper->callbackClass disableInputEvents]; +} diff --git a/xbmc/windowing/osx/WinEventsOSXImpl.h b/xbmc/windowing/osx/WinEventsOSXImpl.h new file mode 100644 index 0000000..2dcacd4 --- /dev/null +++ b/xbmc/windowing/osx/WinEventsOSXImpl.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011-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 "windowing/osx/WinEventsOSX.h" + +#import <CoreGraphics/CoreGraphics.h> +#import <Foundation/Foundation.h> + +@interface CWinEventsOSXImpl : NSObject + +- (void)MessagePush:(XBMC_Event*)newEvent; +- (size_t)GetQueueSize; +- (bool)MessagePump; +- (void)enableInputEvents; +- (void)disableInputEvents; +- (XBMC_Event)keyPressEvent:(CGEventRef*)event; + +@end diff --git a/xbmc/windowing/osx/WinEventsOSXImpl.mm b/xbmc/windowing/osx/WinEventsOSXImpl.mm new file mode 100644 index 0000000..48c4e37 --- /dev/null +++ b/xbmc/windowing/osx/WinEventsOSXImpl.mm @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2012-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 "WinEventsOSXImpl.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/Application.h" +#include "input/XBMC_keysym.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "input/mouse/MouseStat.h" +#include "messaging/ApplicationMessenger.h" +#include "threads/CriticalSection.h" +#include "utils/log.h" +#include "windowing/osx/WinSystemOSX.h" + +#include <memory> +#include <mutex> +#include <queue> + +#import <AppKit/AppKit.h> +#import <Carbon/Carbon.h> // kvk_ANSI_ keycodes +#import <CoreGraphics/CoreGraphics.h> +#import <Foundation/Foundation.h> + +#pragma mark - objc implementation + +@implementation CWinEventsOSXImpl +{ + std::queue<XBMC_Event> events; + CCriticalSection m_inputlock; + id mLocalMonitorId; +} + +#pragma mark - init + +- (instancetype)init +{ + self = [super init]; + + [self enableInputEvents]; + + return self; +} + +- (void)MessagePush:(XBMC_Event*)newEvent +{ + std::unique_lock<CCriticalSection> lock(m_inputlock); + events.emplace(*newEvent); +} + +- (bool)MessagePump +{ + + bool ret = false; + + // Do not always loop, only pump the initial queued count events. else if ui keep pushing + // events the loop won't finish then it will block xbmc main message loop. + for (size_t pumpEventCount = [self GetQueueSize]; pumpEventCount > 0; --pumpEventCount) + { + // Pop up only one event per time since in App::OnEvent it may init modal dialog which init + // deeper message loop and call the deeper MessagePump from there. + XBMC_Event pumpEvent = {}; + { + std::unique_lock<CCriticalSection> lock(m_inputlock); + if (events.size() == 0) + return ret; + pumpEvent = events.front(); + events.pop(); + } + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + ret |= appPort->OnEvent(pumpEvent); + } + return ret; +} + +- (size_t)GetQueueSize +{ + std::unique_lock<CCriticalSection> lock(m_inputlock); + return events.size(); +} + +- (unichar)OsxKey2XbmcKey:(unichar)character +{ + switch (character) + { + case kVK_ANSI_8: + case NSLeftArrowFunctionKey: + return XBMCK_LEFT; + case kVK_ANSI_0: + case NSRightArrowFunctionKey: + return XBMCK_RIGHT; + case kVK_ANSI_RightBracket: + case NSUpArrowFunctionKey: + return XBMCK_UP; + case kVK_ANSI_O: + case NSDownArrowFunctionKey: + return XBMCK_DOWN; + case NSDeleteCharacter: + return XBMCK_BACKSPACE; + default: + return character; + } +} + +- (XBMCMod)OsxMod2XbmcMod:(CGEventFlags)appleModifier +{ + unsigned int xbmcModifier = XBMCKMOD_NONE; + // shift left + if (appleModifier & kCGEventFlagMaskAlphaShift) + xbmcModifier |= XBMCKMOD_LSHIFT; + // shift right + if (appleModifier & kCGEventFlagMaskShift) + xbmcModifier |= XBMCKMOD_RSHIFT; + // left ctrl + if (appleModifier & kCGEventFlagMaskControl) + xbmcModifier |= XBMCKMOD_LCTRL; + // left alt/option + if (appleModifier & kCGEventFlagMaskAlternate) + xbmcModifier |= XBMCKMOD_LALT; + // left command + if (appleModifier & kCGEventFlagMaskCommand) + xbmcModifier |= XBMCKMOD_LMETA; + + return static_cast<XBMCMod>(xbmcModifier); +} + +- (bool)ProcessOSXShortcuts:(XBMC_Event&)event +{ + const auto cmd = (event.key.keysym.mod & (XBMCKMOD_LMETA | XBMCKMOD_RMETA)) != 0; + if (cmd && event.type == XBMC_KEYDOWN) + { + switch (event.key.keysym.sym) + { + case XBMCK_q: // CMD-q to quit + if (!g_application.m_bStop) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT); + return true; + + case XBMCK_CTRLF: // CMD-CTRL-f to toggle fullscreen + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + return true; + + case XBMCK_s: // CMD-s to take a screenshot + { + CAction* action = new CAction(ACTION_TAKE_SCREENSHOT); + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(action)); + return true; + } + case XBMCK_h: // CMD-h to hide (but we minimize for now) + case XBMCK_m: // CMD-m to minimize + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE); + return true; + + default: + return false; + } + } + + return false; +} + +- (void)enableInputEvents +{ + [self disableInputEvents]; // allow only one registration at a time + + // clang-format off + // Create an event tap. We are interested in mouse and keyboard events. + NSEventMask eventMask = + NSEventMaskKeyDown | NSEventMaskKeyUp | + NSEventMaskLeftMouseDown | NSEventMaskLeftMouseUp | + NSEventMaskRightMouseDown | NSEventMaskRightMouseUp | + NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp | + NSEventMaskScrollWheel | + NSEventMaskLeftMouseDragged | + NSEventMaskRightMouseDragged | + NSEventMaskOtherMouseDragged | + NSEventMaskMouseMoved; + // clang-format on + + mLocalMonitorId = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask + handler:^(NSEvent* event) { + return [self InputEventHandler:event]; + }]; +} + +- (void)disableInputEvents +{ + // Disable the local Monitor + if (mLocalMonitorId != nil) + [NSEvent removeMonitor:mLocalMonitorId]; + mLocalMonitorId = nil; +} + +- (NSEvent*)InputEventHandler:(NSEvent*)nsevent +{ + bool passEvent = true; + CGEventRef event = nsevent.CGEvent; + CGEventType type = CGEventGetType(event); + + // The incoming mouse position. + NSPoint location = nsevent.locationInWindow; + if (location.x < 0 || location.y < 0) + return nsevent; + + // cocoa world is upside down ... + auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return nsevent; + + NSRect frame = winSystem->GetWindowDimensions(); + location.y = frame.size.height - location.y; + + XBMC_Event newEvent = {}; + + switch (type) + { + // handle mouse events and transform them into the xbmc event world + case kCGEventLeftMouseUp: + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = XBMC_BUTTON_LEFT; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventLeftMouseDown: + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = XBMC_BUTTON_LEFT; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventRightMouseUp: + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = XBMC_BUTTON_RIGHT; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventRightMouseDown: + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = XBMC_BUTTON_RIGHT; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventOtherMouseUp: + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.button = XBMC_BUTTON_MIDDLE; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventOtherMouseDown: + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.button = XBMC_BUTTON_MIDDLE; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventMouseMoved: + case kCGEventLeftMouseDragged: + case kCGEventRightMouseDragged: + case kCGEventOtherMouseDragged: + newEvent.type = XBMC_MOUSEMOTION; + newEvent.motion.x = location.x; + newEvent.motion.y = location.y; + [self MessagePush:&newEvent]; + break; + case kCGEventScrollWheel: + // very strange, real scrolls have non-zero deltaY followed by same number of events + // with a zero deltaY. This reverses our scroll which is WTF? anoying. Trap them out here. + if (nsevent.deltaY != 0.0) + { + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.x = location.x; + newEvent.button.y = location.y; + newEvent.button.button = + CGEventGetIntegerValueField(event, kCGScrollWheelEventDeltaAxis1) > 0 + ? XBMC_BUTTON_WHEELUP + : XBMC_BUTTON_WHEELDOWN; + [self MessagePush:&newEvent]; + + newEvent.type = XBMC_MOUSEBUTTONUP; + [self MessagePush:&newEvent]; + } + break; + + // handle keyboard events and transform them into the xbmc event world + case kCGEventKeyUp: + newEvent = [self keyPressEvent:&event]; + newEvent.type = XBMC_KEYUP; + + [self MessagePush:&newEvent]; + passEvent = false; + break; + case kCGEventKeyDown: + newEvent = [self keyPressEvent:&event]; + newEvent.type = XBMC_KEYDOWN; + + if (![self ProcessOSXShortcuts:newEvent]) + [self MessagePush:&newEvent]; + passEvent = false; + + break; + default: + return nsevent; + } + // We must return the event for it to be useful if not already handled + if (passEvent) + return nsevent; + else + return nullptr; +} + +- (XBMC_Event)keyPressEvent:(CGEventRef*)event +{ + UniCharCount actualStringLength = 0; + // Get stringlength of event + CGEventKeyboardGetUnicodeString(*event, 0, &actualStringLength, nullptr); + + // Create array with size of event string + UniChar unicodeString[actualStringLength]; + memset(unicodeString, 0, sizeof(unicodeString)); + + auto keycode = + static_cast<CGKeyCode>(CGEventGetIntegerValueField(*event, kCGKeyboardEventKeycode)); + CGEventKeyboardGetUnicodeString(*event, sizeof(unicodeString) / sizeof(*unicodeString), + &actualStringLength, unicodeString); + + XBMC_Event newEvent = {}; + + // May be possible for actualStringLength > 1. Havent been able to replicate anything + // larger than 1, but keep in mind for any regressions + if (actualStringLength == 0) + { + return newEvent; + } + else if (actualStringLength > 1) + { + CLog::Log(LOGERROR, "CWinEventsOSXImpl::keyPressEvent - event string > 1 - size: {}", + static_cast<int>(actualStringLength)); + return newEvent; + } + + unicodeString[0] = [self OsxKey2XbmcKey:unicodeString[0]]; + + newEvent.key.keysym.scancode = keycode; + newEvent.key.keysym.sym = static_cast<XBMCKey>(unicodeString[0]); + newEvent.key.keysym.unicode = unicodeString[0]; + newEvent.key.keysym.mod = [self OsxMod2XbmcMod:CGEventGetFlags(*event)]; + + return newEvent; +} + +@end diff --git a/xbmc/windowing/osx/WinSystemOSX.h b/xbmc/windowing/osx/WinSystemOSX.h new file mode 100644 index 0000000..0ac08d4 --- /dev/null +++ b/xbmc/windowing/osx/WinSystemOSX.h @@ -0,0 +1,120 @@ +/* + * 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 + +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "threads/Timer.h" +#include "windowing/WinSystem.h" + +#include <memory> +#include <string> +#include <vector> + +typedef struct _CGLContextObject* CGLContextObj; +typedef struct CGRect NSRect; + +class IDispResource; +class CWinEventsOSX; +#ifdef __OBJC__ +@class NSWindow; +@class OSXGLView; +#else +struct NSWindow; +struct OSXGLView; +#endif + +class CWinSystemOSX : public CWinSystemBase, public ITimerCallback +{ +public: + CWinSystemOSX(); + ~CWinSystemOSX() override; + + // ITimerCallback interface + void OnTimeout() override; + + // CWinSystemBase + 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 UpdateResolutions() override; + void NotifyAppFocusChange(bool bGaining) override; + void ShowOSMouse(bool show) override; + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + void OnMove(int x, int y) override; + + void SetOcclusionState(bool occluded); + + std::string GetClipboardText() override; + + void Register(IDispResource* resource) override; + void Unregister(IDispResource* resource) override; + + std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override; + + void WindowChangedScreen(); + + void AnnounceOnLostDevice(); + void AnnounceOnResetDevice(); + void HandleOnResetDevice(); + void StartLostDeviceTimer(); + void StopLostDeviceTimer(); + + void SetMovedToOtherScreen(bool moved) { m_movedToOtherScreen = moved; } + int CheckDisplayChanging(uint32_t flags); + void SetFullscreenWillToggle(bool toggle) { m_fullscreenWillToggle = toggle; } + bool GetFullscreenWillToggle() { return m_fullscreenWillToggle; } + + CGLContextObj GetCGLContextObj(); + + std::vector<std::string> GetConnectedOutputs() override; + + // winevents override + bool MessagePump() override; + + NSRect GetWindowDimensions(); + void enableInputEvents(); + void disableInputEvents(); + +protected: + std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override; + + void GetScreenResolution(int* w, int* h, double* fps, int screenIdx); + void EnableVSync(bool enable); + bool SwitchToVideoMode(int width, int height, double refreshrate); + void FillInVideoModes(); + bool FlushBuffer(); + bool IsObscured(); + + bool DestroyWindowInternal(); + + std::unique_ptr<CWinEventsOSX> m_winEvents; + + std::string m_name; + bool m_obscured; + NSWindow* m_appWindow; + OSXGLView* m_glView; + bool m_movedToOtherScreen; + int m_lastDisplayNr; + double m_refreshRate; + + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + CTimer m_lostDeviceTimer; + bool m_delayDispReset; + XbmcThreads::EndTime<> m_dispResetTimer; + bool m_fullscreenWillToggle; + CCriticalSection m_critSection; +}; diff --git a/xbmc/windowing/osx/WinSystemOSX.mm b/xbmc/windowing/osx/WinSystemOSX.mm new file mode 100644 index 0000000..85dd4b7 --- /dev/null +++ b/xbmc/windowing/osx/WinSystemOSX.mm @@ -0,0 +1,1305 @@ +/* + * 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 "WinSystemOSX.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "cores/AudioEngine/AESinkFactory.h" +#include "cores/AudioEngine/Sinks/AESinkDARWINOSX.h" +#include "cores/RetroPlayer/process/osx/RPProcessInfoOSX.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h" +#include "cores/VideoPlayer/Process/osx/ProcessInfoOSX.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "guilib/DispResource.h" +#include "guilib/GUIWindowManager.h" +#include "messaging/ApplicationMessenger.h" +#include "rendering/gl/ScreenshotSurfaceGL.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/CriticalSection.h" +#include "utils/StringUtils.h" +#include "utils/log.h" +#include "windowing/osx/CocoaDPMSSupport.h" +#include "windowing/osx/OSScreenSaverOSX.h" +#import "windowing/osx/OpenGL/OSXGLView.h" +#import "windowing/osx/OpenGL/OSXGLWindow.h" +#include "windowing/osx/VideoSyncOsx.h" +#include "windowing/osx/WinEventsOSX.h" + +#include "platform/darwin/DarwinUtils.h" +#include "platform/darwin/DictionaryUtils.h" +#include "platform/darwin/osx/CocoaInterface.h" +#include "platform/darwin/osx/powermanagement/CocoaPowerSyscall.h" + +#include <chrono> +#include <cstdlib> +#include <mutex> +#include <signal.h> + +#import <Cocoa/Cocoa.h> +#import <Foundation/Foundation.h> +#import <IOKit/graphics/IOGraphicsLib.h> +#import <IOKit/pwr_mgt/IOPMLib.h> +#import <QuartzCore/QuartzCore.h> + +using namespace KODI; +using namespace MESSAGING; +using namespace WINDOWING; +using namespace std::chrono_literals; + +#define MAX_DISPLAYS 32 +static NSWindow* blankingWindows[MAX_DISPLAYS]; + +size_t DisplayBitsPerPixelForMode(CGDisplayModeRef mode) +{ + size_t bitsPerPixel = 0; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // No replacement for CGDisplayModeCopyPixelEncoding + // Disable warning for now + CFStringRef pixEnc = CGDisplayModeCopyPixelEncoding(mode); +#pragma GCC diagnostic pop + + if (CFStringCompare(pixEnc, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive) == + kCFCompareEqualTo) + { + bitsPerPixel = 32; + } + else if (CFStringCompare(pixEnc, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive) == + kCFCompareEqualTo) + { + bitsPerPixel = 16; + } + else if (CFStringCompare(pixEnc, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive) == + kCFCompareEqualTo) + { + bitsPerPixel = 8; + } + + CFRelease(pixEnc); + + return bitsPerPixel; +} + +#pragma mark - GetScreenName + +NSString* screenNameForDisplay(CGDirectDisplayID displayID) +{ + NSString* screenName; + @autoreleasepool + { + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // No real replacement of CGDisplayIOServicePort + // Stackoverflow links to https://github.com/glfw/glfw/pull/192 as a possible replacement + // disable warning for now + NSDictionary* deviceInfo = (__bridge_transfer NSDictionary*)IODisplayCreateInfoDictionary( + CGDisplayIOServicePort(displayID), kIODisplayOnlyPreferredName); + +#pragma GCC diagnostic pop + + NSDictionary* localizedNames = + [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]]; + + if ([localizedNames count] > 0) + { + screenName = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]]; + } + } + + if (screenName == nil) + { + screenName = [[NSString alloc] initWithFormat:@"%i", displayID]; + } + else + { + // ensure screen name is unique by appending displayid + screenName = [screenName stringByAppendingFormat:@" (%@)", [@(displayID) stringValue]]; + } + + return screenName; +} + +#pragma mark - GetDisplay + +CGDirectDisplayID GetDisplayID(int screen_index) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + uint32_t numDisplays; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + if (screen_index >= 0 && screen_index < static_cast<int>(numDisplays)) + return (displayArray[screen_index]); + else + return (displayArray[0]); +} + +CGDirectDisplayID GetDisplayIDFromScreen(NSScreen* screen) +{ + NSDictionary* screenInfo = screen.deviceDescription; + NSNumber* screenID = [screenInfo objectForKey:@"NSScreenNumber"]; + + return (CGDirectDisplayID)[screenID longValue]; +} + +int GetDisplayIndex(CGDirectDisplayID display) +{ + CGDirectDisplayID displayArray[MAX_DISPLAYS]; + CGDisplayCount numDisplays; + + // Get the list of displays. + CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays); + while (numDisplays > 0) + { + if (display == displayArray[--numDisplays]) + return numDisplays; + } + return -1; +} + +int GetDisplayIndex(const std::string& dispName) +{ + int ret = 0; + + // Add full screen settings for additional monitors + int numDisplays = NSScreen.screens.count; + + for (int disp = 0; disp < numDisplays; disp++) + { + NSString* name = screenNameForDisplay(GetDisplayID(disp)); + if (name.UTF8String == dispName) + { + ret = disp; + break; + } + } + + return ret; +} + +#pragma mark - Display Modes + +CFArrayRef GetAllDisplayModes(CGDirectDisplayID display) +{ + int value = 1; + + CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value); + if (!number) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Number!"); + return nullptr; + } + + CFStringRef key = kCGDisplayShowDuplicateLowResolutionModes; + CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, (const void**)&key, + (const void**)&number, 1, nullptr, nullptr); + CFRelease(number); + + if (!options) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Dictionary!"); + return nullptr; + } + + CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(display, options); + CFRelease(options); + + if (!displayModes) + { + CLog::Log(LOGERROR, "GetAllDisplayModes - no displaymodes found!"); + return nullptr; + } + + return displayModes; +} + +// try to find mode that matches the desired size, refreshrate +// non interlaced, nonstretched, safe for hardware +CGDisplayModeRef GetMode(int width, int height, double refreshrate, int screenIdx) +{ + if (screenIdx >= (signed)[[NSScreen screens] count]) + return nullptr; + + bool stretched; + bool interlaced; + bool safeForHardware; + int w, h, bitsperpixel; + double rate; + RESOLUTION_INFO res; + + CLog::Log(LOGDEBUG, "GetMode looking for suitable mode with {} x {} @ {} Hz on display {}", width, + height, refreshrate, screenIdx); + + CFArrayRef allModes = GetAllDisplayModes(GetDisplayID(screenIdx)); + + if (!allModes) + return nullptr; + + for (int i = 0; i < CFArrayGetCount(allModes); ++i) + { + CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + uint32_t flags = CGDisplayModeGetIOFlags(displayMode); + stretched = (flags & kDisplayModeStretchedFlag) != 0; + interlaced = (flags & kDisplayModeInterlacedFlag) != 0; + bitsperpixel = DisplayBitsPerPixelForMode(displayMode); + safeForHardware = (flags & kDisplayModeSafetyFlags) != 0; + w = CGDisplayModeGetWidth(displayMode); + h = CGDisplayModeGetHeight(displayMode); + rate = CGDisplayModeGetRefreshRate(displayMode); + + if ((bitsperpixel == 32) && (safeForHardware == true) && (stretched == false) && + (interlaced == false) && (w == width) && (h == height) && + (rate == refreshrate || rate == 0)) + { + CFRelease(allModes); + CLog::Log(LOGDEBUG, "GetMode found a match!"); + return CGDisplayModeRetain(displayMode); + } + } + + CFRelease(allModes); + CLog::Log(LOGERROR, "GetMode - no match found!"); + return nullptr; +} + +// mimic former behavior of deprecated CGDisplayBestModeForParameters +CGDisplayModeRef BestMatchForMode(CGDirectDisplayID display, + size_t bitsPerPixel, + size_t width, + size_t height) +{ + // Loop through all display modes to determine the closest match. + // CGDisplayBestModeForParameters is deprecated on 10.6 so we will emulate it's behavior + // Try to find a mode with the requested depth and equal or greater dimensions first. + // If no match is found, try to find a mode with greater depth and same or greater dimensions. + // If still no match is found, just use the current mode. + CFArrayRef allModes = GetAllDisplayModes(display); + + if (!allModes) + return nullptr; + + CGDisplayModeRef displayMode = nullptr; + + for (int i = 0; i < CFArrayGetCount(allModes); i++) + { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + + if (!mode) + continue; + + if (DisplayBitsPerPixelForMode(mode) != bitsPerPixel) + continue; + + if ((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height)) + { + displayMode = mode; + break; + } + } + + // No depth match was found + if (!displayMode) + { + for (int i = 0; i < CFArrayGetCount(allModes); i++) + { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + + if (!mode) + continue; + + if (DisplayBitsPerPixelForMode(mode) >= bitsPerPixel) + continue; + + if ((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height)) + { + displayMode = mode; + break; + } + } + } + + CFRelease(allModes); + + return displayMode; +} + +#pragma mark - Blank Displays + +void BlankOtherDisplays(int screen_index) +{ + int i; + int numDisplays = [[NSScreen screens] count]; + + // zero out blankingWindows for debugging + for (i = 0; i < MAX_DISPLAYS; i++) + { + blankingWindows[i] = 0; + } + + // Blank. + for (i = 0; i < numDisplays; i++) + { + if (i != screen_index) + { + // Get the size. + NSScreen* pScreen = [NSScreen.screens objectAtIndex:i]; + NSRect screenRect = pScreen.frame; + + // Build a blanking window. + screenRect.origin = NSZeroPoint; + blankingWindows[i] = [[NSWindow alloc] initWithContentRect:screenRect + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:NO + screen:pScreen]; + + [blankingWindows[i] setBackgroundColor:NSColor.blackColor]; + [blankingWindows[i] setLevel:CGShieldingWindowLevel()]; + [blankingWindows[i] makeKeyAndOrderFront:nil]; + } + } +} + +void UnblankDisplays(void) +{ + for (auto i = 0; i < static_cast<int>(NSScreen.screens.count); i++) + { + if (blankingWindows[i] != 0) + { + // Get rid of the blanking windows we created. + [blankingWindows[i] close]; + blankingWindows[i] = 0; + } + } +} + +#pragma mark - Fade Display +//! @Todo Look to replace Fade with CABasicAnimation +static NSWindow* curtainWindow; +void fadeInDisplay(NSScreen* theScreen, double fadeTime) +{ + int fadeSteps = 100; + double fadeInterval = (fadeTime / (double)fadeSteps); + + if (curtainWindow != nil) + { + for (int step = 0; step < fadeSteps; step++) + { + double fade = 1.0 - (step * fadeInterval); + [curtainWindow setAlphaValue:fade]; + + NSDate* nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval]; + [NSRunLoop.currentRunLoop runUntilDate:nextDate]; + } + } + [curtainWindow close]; + curtainWindow = nil; +} + +void fadeOutDisplay(NSScreen* theScreen, double fadeTime) +{ + int fadeSteps = 100; + double fadeInterval = (fadeTime / (double)fadeSteps); + + [NSCursor hide]; + + curtainWindow = [[NSWindow alloc] initWithContentRect:[theScreen frame] + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:YES + screen:theScreen]; + + [curtainWindow setAlphaValue:0.0]; + [curtainWindow setBackgroundColor:NSColor.blackColor]; + [curtainWindow setLevel:NSScreenSaverWindowLevel]; + + [curtainWindow makeKeyAndOrderFront:nil]; + [curtainWindow setFrame:[curtainWindow frameRectForContentRect:[theScreen frame]] + display:YES + animate:NO]; + + for (int step = 0; step < fadeSteps; step++) + { + double fade = step * fadeInterval; + [curtainWindow setAlphaValue:fade]; + + NSDate* nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval]; + [NSRunLoop.currentRunLoop runUntilDate:nextDate]; + } +} + +//--------------------------------------------------------------------------------- +static void DisplayReconfigured(CGDirectDisplayID display, + CGDisplayChangeSummaryFlags flags, + void* userData) +{ + CWinSystemOSX* winsys = (CWinSystemOSX*)userData; + if (!winsys) + return; + + CLog::Log(LOGDEBUG, "CWinSystemOSX::DisplayReconfigured with flags {}", flags); + + // we fire the callbacks on start of configuration + // or when the mode set was finished + // or when we are called with flags == 0 (which is undocumented but seems to happen + // on some macs - we treat it as device reset) + + // first check if we need to call OnLostDevice + if (flags & kCGDisplayBeginConfigurationFlag) + { + // pre/post-reconfiguration changes + RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); + if (res == RES_INVALID) + return; + + NSScreen* pScreen = nil; + unsigned int screenIdx = 0; + + if (screenIdx < NSScreen.screens.count) + { + pScreen = [NSScreen.screens objectAtIndex:screenIdx]; + } + + // kCGDisplayBeginConfigurationFlag is only fired while the screen is still + // valid + if (pScreen) + { + CGDirectDisplayID xbmc_display = GetDisplayIDFromScreen(pScreen); + if (xbmc_display == display) + { + // we only respond to changes on the display we are running on. + winsys->AnnounceOnLostDevice(); + winsys->StartLostDeviceTimer(); + } + } + } + else // the else case checks if we need to call OnResetDevice + { + // we fire if kCGDisplaySetModeFlag is set or if flags == 0 + // (which is undocumented but seems to happen + // on some macs - we treat it as device reset) + // we also don't check the screen here as we might not even have + // one anymore (e.x. when tv is turned off) + if (flags & kCGDisplaySetModeFlag || flags == 0) + { + winsys->StopLostDeviceTimer(); // no need to timeout - we've got the callback + winsys->HandleOnResetDevice(); + } + } +} + +#pragma mark - CWinSystemOSX +//------------------------------------------------------------------------------ +CWinSystemOSX::CWinSystemOSX() : CWinSystemBase(), m_lostDeviceTimer(this) +{ + m_appWindow = nullptr; + m_glView = nullptr; + m_obscured = false; + m_lastDisplayNr = -1; + m_movedToOtherScreen = false; + m_refreshRate = 0.0; + m_delayDispReset = false; + + m_winEvents.reset(new CWinEventsOSX()); + + AE::CAESinkFactory::ClearSinks(); + CAESinkDARWINOSX::Register(); + CCocoaPowerSyscall::Register(); + m_dpms = std::make_shared<CCocoaDPMSSupport>(); +} + +CWinSystemOSX::~CWinSystemOSX() = default; + +void CWinSystemOSX::Register(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemOSX::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +void CWinSystemOSX::AnnounceOnLostDevice() +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::LogF(LOGDEBUG, "Lost Device Announce"); + for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnLostDisplay(); +} + +void CWinSystemOSX::HandleOnResetDevice() +{ + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + if (delay > 0ms) + { + m_delayDispReset = true; + m_dispResetTimer.Set(delay); + } + else + { + AnnounceOnResetDevice(); + } +} + +void CWinSystemOSX::AnnounceOnResetDevice() +{ + double currentFps = m_refreshRate; + int w = 0; + int h = 0; + int currentScreenIdx = m_lastDisplayNr; + // ensure that graphics context knows about the current refreshrate before + // doing the callbacks + GetScreenResolution(&w, &h, ¤tFps, currentScreenIdx); + + CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(currentFps); + + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::LogF(LOGDEBUG, "Reset Device Announce"); + for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); +} + +#pragma mark - Timers + +void CWinSystemOSX::StartLostDeviceTimer() +{ + if (m_lostDeviceTimer.IsRunning()) + m_lostDeviceTimer.Restart(); + else + m_lostDeviceTimer.Start(3000ms, false); +} + +void CWinSystemOSX::StopLostDeviceTimer() +{ + m_lostDeviceTimer.Stop(); +} + +void CWinSystemOSX::OnTimeout() +{ + HandleOnResetDevice(); +} + +#pragma mark - WindowSystem + +bool CWinSystemOSX::InitWindowSystem() +{ + if (!CWinSystemBase::InitWindowSystem()) + return false; + + CGDisplayRegisterReconfigurationCallback(DisplayReconfigured, (void*)this); + + return true; +} + +bool CWinSystemOSX::DestroyWindowSystem() +{ + CGDisplayRemoveReconfigurationCallback(DisplayReconfigured, (void*)this); + + DestroyWindowInternal(); + + if (m_glView) + { + m_glView = nullptr; + } + + UnblankDisplays(); + return true; +} + +bool CWinSystemOSX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + // force initial window creation to be windowed, if fullscreen, it will switch to it below + // fixes the white screen of death if starting fullscreen and switching to windowed. + RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW); + m_nWidth = resInfo.iWidth; + m_nHeight = resInfo.iHeight; + m_bFullScreen = false; + m_name = name; + + __block NSWindow* appWindow; + // because we are not main thread, delay any updates + // and only become keyWindow after it finishes. + [NSAnimationContext beginGrouping]; + [NSAnimationContext.currentContext setCompletionHandler:^{ + [appWindow makeKeyWindow]; + }]; + + const NSUInteger windowStyleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | + NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + + if (m_appWindow == nullptr) + { + // create new content view + NSRect rect = [appWindow contentRectForFrameRect:appWindow.frame]; + + // create new view if we don't have one + if (!m_glView) + m_glView = [[OSXGLView alloc] initWithFrame:rect]; + + OSXGLView* view = (OSXGLView*)m_glView; + + dispatch_sync(dispatch_get_main_queue(), ^{ + appWindow = [[OSXGLWindow alloc] initWithContentRect:NSMakeRect(0, 0, m_nWidth, m_nHeight) + styleMask:windowStyleMask]; + NSString* title = [NSString stringWithUTF8String:m_name.c_str()]; + appWindow.backgroundColor = NSColor.blackColor; + appWindow.title = title; + + NSWindowCollectionBehavior behavior = appWindow.collectionBehavior; + behavior |= NSWindowCollectionBehaviorFullScreenPrimary; + [appWindow setCollectionBehavior:behavior]; + + // associate with current window + [appWindow setContentView:view]; + }); + + [view.getGLContext makeCurrentContext]; + [view.getGLContext update]; + + m_appWindow = appWindow; + m_bWindowCreated = true; + } + + // warning, we can order front but not become + // key window or risk starting up with bad flicker + // becoming key window must happen in completion block. + [(NSWindow*)m_appWindow performSelectorOnMainThread:@selector(orderFront:) + withObject:nil + waitUntilDone:YES]; + + [NSAnimationContext endGrouping]; + + // get screen refreshrate - this is needed + // when we startup in windowed mode and don't run through SetFullScreen + int dummy; + GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr); + + // register platform dependent objects + CDVDFactoryCodec::ClearHWAccels(); + VTB::CDecoder::Register(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CLinuxRendererGL::Register(); + CRendererVTB::Register(); + VIDEOPLAYER::CProcessInfoOSX::Register(); + RETRO::CRPProcessInfoOSX::Register(); + RETRO::CRPProcessInfoOSX::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL); + CScreenshotSurfaceGL::Register(); + + return true; +} + +bool CWinSystemOSX::DestroyWindowInternal() +{ + // set this 1st, we should really mutex protext m_appWindow in this class + m_bWindowCreated = false; + if (m_appWindow) + { + NSWindow* oldAppWindow = m_appWindow; + m_appWindow = nullptr; + dispatch_sync(dispatch_get_main_queue(), ^{ + [oldAppWindow setContentView:nil]; + }); + } + + return true; +} + +bool CWinSystemOSX::DestroyWindow() +{ + return true; +} + +bool CWinSystemOSX::Minimize() +{ + @autoreleasepool + { + dispatch_sync(dispatch_get_main_queue(), ^{ + [NSApplication.sharedApplication miniaturizeAll:nil]; + }); + } + return true; +} + +bool CWinSystemOSX::Restore() +{ + @autoreleasepool + { + dispatch_sync(dispatch_get_main_queue(), ^{ + [NSApplication.sharedApplication unhide:nil]; + }); + } + return true; +} + +bool CWinSystemOSX::Show(bool raise) +{ + @autoreleasepool + { + auto app = NSApplication.sharedApplication; + if (raise) + { + [app unhide:nil]; + [app activateIgnoringOtherApps:YES]; + [app arrangeInFront:nil]; + } + else + { + [app unhideWithoutActivation]; + } + } + return true; +} + +bool CWinSystemOSX::Hide() +{ + @autoreleasepool + { + dispatch_sync(dispatch_get_main_queue(), ^{ + [NSApplication.sharedApplication hide:nil]; + }); + } + return true; +} + +NSRect CWinSystemOSX::GetWindowDimensions() +{ + if (m_appWindow) + { + NSWindow* win = (NSWindow*)m_appWindow; + NSRect frame = win.contentView.frame; + return frame; + } +} + +#pragma mark - Resize Window + +bool CWinSystemOSX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + if (!m_appWindow) + return false; + + [(OSXGLWindow*)m_appWindow setResizeState:true]; + + __block OSXGLView* view; + dispatch_sync(dispatch_get_main_queue(), ^{ + view = m_appWindow.contentView; + }); + + if (view) + { + // It seems, that in macOS 10.15 this defaults to YES, but we currently do not support + // Retina resolutions properly. Ensure that the view uses a 1 pixel per point framebuffer. + dispatch_sync(dispatch_get_main_queue(), ^{ + view.wantsBestResolutionOpenGLSurface = NO; + }); + } + + if (newWidth < 0) + { + newWidth = [(NSWindow*)m_appWindow minSize].width; + } + + if (newHeight < 0) + { + newHeight = [(NSWindow*)m_appWindow minSize].height; + } + + if (view) + { + dispatch_sync(dispatch_get_main_queue(), ^{ + NSOpenGLContext* context = [view getGLContext]; + NSWindow* window = m_appWindow; + + NSRect pos = window.frame; + + NSRect myNewContentFrame = NSMakeRect(pos.origin.x, pos.origin.y, newWidth, newHeight); + NSRect myNewWindowRect = [window frameRectForContentRect:myNewContentFrame]; + [window setFrame:myNewWindowRect display:TRUE]; + + [context update]; + }); + } + + m_nWidth = newWidth; + m_nHeight = newHeight; + + [(OSXGLWindow*)m_appWindow setResizeState:false]; + + return true; +} + +bool CWinSystemOSX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // if (m_lastDisplayNr == -1) + // m_lastDisplayNr = res.iScreen; + + __block NSWindow* window = m_appWindow; + + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + m_lastDisplayNr = GetDisplayIndex(settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + + //handle resolution/refreshrate switching early here + if (m_bFullScreen) + { + // switch videomode + SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate)); + // hide the OS mouse + [NSCursor hide]; + } + + dispatch_sync(dispatch_get_main_queue(), ^{ + [window setAllowsConcurrentViewDrawing:NO]; + }); + + if (m_fullscreenWillToggle) + { + ResizeWindow(m_nWidth, m_nHeight, -1, -1); + m_fullscreenWillToggle = false; + return true; + } + + if (m_bFullScreen) + { + // This is Cocoa Windowed FullScreen Mode + // Get the screen rect of our current display + NSScreen* pScreen = [NSScreen.screens objectAtIndex:m_lastDisplayNr]; + + // remove frame origin offset of original display + pScreen.frame.origin = NSZeroPoint; + + dispatch_sync(dispatch_get_main_queue(), ^{ + [window.contentView setFrameSize:NSMakeSize(m_nWidth, m_nHeight)]; + window.title = @""; + [window setAllowsConcurrentViewDrawing:YES]; + }); + + // Blank other displays if requested. + if (blankOtherDisplays) + BlankOtherDisplays(m_lastDisplayNr); + } + else + { + // Show menubar. + dispatch_sync(dispatch_get_main_queue(), ^{ + [NSApplication.sharedApplication setPresentationOptions:NSApplicationPresentationDefault]; + }); + + // Unblank. + // Force the unblank when returning from fullscreen, we get called with blankOtherDisplays set false. + //if (blankOtherDisplays) + UnblankDisplays(); + } + + //DisplayFadeFromBlack(fade_token, needtoshowme); + + m_fullscreenWillToggle = true; + // toggle cocoa fullscreen mode + if ([m_appWindow respondsToSelector:@selector(toggleFullScreen:)]) + [m_appWindow performSelectorOnMainThread:@selector(toggleFullScreen:) + withObject:nil + waitUntilDone:YES]; + + ResizeWindow(m_nWidth, m_nHeight, -1, -1); + + return true; +} + +#pragma mark - Resolution + +void CWinSystemOSX::UpdateResolutions() +{ + CWinSystemBase::UpdateResolutions(); + + // Add desktop resolution + int w; + int h; + double fps; + + int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOSCREEN_MONITOR)); + GetScreenResolution(&w, &h, &fps, dispIdx); + NSString* dispName = screenNameForDisplay(GetDisplayID(dispIdx)); + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), + dispName.UTF8String, w, h, fps, 0); + + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + // now just fill in the possible resolutions for the attached screens + // and push to the resolution info vector + FillInVideoModes(); + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +void CWinSystemOSX::GetScreenResolution(int* w, int* h, double* fps, int screenIdx) +{ + CGDirectDisplayID display_id = (CGDirectDisplayID)GetDisplayID(screenIdx); + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display_id); + *w = CGDisplayModeGetWidth(mode); + *h = CGDisplayModeGetHeight(mode); + *fps = CGDisplayModeGetRefreshRate(mode); + CGDisplayModeRelease(mode); + if (static_cast<int>(*fps) == 0) + { + // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays. + *fps = 60.0; + } +} + +#pragma mark - Video Modes + +bool CWinSystemOSX::SwitchToVideoMode(int width, int height, double refreshrate) +{ + CGDisplayModeRef dispMode = nullptr; + + int screenIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOSCREEN_MONITOR)); + + // Figure out the screen size. (default to main screen) + CGDirectDisplayID display_id = GetDisplayID(screenIdx); + + // find mode that matches the desired size, refreshrate + // non interlaced, nonstretched, safe for hardware + dispMode = GetMode(width, height, refreshrate, screenIdx); + + //not found - fallback to bestemdeforparameters + if (!dispMode) + { + dispMode = BestMatchForMode(display_id, 32, width, height); + + if (!dispMode) + { + dispMode = BestMatchForMode(display_id, 16, width, height); + + // still no match? fallback to current resolution of the display which HAS to work [tm] + if (!dispMode) + { + int currentWidth; + int currentHeight; + double currentRefresh; + + GetScreenResolution(¤tWidth, ¤tHeight, ¤tRefresh, screenIdx); + dispMode = GetMode(currentWidth, currentHeight, currentRefresh, screenIdx); + + // no way to get a resolution set + if (!dispMode) + return false; + } + } + } + // switch mode and return success + CGDisplayCapture(display_id); + CGDisplayConfigRef cfg; + CGBeginDisplayConfiguration(&cfg); + CGConfigureDisplayWithDisplayMode(cfg, display_id, dispMode, nullptr); + CGError err = CGCompleteDisplayConfiguration(cfg, kCGConfigureForAppOnly); + CGDisplayRelease(display_id); + + m_refreshRate = CGDisplayModeGetRefreshRate(dispMode); + + Cocoa_CVDisplayLinkUpdate(); + + return (err == kCGErrorSuccess); +} + +void CWinSystemOSX::FillInVideoModes() +{ + int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString( + CSettings::SETTING_VIDEOSCREEN_MONITOR)); + + for (int disp = 0; disp < static_cast<int>(NSScreen.screens.count); disp++) + { + bool stretched; + bool interlaced; + bool safeForHardware; + int w, h, bitsperpixel; + double refreshrate; + RESOLUTION_INFO res; + + CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(disp)); + NSString* dispName = screenNameForDisplay(GetDisplayID(disp)); + + CLog::LogF(LOGINFO, "Display {} has name {}", disp, [dispName UTF8String]); + + if (!displayModes) + continue; + + for (int i = 0; i < CFArrayGetCount(displayModes); ++i) + { + CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i); + + uint32_t flags = CGDisplayModeGetIOFlags(displayMode); + stretched = (flags & kDisplayModeStretchedFlag) != 0; + interlaced = (flags & kDisplayModeInterlacedFlag) != 0; + bitsperpixel = DisplayBitsPerPixelForMode(displayMode); + safeForHardware = (flags & kDisplayModeSafetyFlags) != 0; + + if ((bitsperpixel == 32) && (safeForHardware == true) && (stretched == false) && + (interlaced == false)) + { + w = CGDisplayModeGetWidth(displayMode); + h = CGDisplayModeGetHeight(displayMode); + refreshrate = CGDisplayModeGetRefreshRate(displayMode); + if (static_cast<int>(refreshrate) == 0) // LCD display? + { + // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays. + refreshrate = 60.0; + } + CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {} @ {} Hz", disp, w, + h, refreshrate); + + // only add the resolution if it belongs to "our" screen + // all others are only logged above... + if (disp == dispIdx) + { + UpdateDesktopResolution(res, (dispName != nil) ? [dispName UTF8String] : "Unknown", w, h, + refreshrate, 0); + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + } + } + } + CFRelease(displayModes); + } +} + +#pragma mark - Occlusion + +bool CWinSystemOSX::IsObscured() +{ + if (m_obscured) + CLog::LogF(LOGDEBUG, "Obscured"); + return m_obscured; +} + +void CWinSystemOSX::SetOcclusionState(bool occluded) +{ + // m_obscured = occluded; + // CLog::LogF(LOGDEBUG, "{}", occluded ? "true":"false"); +} + +void CWinSystemOSX::NotifyAppFocusChange(bool bGaining) +{ + if (!(m_bFullScreen && bGaining)) + return; + @autoreleasepool + { + // find the window + NSOpenGLContext* context = NSOpenGLContext.currentContext; + if (context) + { + NSView* view; + + view = context.view; + if (view) + { + NSWindow* window; + window = view.window; + if (window) + { + [window orderFront:nil]; + } + } + } + } +} + +#pragma mark - Window Move + +void CWinSystemOSX::OnMove(int x, int y) +{ + static double oldRefreshRate = m_refreshRate; + Cocoa_CVDisplayLinkUpdate(); + + int dummy = 0; + GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr); + + if (oldRefreshRate != m_refreshRate) + { + oldRefreshRate = m_refreshRate; + + // send a message so that videoresolution (and refreshrate) is changed + NSWindow* win = m_appWindow; + NSRect frame = win.contentView.frame; + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_VIDEORESIZE, frame.size.width, + frame.size.height); + } +} + +void CWinSystemOSX::WindowChangedScreen() +{ + // user has moved the window to a + // different screen + NSOpenGLContext* context = [NSOpenGLContext currentContext]; + m_lastDisplayNr = -1; + + // if we are here the user dragged the window to a different + // screen and we return the screen of the window + if (context) + { + NSView* view; + + view = context.view; + if (view) + { + NSWindow* window; + window = view.window; + if (window) + { + m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen(window.screen)); + } + } + } + if (m_lastDisplayNr == -1) + m_lastDisplayNr = 0; // default to main screen +} + +CGLContextObj CWinSystemOSX::GetCGLContextObj() +{ + __block CGLContextObj cglcontex = nullptr; + if (m_appWindow) + { + dispatch_sync(dispatch_get_main_queue(), ^{ + OSXGLView* contentView = m_appWindow.contentView; + cglcontex = contentView.getGLContext.CGLContextObj; + }); + } + + return cglcontex; +} + +bool CWinSystemOSX::FlushBuffer() +{ + if (m_appWindow) + { + dispatch_sync(dispatch_get_main_queue(), ^{ + OSXGLView* contentView = m_appWindow.contentView; + NSOpenGLContext* glcontex = contentView.getGLContext; + [glcontex flushBuffer]; + }); + } + + return true; +} + +#pragma mark - Vsync + +void CWinSystemOSX::EnableVSync(bool enable) +{ + // OpenGL Flush synchronised with vertical retrace + GLint swapInterval = enable ? 1 : 0; + [NSOpenGLContext.currentContext setValues:&swapInterval + forParameter:NSOpenGLContextParameterSwapInterval]; +} + +std::unique_ptr<CVideoSync> CWinSystemOSX::GetVideoSync(void* clock) +{ + return std::make_unique<CVideoSyncOsx>(clock); +} + +std::vector<std::string> CWinSystemOSX::GetConnectedOutputs() +{ + std::vector<std::string> outputs; + outputs.push_back("Default"); + + int numDisplays = [[NSScreen screens] count]; + + for (int disp = 0; disp < numDisplays; disp++) + { + NSString* dispName = screenNameForDisplay(GetDisplayID(disp)); + outputs.push_back(dispName.UTF8String); + } + + return outputs; +} + +#pragma mark - OSScreenSaver + +std::unique_ptr<IOSScreenSaver> CWinSystemOSX::GetOSScreenSaverImpl() +{ + return std::make_unique<COSScreenSaverOSX>(); +} + +#pragma mark - Input + +bool CWinSystemOSX::MessagePump() +{ + return m_winEvents->MessagePump(); +} + +void CWinSystemOSX::enableInputEvents() +{ + m_winEvents->enableInputEvents(); +} + +void CWinSystemOSX::disableInputEvents() +{ + m_winEvents->disableInputEvents(); +} + +std::string CWinSystemOSX::GetClipboardText() +{ + std::string utf8_text; + + const char* szStr = Cocoa_Paste(); + if (szStr) + utf8_text = szStr; + + return utf8_text; +} + +void CWinSystemOSX::ShowOSMouse(bool show) +{ +} + +#pragma mark - Unused + +CGDisplayFadeReservationToken DisplayFadeToBlack(bool fade) +{ + // Fade to black to hide resolution-switching flicker and garbage. + CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken; + if (CGAcquireDisplayFadeReservation(5, &fade_token) == kCGErrorSuccess && fade) + CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, + TRUE); + + return (fade_token); +} + +void DisplayFadeFromBlack(CGDisplayFadeReservationToken fade_token, bool fade) +{ + if (fade_token != kCGDisplayFadeReservationInvalidToken) + { + if (fade) + CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, + 0.0, FALSE); + CGReleaseDisplayFadeReservation(fade_token); + } +} diff --git a/xbmc/windowing/tvos/CMakeLists.txt b/xbmc/windowing/tvos/CMakeLists.txt new file mode 100644 index 0000000..d9aa9cc --- /dev/null +++ b/xbmc/windowing/tvos/CMakeLists.txt @@ -0,0 +1,10 @@ +set(SOURCES OSScreenSaverTVOS.mm + WinEventsTVOS.mm + WinSystemTVOS.mm + VideoSyncTVos.cpp) +set(HEADERS OSScreenSaverTVOS.h + WinEventsTVOS.h + WinSystemTVOS.h + VideoSyncTVos.h) + +core_add_library(windowing_tvos) diff --git a/xbmc/windowing/tvos/OSScreenSaverTVOS.h b/xbmc/windowing/tvos/OSScreenSaverTVOS.h new file mode 100644 index 0000000..2b5d3bd --- /dev/null +++ b/xbmc/windowing/tvos/OSScreenSaverTVOS.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2017-2019 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 "windowing/OSScreenSaver.h" + +class COSScreenSaverTVOS : public KODI::WINDOWING::IOSScreenSaver +{ +public: + COSScreenSaverTVOS() = default; + void Inhibit() override; + void Uninhibit() override; +}; diff --git a/xbmc/windowing/tvos/OSScreenSaverTVOS.mm b/xbmc/windowing/tvos/OSScreenSaverTVOS.mm new file mode 100644 index 0000000..7af5580 --- /dev/null +++ b/xbmc/windowing/tvos/OSScreenSaverTVOS.mm @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017-2019 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. + */ + +#import "OSScreenSaverTVOS.h" + +#import "platform/darwin/tvos/XBMCController.h" + +void COSScreenSaverTVOS::Inhibit() +{ + [g_xbmcController disableScreenSaver]; +} + +void COSScreenSaverTVOS::Uninhibit() +{ + [g_xbmcController enableScreenSaver]; +} diff --git a/xbmc/windowing/tvos/VideoSyncTVos.cpp b/xbmc/windowing/tvos/VideoSyncTVos.cpp new file mode 100644 index 0000000..c00aa60 --- /dev/null +++ b/xbmc/windowing/tvos/VideoSyncTVos.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2015-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 "VideoSyncTVos.h" + +#include "cores/VideoPlayer/VideoReferenceClock.h" +#include "utils/MathUtils.h" +#include "utils/TimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#import "windowing/tvos/WinSystemTVOS.h" + +bool CVideoSyncTVos::Setup(PUPDATECLOCK func) +{ + CLog::Log(LOGDEBUG, "CVideoSyncTVos::{} setting up TVOS", __FUNCTION__); + + //init the vblank timestamp + m_LastVBlankTime = CurrentHostCounter(); + UpdateClock = func; + m_abortEvent.Reset(); + + bool setupOk = InitDisplayLink(); + if (setupOk) + { + m_winSystem.Register(this); + } + + return setupOk; +} + +void CVideoSyncTVos::Run(CEvent& stopEvent) +{ + //because cocoa has a vblank callback, we just keep sleeping until we're asked to stop the thread + XbmcThreads::CEventGroup waitGroup{&stopEvent, &m_abortEvent}; + waitGroup.wait(); +} + +void CVideoSyncTVos::Cleanup() +{ + CLog::Log(LOGDEBUG, "CVideoSyncTVos::{} cleaning up TVOS", __FUNCTION__); + DeinitDisplayLink(); + m_winSystem.Unregister(this); +} + +float CVideoSyncTVos::GetFps() +{ + m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS(); + CLog::Log(LOGDEBUG, "CVideoSyncTVos::{} Detected refreshrate: {} hertz", __FUNCTION__, m_fps); + return m_fps; +} + +void CVideoSyncTVos::OnResetDisplay() +{ + m_abortEvent.Set(); +} + +void CVideoSyncTVos::TVosVblankHandler() +{ + int64_t nowtime = CurrentHostCounter(); + + //calculate how many vblanks happened + double VBlankTime = + static_cast<double>(nowtime - m_LastVBlankTime) / static_cast<double>(CurrentHostFrequency()); + int NrVBlanks = MathUtils::round_int(VBlankTime * static_cast<double>(m_fps)); + + //save the timestamp of this vblank so we can calculate how many happened next time + m_LastVBlankTime = nowtime; + + //update the vblank timestamp, update the clock and send a signal that we got a vblank + UpdateClock(NrVBlanks, nowtime, m_refClock); +} + +bool CVideoSyncTVos::InitDisplayLink() +{ + bool ret = true; + CLog::Log(LOGDEBUG, "CVideoSyncTVos: setting up displaylink"); + if (!m_winSystem.InitDisplayLink(this)) + { + CLog::Log(LOGDEBUG, "CVideoSyncTVos: InitDisplayLink failed"); + ret = false; + } + return ret; +} + +void CVideoSyncTVos::DeinitDisplayLink() +{ + m_winSystem.DeinitDisplayLink(); +} diff --git a/xbmc/windowing/tvos/VideoSyncTVos.h b/xbmc/windowing/tvos/VideoSyncTVos.h new file mode 100644 index 0000000..de78226 --- /dev/null +++ b/xbmc/windowing/tvos/VideoSyncTVos.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015-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 "guilib/DispResource.h" +#include "windowing/VideoSync.h" + +class CWinSystemTVOS; + +class CVideoSyncTVos : public CVideoSync, IDispResource +{ +public: + CVideoSyncTVos(void* clock, CWinSystemTVOS& winSystem) : CVideoSync(clock), m_winSystem(winSystem) + { + } + + // CVideoSync interface + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stopEvent) override; + void Cleanup() override; + float GetFps() override; + + // IDispResource interface + void OnResetDisplay() override; + + // used in the displaylink callback + void TVosVblankHandler(); + +private: + // CVideoSyncDarwin interface + virtual bool InitDisplayLink(); + virtual void DeinitDisplayLink(); + + //timestamp of the last vblank, used for calculating how many vblanks happened + int64_t m_LastVBlankTime = 0; + CEvent m_abortEvent; + CWinSystemTVOS& m_winSystem; +}; diff --git a/xbmc/windowing/tvos/WinEventsTVOS.h b/xbmc/windowing/tvos/WinEventsTVOS.h new file mode 100644 index 0000000..fa30665 --- /dev/null +++ b/xbmc/windowing/tvos/WinEventsTVOS.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012-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 "threads/Event.h" +#include "threads/Thread.h" +#include "windowing/WinEvents.h" + +#include <list> +#include <queue> +#include <string> +#include <vector> + +class CWinEventsTVOS : public IWinEvents, public CThread +{ +public: + CWinEventsTVOS(); + ~CWinEventsTVOS(); + + void MessagePush(XBMC_Event* newEvent); + size_t GetQueueSize(); + + bool MessagePump(); + +private: + CCriticalSection m_eventsCond; + std::list<XBMC_Event> m_events; +}; diff --git a/xbmc/windowing/tvos/WinEventsTVOS.mm b/xbmc/windowing/tvos/WinEventsTVOS.mm new file mode 100644 index 0000000..67e9e54 --- /dev/null +++ b/xbmc/windowing/tvos/WinEventsTVOS.mm @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012-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 "WinEventsTVOS.h" + +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "guilib/GUIWindowManager.h" +#include "input/InputManager.h" +#include "input/XBMC_vkeys.h" +#include "threads/CriticalSection.h" +#include "utils/log.h" + +#include <list> +#include <mutex> + +static CCriticalSection g_inputCond; + +static std::list<XBMC_Event> events; + +CWinEventsTVOS::CWinEventsTVOS() : CThread("CWinEventsTVOS") +{ + CLog::Log(LOGDEBUG, "CWinEventsTVOS::CWinEventsTVOS"); + Create(); +} + +CWinEventsTVOS::~CWinEventsTVOS() +{ + m_bStop = true; + StopThread(true); +} + +void CWinEventsTVOS::MessagePush(XBMC_Event* newEvent) +{ + std::unique_lock<CCriticalSection> lock(m_eventsCond); + + m_events.push_back(*newEvent); +} + +size_t CWinEventsTVOS::GetQueueSize() +{ + std::unique_lock<CCriticalSection> lock(g_inputCond); + return events.size(); +} + + +bool CWinEventsTVOS::MessagePump() +{ + bool ret = false; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + + // Do not always loop, only pump the initial queued count events. else if ui keep pushing + // events the loop won't finish then it will block xbmc main message loop. + for (size_t pumpEventCount = GetQueueSize(); pumpEventCount > 0; --pumpEventCount) + { + // Pop up only one event per time since in App::OnEvent it may init modal dialog which init + // deeper message loop and call the deeper MessagePump from there. + XBMC_Event pumpEvent; + { + std::unique_lock<CCriticalSection> lock(g_inputCond); + if (events.empty()) + return ret; + pumpEvent = events.front(); + events.pop_front(); + } + + if (appPort) + ret = appPort->OnEvent(pumpEvent); + } + return ret; +} diff --git a/xbmc/windowing/tvos/WinSystemTVOS.h b/xbmc/windowing/tvos/WinSystemTVOS.h new file mode 100644 index 0000000..774b7e9 --- /dev/null +++ b/xbmc/windowing/tvos/WinSystemTVOS.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010-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 "rendering/gles/RenderSystemGLES.h" +#include "threads/CriticalSection.h" +#include "threads/Timer.h" +#include "windowing/OSScreenSaver.h" +#include "windowing/WinSystem.h" + +#include <memory> +#include <string> +#include <vector> + +#include <CoreVideo/CVOpenGLESTextureCache.h> + +class IDispResource; +class CVideoSyncTVos; +struct CADisplayLinkWrapper; + +class CWinSystemTVOS : public CWinSystemBase, public CRenderSystemGLES, public ITimerCallback +{ +public: + CWinSystemTVOS(); + virtual ~CWinSystemTVOS(); + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // ITimerCallback interface + virtual void OnTimeout() override {} + + void MessagePush(XBMC_Event* newEvent); + size_t GetQueueSize(); + void AnnounceOnLostDevice(); + void AnnounceOnResetDevice(); + void StartLostDeviceTimer(); + void StopLostDeviceTimer(); + int GetDisplayIndexFromSettings(); + // Implementation of CWinSystemBase + CRenderSystemBase* GetRenderSystem() override { return this; } + 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 UpdateResolutions() override; + bool CanDoWindowed() override { return false; } + + void ShowOSMouse(bool show) override {} + bool HasCursor() override; + + void NotifyAppActiveChange(bool bActivated) override; + + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + + bool IsExtSupported(const char* extension) const override; + + bool BeginRender() override; + bool EndRender() override; + + void Register(IDispResource* resource) override; + void Unregister(IDispResource* resource) override; + + //virtual std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override; + + std::vector<std::string> GetConnectedOutputs() override; + + bool InitDisplayLink(CVideoSyncTVos* syncImpl); + void DeinitDisplayLink(void); + void OnAppFocusChange(bool focus); + bool IsBackgrounded() const { return m_bIsBackgrounded; } + CVEAGLContext GetEAGLContextObj(); + + // winevents override + bool MessagePump() override; + +protected: + virtual std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override; + void PresentRenderImpl(bool rendered) override; + void SetVSyncImpl(bool enable) override {} + + void* m_glView; // EAGLView opaque + void* m_WorkingContext; // shared EAGLContext opaque + bool m_bWasFullScreenBeforeMinimize; + std::string m_eglext; + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + bool m_bIsBackgrounded; + CTimer m_lostDeviceTimer; + +private: + bool GetScreenResolution(int* w, int* h, double* fps, int screenIdx); + void FillInVideoModes(int screenIdx); + bool SwitchToVideoMode(int width, int height, double refreshrate); + CADisplayLinkWrapper* m_pDisplayLink; +}; diff --git a/xbmc/windowing/tvos/WinSystemTVOS.mm b/xbmc/windowing/tvos/WinSystemTVOS.mm new file mode 100644 index 0000000..bd47c6f --- /dev/null +++ b/xbmc/windowing/tvos/WinSystemTVOS.mm @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2010-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 "WinSystemTVOS.h" + +#include "ServiceBroker.h" +#import "cores/AudioEngine/Sinks/AESinkDARWINTVOS.h" +#include "cores/RetroPlayer/process/ios/RPProcessInfoIOS.h" +#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h" +#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h" +#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h" +#include "cores/VideoPlayer/Process/ios/ProcessInfoIOS.h" +#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.h" +#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h" +#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h" +#include "filesystem/SpecialProtocol.h" +#include "guilib/DispResource.h" +#include "guilib/Texture.h" +#include "messaging/ApplicationMessenger.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/OSScreenSaver.h" +#include "windowing/WindowSystemFactory.h" +#import "windowing/tvos/OSScreenSaverTVOS.h" +#import "windowing/tvos/VideoSyncTVos.h" +#import "windowing/tvos/WinEventsTVOS.h" + +#import "platform/darwin/DarwinUtils.h" +#import "platform/darwin/tvos/TVOSDisplayManager.h" +#import "platform/darwin/tvos/XBMCController.h" + +#include <memory> +#include <mutex> +#include <vector> + +#import <Foundation/Foundation.h> +#import <OpenGLES/ES2/gl.h> +#import <OpenGLES/ES2/glext.h> +#import <QuartzCore/CADisplayLink.h> + +using namespace std::chrono_literals; + +#define CONST_HDMI "HDMI" + +// if there was a devicelost callback +// but no device reset for 3 secs +// a timeout fires the reset callback +// (for ensuring that e.x. AE isn't stuck) +constexpr auto LOST_DEVICE_TIMEOUT_MS{3000ms}; + +// TVOSDisplayLinkCallback is defined in the lower part of the file +@interface TVOSDisplayLinkCallback : NSObject +{ +@private + CVideoSyncTVos* videoSyncImpl; +} +@property(nonatomic, setter=SetVideoSyncImpl:) CVideoSyncTVos* videoSyncImpl; +- (void)runDisplayLink; +@end + +using namespace KODI; +using namespace MESSAGING; + +struct CADisplayLinkWrapper +{ + CADisplayLink* impl; + TVOSDisplayLinkCallback* callbackClass; +}; + +void CWinSystemTVOS::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem); +} + +std::unique_ptr<CWinSystemBase> CWinSystemTVOS::CreateWinSystem() +{ + return std::make_unique<CWinSystemTVOS>(); +} + +void CWinSystemTVOS::MessagePush(XBMC_Event* newEvent) +{ + dynamic_cast<CWinEventsTVOS&>(*m_winEvents).MessagePush(newEvent); +} + +size_t CWinSystemTVOS::GetQueueSize() +{ + return dynamic_cast<CWinEventsTVOS&>(*m_winEvents).GetQueueSize(); +} + +void CWinSystemTVOS::AnnounceOnLostDevice() +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::Log(LOGDEBUG, "CWinSystemTVOS::AnnounceOnLostDevice"); + for (auto dispResource : m_resources) + dispResource->OnLostDisplay(); +} + +void CWinSystemTVOS::AnnounceOnResetDevice() +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + // tell any shared resources + CLog::Log(LOGDEBUG, "CWinSystemTVOS::AnnounceOnResetDevice"); + for (auto dispResource : m_resources) + dispResource->OnResetDisplay(); +} + +void CWinSystemTVOS::StartLostDeviceTimer() +{ + if (m_lostDeviceTimer.IsRunning()) + m_lostDeviceTimer.Restart(); + else + m_lostDeviceTimer.Start(LOST_DEVICE_TIMEOUT_MS, false); +} + +void CWinSystemTVOS::StopLostDeviceTimer() +{ + m_lostDeviceTimer.Stop(); +} + + +int CWinSystemTVOS::GetDisplayIndexFromSettings() +{ + // ATV only supports 1 screen with id = 0 + return 0; +} + +CWinSystemTVOS::CWinSystemTVOS() : CWinSystemBase(), m_lostDeviceTimer(this) +{ + m_bIsBackgrounded = false; + m_pDisplayLink = new CADisplayLinkWrapper; + m_pDisplayLink->callbackClass = [[TVOSDisplayLinkCallback alloc] init]; + + m_winEvents.reset(new CWinEventsTVOS()); + + CAESinkDARWINTVOS::Register(); +} + +CWinSystemTVOS::~CWinSystemTVOS() +{ + m_pDisplayLink->callbackClass = nil; + delete m_pDisplayLink; +} + +bool CWinSystemTVOS::InitWindowSystem() +{ + return CWinSystemBase::InitWindowSystem(); +} + +bool CWinSystemTVOS::DestroyWindowSystem() +{ + return true; +} + +std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> CWinSystemTVOS::GetOSScreenSaverImpl() +{ + std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> screensaver = + std::make_unique<COSScreenSaverTVOS>(); + return screensaver; +} + +bool CWinSystemTVOS::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + if (!SetFullScreen(fullScreen, res, false)) + return false; + + [g_xbmcController setFramebuffer]; + + m_bWindowCreated = true; + + m_eglext = " "; + + const char* tmpExtensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS)); + if (tmpExtensions != nullptr) + { + m_eglext += tmpExtensions; + m_eglext += " "; + } + + CLog::Log(LOGDEBUG, "EGL_EXTENSIONS: {}", m_eglext); + + // register platform dependent objects + CDVDFactoryCodec::ClearHWAccels(); + VTB::CDecoder::Register(); + VIDEOPLAYER::CRendererFactory::ClearRenderer(); + CLinuxRendererGLES::Register(); + CRendererVTB::Register(); + VIDEOPLAYER::CProcessInfoIOS::Register(); + RETRO::CRPProcessInfoIOS::Register(); + RETRO::CRPProcessInfoIOS::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES); + + return true; +} + +bool CWinSystemTVOS::DestroyWindow() +{ + return true; +} + +bool CWinSystemTVOS::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + if (m_nWidth != newWidth || m_nHeight != newHeight) + { + m_nWidth = newWidth; + m_nHeight = newHeight; + } + + CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight); + + return true; +} + +bool CWinSystemTVOS::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + + CLog::Log(LOGDEBUG, "About to switch to {} x {} @ {}", m_nWidth, m_nHeight, res.fRefreshRate); + SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate)); + CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight); + + return true; +} + +bool CWinSystemTVOS::SwitchToVideoMode(int width, int height, double refreshrate) +{ + /*! @todo Currently support SDR dynamic range only. HDR shouldn't be done during + * a modeswitch. Look to create supplemental method to handle sdr/hdr enable + */ + [g_xbmcController.displayManager displayRateSwitch:refreshrate + withDynamicRange:0 /*dynamicRange*/]; + return true; +} + +bool CWinSystemTVOS::GetScreenResolution(int* w, int* h, double* fps, int screenIdx) +{ + *w = [g_xbmcController.displayManager getScreenSize].width; + *h = [g_xbmcController.displayManager getScreenSize].height; + *fps = static_cast<double>([g_xbmcController.displayManager getDisplayRate]); + + CLog::Log(LOGDEBUG, "Current resolution Screen: {} with {} x {} @ {}", screenIdx, *w, *h, *fps); + return true; +} + +void CWinSystemTVOS::UpdateResolutions() +{ + // Add display resolution + int w, h; + double fps; + CWinSystemBase::UpdateResolutions(); + + int screenIdx = GetDisplayIndexFromSettings(); + + //first screen goes into the current desktop mode + if (GetScreenResolution(&w, &h, &fps, screenIdx)) + UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), + CONST_HDMI, w, h, fps, 0); + + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + //now just fill in the possible resolutions for the attached screens + //and push to the resolution info vector + FillInVideoModes(screenIdx); +} + +void CWinSystemTVOS::FillInVideoModes(int screenIdx) +{ + // Potential refresh rates + std::vector<float> supportedDispRefreshRates = {23.976f, 24.000f, 25.000f, 29.970f, + 30.000f, 50.000f, 59.940f, 60.000f}; + + UIScreen* aScreen = UIScreen.screens[screenIdx]; + UIScreenMode* mode = aScreen.currentMode; + int w = mode.size.width; + int h = mode.size.height; + + //! @Todo handle different resolutions than native (ie 720p/1080p on a 4k display) + + for (float refreshrate : supportedDispRefreshRates) + { + RESOLUTION_INFO res; + UpdateDesktopResolution(res, CONST_HDMI, w, h, refreshrate, 0); + CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {} RefreshRate:{} ", + screenIdx, w, h, refreshrate); + + CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res); + CDisplaySettings::GetInstance().AddResolutionInfo(res); + } +} + +bool CWinSystemTVOS::IsExtSupported(const char* extension) const +{ + if (strncmp(extension, "EGL_", 4) != 0) + return CRenderSystemGLES::IsExtSupported(extension); + + std::string name = ' ' + std::string(extension) + ' '; + + return m_eglext.find(name) != std::string::npos; +} + + +bool CWinSystemTVOS::BeginRender() +{ + bool rtn; + + [g_xbmcController setFramebuffer]; + + rtn = CRenderSystemGLES::BeginRender(); + return rtn; +} + +bool CWinSystemTVOS::EndRender() +{ + bool rtn; + + rtn = CRenderSystemGLES::EndRender(); + return rtn; +} + +void CWinSystemTVOS::Register(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemTVOS::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +void CWinSystemTVOS::OnAppFocusChange(bool focus) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_bIsBackgrounded = !focus; + CLog::Log(LOGDEBUG, "CWinSystemTVOS::OnAppFocusChange: {}", focus ? 1 : 0); + for (auto dispResource : m_resources) + dispResource->OnAppFocusChange(focus); +} + +//-------------------------------------------------------------- +//-------------------DisplayLink stuff +@implementation TVOSDisplayLinkCallback +@synthesize videoSyncImpl; +//-------------------------------------------------------------- +- (void)runDisplayLink +{ + @autoreleasepool + { + if (videoSyncImpl != nullptr) + videoSyncImpl->TVosVblankHandler(); + } +} +@end + +bool CWinSystemTVOS::InitDisplayLink(CVideoSyncTVos* syncImpl) +{ + unsigned int currentScreenIdx = GetDisplayIndexFromSettings(); + UIScreen* currentScreen = UIScreen.screens[currentScreenIdx]; + m_pDisplayLink->callbackClass.videoSyncImpl = syncImpl; + m_pDisplayLink->impl = [currentScreen displayLinkWithTarget:m_pDisplayLink->callbackClass + selector:@selector(runDisplayLink)]; + + [m_pDisplayLink->impl addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + return m_pDisplayLink->impl != nil; +} + +void CWinSystemTVOS::DeinitDisplayLink(void) +{ + if (m_pDisplayLink->impl) + { + [m_pDisplayLink->impl invalidate]; + m_pDisplayLink->impl = nil; + [m_pDisplayLink->callbackClass SetVideoSyncImpl:nil]; + } +} +//------------DisplayLink stuff end +//-------------------------------------------------------------- + +void CWinSystemTVOS::PresentRenderImpl(bool rendered) +{ + //glFlush; + if (rendered) + [g_xbmcController presentFramebuffer]; +} + +bool CWinSystemTVOS::HasCursor() +{ + return false; +} + +void CWinSystemTVOS::NotifyAppActiveChange(bool bActivated) +{ + if (bActivated && m_bWasFullScreenBeforeMinimize && + !CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot()) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); +} + +bool CWinSystemTVOS::Minimize() +{ + m_bWasFullScreenBeforeMinimize = + CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot(); + if (m_bWasFullScreenBeforeMinimize) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + + return true; +} + +bool CWinSystemTVOS::Restore() +{ + return false; +} + +bool CWinSystemTVOS::Hide() +{ + return true; +} + +bool CWinSystemTVOS::Show(bool raise) +{ + return true; +} + +CVEAGLContext CWinSystemTVOS::GetEAGLContextObj() +{ + return [g_xbmcController getEAGLContextObj]; +} + +std::vector<std::string> CWinSystemTVOS::GetConnectedOutputs() +{ + std::vector<std::string> outputs; + outputs.emplace_back("Default"); + outputs.emplace_back(CONST_HDMI); + + return outputs; +} + +bool CWinSystemTVOS::MessagePump() +{ + return m_winEvents->MessagePump(); +} 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; +}; + + +} +} +} diff --git a/xbmc/windowing/win10/CMakeLists.txt b/xbmc/windowing/win10/CMakeLists.txt new file mode 100644 index 0000000..7e41c17 --- /dev/null +++ b/xbmc/windowing/win10/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SOURCES WinEventsWin10.cpp + WinSystemWin10.cpp + WinSystemWin10DX.cpp + ../windows/VideoSyncD3D.cpp) + +set(HEADERS WinEventsWin10.h + WinSystemWin10.h + WinSystemWin10DX.h + ../windows/VideoSyncD3D.h + ../windows/WinKeyMap.h) + +core_add_library(windowing_windowsstore) diff --git a/xbmc/windowing/win10/WinEventsWin10.cpp b/xbmc/windowing/win10/WinEventsWin10.cpp new file mode 100644 index 0000000..35da911 --- /dev/null +++ b/xbmc/windowing/win10/WinEventsWin10.cpp @@ -0,0 +1,658 @@ +/* + * 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 "WinEventsWin10.h" + +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "application/AppInboundProtocol.h" +#include "application/Application.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "input/mouse/MouseStat.h" +#include "input/touch/generic/GenericTouchInputHandler.h" +#include "interfaces/AnnouncementManager.h" +#include "messaging/ApplicationMessenger.h" +#include "rendering/dx/DeviceResources.h" +#include "rendering/dx/RenderContext.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/SystemInfo.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "windowing/windows/WinKeyMap.h" + +#include "platform/win10/input/RemoteControlXbox.h" + +#include <winrt/Windows.Devices.Input.h> + +namespace winrt +{ + using namespace Windows::Foundation; +} +using namespace winrt::Windows::ApplicationModel::Core; +using namespace winrt::Windows::Devices::Input; +using namespace winrt::Windows::Graphics::Display; +using namespace winrt::Windows::Media; +using namespace winrt::Windows::System; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::Input; +using namespace winrt::Windows::UI::ViewManagement; + +using namespace PERIPHERALS; + +static winrt::Point GetScreenPoint(winrt::Point point) +{ + auto dpi = DX::DeviceResources::Get()->GetDpi(); + return winrt::Point(DX::ConvertDipsToPixels(point.X, dpi), DX::ConvertDipsToPixels(point.Y, dpi)); +} + +CWinEventsWin10::CWinEventsWin10() = default; +CWinEventsWin10::~CWinEventsWin10() = default; + +void CWinEventsWin10::InitOSKeymap(void) +{ + KODI::WINDOWING::WINDOWS::DIB_InitOSKeymap(); +} + +void CWinEventsWin10::MessagePush(XBMC_Event *newEvent) +{ + // push input events in the queue they may init modal dialog which init + // deeper message loop and call the deeper MessagePump from there. + if ( newEvent->type == XBMC_KEYDOWN + || newEvent->type == XBMC_KEYUP + || newEvent->type == XBMC_MOUSEMOTION + || newEvent->type == XBMC_MOUSEBUTTONDOWN + || newEvent->type == XBMC_MOUSEBUTTONUP + || newEvent->type == XBMC_TOUCH) + { + m_events.push(*newEvent); + } + else + { + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->OnEvent(*newEvent); + } +} + +bool CWinEventsWin10::MessagePump() +{ + bool ret = false; + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + + // processes all pending events and exits immediately + CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent); + + XBMC_Event pumpEvent; + while (m_events.try_pop(pumpEvent)) + { + if (appPort) + ret |= appPort->OnEvent(pumpEvent); + + if (pumpEvent.type == XBMC_MOUSEBUTTONUP) + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_UNFOCUS_ALL, 0, 0, 0, 0); + } + return ret; +} + +size_t CWinEventsWin10::GetQueueSize() +{ + return m_events.unsafe_size(); +} + +void CWinEventsWin10::InitEventHandlers(const CoreWindow& window) +{ + CWinEventsWin10::InitOSKeymap(); + + //window->SetPointerCapture(); + + // window + window.SizeChanged({ this, &CWinEventsWin10::OnWindowSizeChanged }); + window.ResizeStarted({ this, &CWinEventsWin10::OnWindowResizeStarted }); + window.ResizeCompleted({ this, &CWinEventsWin10::OnWindowResizeCompleted }); + window.Closed({ this, &CWinEventsWin10::OnWindowClosed}); + window.VisibilityChanged(CWinEventsWin10::OnVisibilityChanged); + window.Activated(CWinEventsWin10::OnWindowActivationChanged); + // mouse, touch and pen + window.PointerPressed({ this, &CWinEventsWin10::OnPointerPressed }); + window.PointerMoved({ this, &CWinEventsWin10::OnPointerMoved }); + window.PointerReleased({ this, &CWinEventsWin10::OnPointerReleased }); + window.PointerExited({ this, &CWinEventsWin10::OnPointerExited }); + window.PointerWheelChanged({ this, &CWinEventsWin10::OnPointerWheelChanged }); + // keyboard + window.Dispatcher().AcceleratorKeyActivated({ this, &CWinEventsWin10::OnAcceleratorKeyActivated }); + // display + DisplayInformation currentDisplayInformation = DisplayInformation::GetForCurrentView(); + currentDisplayInformation.DpiChanged(CWinEventsWin10::OnDpiChanged); + currentDisplayInformation.OrientationChanged(CWinEventsWin10::OnOrientationChanged); + DisplayInformation::DisplayContentsInvalidated(CWinEventsWin10::OnDisplayContentsInvalidated); + // system + SystemNavigationManager sysNavManager = SystemNavigationManager::GetForCurrentView(); + sysNavManager.BackRequested(CWinEventsWin10::OnBackRequested); + + // requirement for backgroup playback + m_smtc = SystemMediaTransportControls::GetForCurrentView(); + if (m_smtc) + { + m_smtc.IsPlayEnabled(true); + m_smtc.IsPauseEnabled(true); + m_smtc.IsStopEnabled(true); + m_smtc.IsRecordEnabled(true); + m_smtc.IsNextEnabled(true); + m_smtc.IsPreviousEnabled(true); + m_smtc.IsFastForwardEnabled(true); + m_smtc.IsRewindEnabled(true); + m_smtc.IsChannelUpEnabled(true); + m_smtc.IsChannelDownEnabled(true); + if (CSysInfo::GetWindowsDeviceFamily() != CSysInfo::WindowsDeviceFamily::Xbox) + { + m_smtc.ButtonPressed(CWinEventsWin10::OnSystemMediaButtonPressed); + } + m_smtc.IsEnabled(true);; + CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this); + } + if (CSysInfo::GetWindowsDeviceFamily() == CSysInfo::WindowsDeviceFamily::Xbox) + { + m_remote = std::make_unique<CRemoteControlXbox>(); + m_remote->Initialize(); + } +} + +void CWinEventsWin10::UpdateWindowSize() +{ + auto size = DX::DeviceResources::Get()->GetOutputSize(); + + CLog::Log(LOGDEBUG, __FUNCTION__ ": window resize event {:f} x {:f} (as:{})", size.Width, + size.Height, + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen ? "true" + : "false"); + + auto appView = ApplicationView::GetForCurrentView(); + appView.SetDesiredBoundsMode(ApplicationViewBoundsMode::UseCoreWindow); + + // seems app has lost FS mode it may occurs if an user use core window's button + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen && !appView.IsFullScreenMode()) + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = false; + + XBMC_Event newEvent = {}; + newEvent.type = XBMC_VIDEORESIZE; + newEvent.resize.w = size.Width; + newEvent.resize.h = size.Height; + if (g_application.GetRenderGUI() && !DX::Windowing()->IsAlteringWindow() && newEvent.resize.w > 0 && newEvent.resize.h > 0) + MessagePush(&newEvent); +} + +void CWinEventsWin10::OnResize(float width, float height) +{ + CLog::Log(LOGDEBUG, __FUNCTION__": window size changed."); + m_logicalWidth = width; + m_logicalHeight = height; + m_bResized = true; + + if (m_sizeChanging) + return; + + HandleWindowSizeChanged(); +} + +// Window event handlers. +void CWinEventsWin10::OnWindowSizeChanged(const CoreWindow&, const WindowSizeChangedEventArgs& args) +{ + OnResize(args.Size().Width, args.Size().Height); +} + +void CWinEventsWin10::OnWindowResizeStarted(const CoreWindow& sender, const winrt::IInspectable&) +{ + CLog::Log(LOGDEBUG, __FUNCTION__": window resize started."); + m_logicalPosX = sender.Bounds().X; + m_logicalPosY = sender.Bounds().Y; + m_sizeChanging = true; +} + +void CWinEventsWin10::OnWindowResizeCompleted(const CoreWindow& sender, const winrt::IInspectable&) +{ + CLog::Log(LOGDEBUG, __FUNCTION__": window resize completed."); + m_sizeChanging = false; + + if (m_logicalPosX != sender.Bounds().X || m_logicalPosY != sender.Bounds().Y) + m_bMoved = true; + + HandleWindowSizeChanged(); +} + +void CWinEventsWin10::HandleWindowSizeChanged() +{ + CLog::Log(LOGDEBUG, __FUNCTION__": window size/move handled."); + if (m_bMoved) + { + // it will get position from CoreWindow + DX::Windowing()->OnMove(0, 0); + } + if (m_bResized) + { + DX::Windowing()->OnResize(m_logicalWidth, m_logicalHeight); + UpdateWindowSize(); + } + m_bResized = false; + m_bMoved = false; +} + +void CWinEventsWin10::OnVisibilityChanged(const CoreWindow& sender, const VisibilityChangedEventArgs& args) +{ + bool active = g_application.GetRenderGUI(); + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->SetRenderGUI(args.Visible()); + + if (g_application.GetRenderGUI() != active) + DX::Windowing()->NotifyAppActiveChange(g_application.GetRenderGUI()); + CLog::Log(LOGDEBUG, __FUNCTION__ ": window is {}", + g_application.GetRenderGUI() ? "shown" : "hidden"); +} + +void CWinEventsWin10::OnWindowActivationChanged(const CoreWindow& sender, const WindowActivatedEventArgs& args) +{ + bool active = g_application.GetRenderGUI(); + if (args.WindowActivationState() == CoreWindowActivationState::Deactivated) + { + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->SetRenderGUI(DX::Windowing()->WindowedMode()); + } + else if (args.WindowActivationState() == CoreWindowActivationState::PointerActivated + || args.WindowActivationState() == CoreWindowActivationState::CodeActivated) + { + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + if (appPort) + appPort->SetRenderGUI(true); + } + if (g_application.GetRenderGUI() != active) + DX::Windowing()->NotifyAppActiveChange(g_application.GetRenderGUI()); + CLog::Log(LOGDEBUG, __FUNCTION__ ": window is {}", + g_application.GetRenderGUI() ? "active" : "inactive"); +} + +void CWinEventsWin10::OnWindowClosed(const CoreWindow& sender, const CoreWindowEventArgs& args) +{ + // send quit command to the application if it's still running + if (!g_application.m_bStop) + { + XBMC_Event newEvent = {}; + newEvent.type = XBMC_QUIT; + MessagePush(&newEvent); + } +} + +void CWinEventsWin10::OnPointerPressed(const CoreWindow&, const PointerEventArgs& args) +{ + XBMC_Event newEvent = {}; + + PointerPoint point = args.CurrentPoint(); + auto position = GetScreenPoint(point.Position()); + + if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Touch) + { + CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputDown, position.X, position.Y, point.Timestamp(), 0, 10); + return; + } + else + { + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.x = position.X; + newEvent.button.y = position.Y; + if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Mouse) + { + if (point.Properties().IsLeftButtonPressed()) + newEvent.button.button = XBMC_BUTTON_LEFT; + else if (point.Properties().IsMiddleButtonPressed()) + newEvent.button.button = XBMC_BUTTON_MIDDLE; + else if (point.Properties().IsRightButtonPressed()) + newEvent.button.button = XBMC_BUTTON_RIGHT; + } + else if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Pen) + { + // pen + // TODO + } + } + MessagePush(&newEvent); +} + +void CWinEventsWin10::OnPointerMoved(const CoreWindow&, const PointerEventArgs& args) +{ + PointerPoint point = args.CurrentPoint(); + auto position = GetScreenPoint(point.Position()); + + if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Touch) + { + if (point.IsInContact()) + { + CGenericTouchInputHandler::GetInstance().UpdateTouchPointer(0, position.X, position.Y, point.Timestamp(), 10.f); + CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputMove, position.X, position.Y, point.Timestamp(), 0, 10.f); + } + return; + } + + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEMOTION; + newEvent.motion.x = position.X; + newEvent.motion.y = position.Y; + MessagePush(&newEvent); +} + +void CWinEventsWin10::OnPointerReleased(const CoreWindow&, const PointerEventArgs& args) +{ + PointerPoint point = args.CurrentPoint(); + auto position = GetScreenPoint(point.Position()); + + if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Touch) + { + CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputUp, position.X, position.Y, point.Timestamp(), 0, 10); + return; + } + + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.x = position.X; + newEvent.button.y = position.Y; + + if (point.Properties().PointerUpdateKind() == PointerUpdateKind::LeftButtonReleased) + newEvent.button.button = XBMC_BUTTON_LEFT; + else if (point.Properties().PointerUpdateKind() == PointerUpdateKind::MiddleButtonReleased) + newEvent.button.button = XBMC_BUTTON_MIDDLE; + else if (point.Properties().PointerUpdateKind() == PointerUpdateKind::RightButtonReleased) + newEvent.button.button = XBMC_BUTTON_RIGHT; + + MessagePush(&newEvent); +} + +void CWinEventsWin10::OnPointerExited(const CoreWindow&, const PointerEventArgs& args) +{ + const PointerPoint& point = args.CurrentPoint(); + auto position = GetScreenPoint(point.Position()); + + if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Touch) + { + CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputAbort, position.X, position.Y, point.Timestamp(), 0, 10); + } +} + +void CWinEventsWin10::OnPointerWheelChanged(const CoreWindow&, const PointerEventArgs& args) +{ + XBMC_Event newEvent = {}; + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.x = args.CurrentPoint().Position().X; + newEvent.button.y = args.CurrentPoint().Position().Y; + newEvent.button.button = args.CurrentPoint().Properties().MouseWheelDelta() > 0 ? XBMC_BUTTON_WHEELUP : XBMC_BUTTON_WHEELDOWN; + MessagePush(&newEvent); + newEvent.type = XBMC_MOUSEBUTTONUP; + MessagePush(&newEvent); +} + +void CWinEventsWin10::Kodi_KeyEvent(unsigned int vkey, unsigned scancode, unsigned keycode, bool isDown) +{ + using State = CoreVirtualKeyStates; + + XBMC_keysym keysym = {}; + keysym.scancode = scancode; + keysym.sym = KODI::WINDOWING::WINDOWS::VK_keymap[vkey]; + keysym.unicode = keycode; + + auto window = CoreWindow::GetForCurrentThread(); + + uint16_t mod = (uint16_t)XBMCKMOD_NONE; + // If left control and right alt are down this usually means that AltGr is down + if ((window.GetKeyState(VirtualKey::LeftControl) & State::Down) == State::Down + && (window.GetKeyState(VirtualKey::RightMenu) & State::Down) == State::Down) + { + mod |= XBMCKMOD_MODE; + mod |= XBMCKMOD_MODE; + } + else + { + if ((window.GetKeyState(VirtualKey::LeftControl) & State::Down) == State::Down) + mod |= XBMCKMOD_LCTRL; + if ((window.GetKeyState(VirtualKey::RightMenu) & State::Down) == State::Down) + mod |= XBMCKMOD_RALT; + } + + // Check the remaining modifiers + if ((window.GetKeyState(VirtualKey::LeftShift) & State::Down) == State::Down) + mod |= XBMCKMOD_LSHIFT; + if ((window.GetKeyState(VirtualKey::RightShift) & State::Down) == State::Down) + mod |= XBMCKMOD_RSHIFT; + if ((window.GetKeyState(VirtualKey::RightControl) & State::Down) == State::Down) + mod |= XBMCKMOD_RCTRL; + if ((window.GetKeyState(VirtualKey::LeftMenu) & State::Down) == State::Down) + mod |= XBMCKMOD_LALT; + if ((window.GetKeyState(VirtualKey::LeftWindows) & State::Down) == State::Down) + mod |= XBMCKMOD_LSUPER; + if ((window.GetKeyState(VirtualKey::RightWindows) & State::Down) == State::Down) + mod |= XBMCKMOD_LSUPER; + + keysym.mod = static_cast<XBMCMod>(mod); + + XBMC_Event newEvent = {}; + newEvent.type = isDown ? XBMC_KEYDOWN : XBMC_KEYUP; + newEvent.key.keysym = keysym; + MessagePush(&newEvent); +} + +void CWinEventsWin10::OnAcceleratorKeyActivated(const CoreDispatcher&, const AcceleratorKeyEventArgs& args) +{ + static auto lockedState = CoreVirtualKeyStates::Locked; + static VirtualKey keyStore = VirtualKey::None; + + // skip if device is remote control + if (m_remote && m_remote->IsRemoteDevice(args.DeviceId().c_str())) + return; + + bool isDown = false; + unsigned keyCode = 0; + unsigned vk = static_cast<unsigned>(args.VirtualKey()); + + auto window = CoreWindow::GetForCurrentThread(); + bool numLockLocked = ((window.GetKeyState(VirtualKey::NumberKeyLock) & lockedState) == lockedState); + + switch (args.EventType()) + { + case CoreAcceleratorKeyEventType::KeyDown: + case CoreAcceleratorKeyEventType::SystemKeyDown: + { + if ( (vk == 0x08) // VK_BACK + || (vk == 0x09) // VK_TAB + || (vk == 0x0C) // VK_CLEAR + || (vk == 0x0D) // VK_RETURN + || (vk == 0x1B) // VK_ESCAPE + || (vk == 0x20) // VK_SPACE + || (vk >= 0x30 && vk <= 0x39) // numeric keys + || (vk >= 0x41 && vk <= 0x5A) // alphabetic keys + || (vk >= 0x60 && vk <= 0x69 && numLockLocked) // keypad numeric (if numlock is on) + || (vk >= 0x6A && vk <= 0x6F) // keypad keys except numeric + || (vk >= 0x92 && vk <= 0x96) // OEM specific + || (vk >= 0xBA && vk <= 0xC0) // OEM specific + || (vk >= 0xDB && vk <= 0xDF) // OEM specific + || (vk >= 0xE1 && vk <= 0xF5 && vk != 0xE5 && vk != 0xE7 && vk != 0xE8) // OEM specific + ) + { + // store this for character events, because VirtualKey is key code on character event. + keyStore = args.VirtualKey(); + return; + } + isDown = true; + break; + } + case CoreAcceleratorKeyEventType::KeyUp: + case CoreAcceleratorKeyEventType::SystemKeyUp: + break; + case CoreAcceleratorKeyEventType::Character: + case CoreAcceleratorKeyEventType::SystemCharacter: + case CoreAcceleratorKeyEventType::UnicodeCharacter: + case CoreAcceleratorKeyEventType::DeadCharacter: + case CoreAcceleratorKeyEventType::SystemDeadCharacter: + { + // VirtualKey is KeyCode + keyCode = static_cast<unsigned>(args.VirtualKey()); + // rewrite vk with stored value + vk = static_cast<unsigned>(keyStore); + // reset stored value + keyStore = VirtualKey::None; + isDown = true; + } + default: + break; + } + + Kodi_KeyEvent(vk, args.KeyStatus().ScanCode, keyCode, isDown); + args.Handled(true); +} + +// DisplayInformation event handlers. +void CWinEventsWin10::OnDpiChanged(const DisplayInformation& sender, const winrt::IInspectable&) +{ + // Note: The value for LogicalDpi retrieved here may not match the effective DPI of the app + // if it is being scaled for high resolution devices. Once the DPI is set on DeviceResources, + // you should always retrieve it using the GetDpi method. + // See DeviceResources.cpp for more details. + //critical_section::scoped_lock lock(m_deviceResources->GetCriticalSection()); + RECT resizeRect = { 0,0,0,0 }; + DX::Windowing()->DPIChanged(sender.LogicalDpi(), resizeRect); + CGenericTouchInputHandler::GetInstance().SetScreenDPI(DX::DisplayMetrics::Dpi100); +} + +void CWinEventsWin10::OnOrientationChanged(const DisplayInformation&, const winrt::IInspectable&) +{ + //critical_section::scoped_lock lock(m_deviceResources->GetCriticalSection()); + //m_deviceResources->SetCurrentOrientation(sender->CurrentOrientation); + + //auto size = DX::DeviceResources::Get()->GetOutputSize(); + //UpdateWindowSize(size.Width, size.Height); +} + +void CWinEventsWin10::OnDisplayContentsInvalidated(const DisplayInformation&, const winrt::IInspectable&) +{ + CLog::Log(LOGDEBUG, __FUNCTION__": onevent."); + DX::DeviceResources::Get()->ValidateDevice(); +} + +void CWinEventsWin10::OnBackRequested(const winrt::IInspectable&, const BackRequestedEventArgs& args) +{ + // handle this only on windows mobile + if (CSysInfo::GetWindowsDeviceFamily() == CSysInfo::WindowsDeviceFamily::Mobile) + { + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(new CAction(ACTION_NAV_BACK))); + } + args.Handled(true); +} + +void CWinEventsWin10::OnSystemMediaButtonPressed(const SystemMediaTransportControls&, const SystemMediaTransportControlsButtonPressedEventArgs& args) +{ + int action = ACTION_NONE; + switch (args.Button()) + { + case SystemMediaTransportControlsButton::ChannelDown: + action = ACTION_CHANNEL_DOWN; + break; + case SystemMediaTransportControlsButton::ChannelUp: + action = ACTION_CHANNEL_UP; + break; + case SystemMediaTransportControlsButton::FastForward: + action = ACTION_PLAYER_FORWARD; + break; + case SystemMediaTransportControlsButton::Rewind: + action = ACTION_PLAYER_REWIND; + break; + case SystemMediaTransportControlsButton::Next: + action = ACTION_NEXT_ITEM; + break; + case SystemMediaTransportControlsButton::Previous: + action = ACTION_PREV_ITEM; + break; + case SystemMediaTransportControlsButton::Pause: + case SystemMediaTransportControlsButton::Play: + action = ACTION_PLAYER_PLAYPAUSE; + break; + case SystemMediaTransportControlsButton::Stop: + action = ACTION_STOP; + break; + case SystemMediaTransportControlsButton::Record: + action = ACTION_RECORD; + break; + default: + break; + } + if (action != ACTION_NONE) + { + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(new CAction(action))); + } +} + +void CWinEventsWin10::Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) +{ + if (flag & ANNOUNCEMENT::Player) + { + double speed = 1.0; + if (data.isMember("player") && data["player"].isMember("speed")) + speed = data["player"]["speed"].asDouble(1.0); + + bool changed = false; + MediaPlaybackStatus status = MediaPlaybackStatus::Changing; + + if (message == "OnPlay" || message == "OnResume") + { + changed = true; + status = MediaPlaybackStatus::Playing; + } + else if (message == "OnStop") + { + changed = true; + status = MediaPlaybackStatus::Stopped; + } + else if (message == "OnPause") + { + changed = true; + status = MediaPlaybackStatus::Paused; + } + else if (message == "OnSpeedChanged") + { + changed = true; + status = speed != 0.0 ? MediaPlaybackStatus::Playing : MediaPlaybackStatus::Paused; + } + + if (changed) + { + try + { + auto dispatcher = CoreApplication::MainView().Dispatcher(); + if (dispatcher) + { + dispatcher.RunAsync(CoreDispatcherPriority::Normal, DispatchedHandler([status, speed] + { + auto smtc = SystemMediaTransportControls::GetForCurrentView(); + if (!smtc) + return; + + smtc.PlaybackStatus(status); + smtc.PlaybackRate(speed); + })); + } + } + catch (const winrt::hresult_error&) + { + } + } + } +} diff --git a/xbmc/windowing/win10/WinEventsWin10.h b/xbmc/windowing/win10/WinEventsWin10.h new file mode 100644 index 0000000..c4bd345 --- /dev/null +++ b/xbmc/windowing/win10/WinEventsWin10.h @@ -0,0 +1,83 @@ +/* + * 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 + +#include "interfaces/IAnnouncer.h" +#include "windowing/WinEvents.h" + +#include <concurrent_queue.h> +#include <winrt/Windows.Media.h> + +class CRemoteControlXbox; + +class CWinEventsWin10 : public IWinEvents + , public ANNOUNCEMENT::IAnnouncer +{ +public: + CWinEventsWin10(); + virtual ~CWinEventsWin10(); + + void MessagePush(XBMC_Event *newEvent); + bool MessagePump() override; + virtual size_t GetQueueSize(); + + // initialization + void InitEventHandlers(const winrt::Windows::UI::Core::CoreWindow&); + static void InitOSKeymap(void); + + // Window event handlers. + void OnWindowSizeChanged(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::WindowSizeChangedEventArgs&); + void OnWindowResizeStarted(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::Foundation::IInspectable&); + void OnWindowResizeCompleted(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::Foundation::IInspectable&); + void OnWindowClosed(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::CoreWindowEventArgs&); + static void OnWindowActivationChanged(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::WindowActivatedEventArgs&); + static void OnVisibilityChanged(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::VisibilityChangedEventArgs&); + // touch mouse and pen + void OnPointerPressed(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&); + void OnPointerMoved(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&); + void OnPointerReleased(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&); + void OnPointerExited(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&); + void OnPointerWheelChanged(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&); + // keyboard + void OnAcceleratorKeyActivated(const winrt::Windows::UI::Core::CoreDispatcher&, const winrt::Windows::UI::Core::AcceleratorKeyEventArgs&); + + // DisplayInformation event handlers. + static void OnDpiChanged(const winrt::Windows::Graphics::Display::DisplayInformation&, const winrt::Windows::Foundation::IInspectable&); + static void OnOrientationChanged(const winrt::Windows::Graphics::Display::DisplayInformation&, const winrt::Windows::Foundation::IInspectable&); + static void OnDisplayContentsInvalidated(const winrt::Windows::Graphics::Display::DisplayInformation&, const winrt::Windows::Foundation::IInspectable&); + // system + static void OnBackRequested(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::UI::Core::BackRequestedEventArgs&); + // system media handlers + static void OnSystemMediaButtonPressed(const winrt::Windows::Media::SystemMediaTransportControls& + , const winrt::Windows::Media::SystemMediaTransportControlsButtonPressedEventArgs&); + // IAnnouncer overrides + void Announce(ANNOUNCEMENT::AnnouncementFlag flag, + const std::string& sender, + const std::string& message, + const CVariant& data) override; + +private: + friend class CWinSystemWin10; + + void OnResize(float width, float height); + void UpdateWindowSize(); + void Kodi_KeyEvent(unsigned int vkey, unsigned scancode, unsigned keycode, bool isDown); + void HandleWindowSizeChanged(); + + Concurrency::concurrent_queue<XBMC_Event> m_events; + winrt::Windows::Media::SystemMediaTransportControls m_smtc{ nullptr }; + bool m_bResized{ false }; + bool m_bMoved{ false }; + bool m_sizeChanging{ false }; + float m_logicalWidth{ 0 }; + float m_logicalHeight{ 0 }; + float m_logicalPosX{ 0 }; + float m_logicalPosY{ 0 }; + std::unique_ptr<CRemoteControlXbox> m_remote; +}; diff --git a/xbmc/windowing/win10/WinSystemWin10.cpp b/xbmc/windowing/win10/WinSystemWin10.cpp new file mode 100644 index 0000000..6de3f78 --- /dev/null +++ b/xbmc/windowing/win10/WinSystemWin10.cpp @@ -0,0 +1,660 @@ +/* + * 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 "WinSystemWin10.h" + +#include "ServiceBroker.h" +#include "WinEventsWin10.h" +#include "application/Application.h" +#include "cores/AudioEngine/AESinkFactory.h" +#include "cores/AudioEngine/Sinks/AESinkWASAPI.h" +#include "cores/AudioEngine/Sinks/AESinkXAudio.h" +#include "rendering/dx/DirectXHelper.h" +#include "rendering/dx/RenderContext.h" +#include "rendering/dx/ScreenshotSurfaceWindows.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/SystemInfo.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/windows/VideoSyncD3D.h" + +#include "platform/win10/AsyncHelpers.h" +#include "platform/win32/CharsetConverter.h" + +#include <mutex> + +#pragma pack(push,8) + +#include <tpcshrd.h> +#include <ppltasks.h> +#include <winrt/Windows.ApplicationModel.DataTransfer.h> +#include <winrt/Windows.Foundation.Metadata.h> +#include <winrt/Windows.Graphics.Display.h> +#include <winrt/Windows.Graphics.Display.Core.h> + +using namespace winrt::Windows::ApplicationModel::DataTransfer; +using namespace winrt::Windows::Foundation::Metadata; +using namespace winrt::Windows::Graphics::Display; +using namespace winrt::Windows::Graphics::Display::Core; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::ViewManagement; + +using namespace std::chrono_literals; + +CWinSystemWin10::CWinSystemWin10() + : CWinSystemBase() + , m_ValidWindowedPosition(false) + , m_IsAlteringWindow(false) + , m_delayDispReset(false) + , m_state(WINDOW_STATE_WINDOWED) + , m_fullscreenState(WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW) + , m_windowState(WINDOW_WINDOW_STATE_WINDOWED) + , m_inFocus(false) + , m_bMinimized(false) +{ + m_winEvents.reset(new CWinEventsWin10()); + + AE::CAESinkFactory::ClearSinks(); + CAESinkXAudio::Register(); + CAESinkWASAPI::Register(); + CScreenshotSurfaceWindows::Register(); +} + +CWinSystemWin10::~CWinSystemWin10() +{ +}; + +bool CWinSystemWin10::InitWindowSystem() +{ + m_coreWindow = CoreWindow::GetForCurrentThread(); + dynamic_cast<CWinEventsWin10&>(*m_winEvents).InitEventHandlers(m_coreWindow); + + if (!CWinSystemBase::InitWindowSystem()) + return false; + + if (m_displays.empty()) + { + CLog::Log(LOGERROR, "{} - no suitable monitor found, aborting...", __FUNCTION__); + return false; + } + + return true; +} + +bool CWinSystemWin10::DestroyWindowSystem() +{ + m_bWindowCreated = false; + RestoreDesktopResolution(); + return true; +} + +bool CWinSystemWin10::CanDoWindowed() +{ + return CSysInfo::GetWindowsDeviceFamily() == CSysInfo::Desktop; +} + +bool CWinSystemWin10::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + UpdateStates(fullScreen); + // initialize the state + WINDOW_STATE state = GetState(fullScreen); + + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + m_fRefreshRate = res.fRefreshRate; + m_inFocus = true; + m_bWindowCreated = true; + m_state = state; + + m_coreWindow.Activate(); + + AdjustWindow(); + // dispatch all events currently pending in the queue to show window's content + // and hide UWP splash, without this the Kodi's splash will not be shown + m_coreWindow.Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending); + + return true; +} + +bool CWinSystemWin10::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + m_nWidth = newWidth; + m_nHeight = newHeight; + + if (newLeft > 0) + m_nLeft = newLeft; + + if (newTop > 0) + m_nTop = newTop; + + AdjustWindow(); + + return true; +} + +void CWinSystemWin10::FinishWindowResize(int newWidth, int newHeight) +{ + m_nWidth = newWidth; + m_nHeight = newHeight; + + float dpi = DX::DeviceResources::Get()->GetDpi(); + int dipsWidth = round(DX::ConvertPixelsToDips(m_nWidth, dpi)); + int dipsHeight = round(DX::ConvertPixelsToDips(m_nHeight, dpi)); + + ApplicationView::PreferredLaunchViewSize(winrt::Windows::Foundation::Size(dipsWidth, dipsHeight)); + ApplicationView::PreferredLaunchWindowingMode(ApplicationViewWindowingMode::PreferredLaunchViewSize); +} + +void CWinSystemWin10::AdjustWindow() +{ + CLog::Log(LOGDEBUG, __FUNCTION__": adjusting window if required."); + + auto appView = ApplicationView::GetForCurrentView(); + bool isInFullscreen = appView.IsFullScreenMode(); + + if (m_state == WINDOW_STATE_FULLSCREEN_WINDOW || m_state == WINDOW_STATE_FULLSCREEN) + { + if (!isInFullscreen) + { + if (appView.TryEnterFullScreenMode()) + ApplicationView::PreferredLaunchWindowingMode(ApplicationViewWindowingMode::FullScreen); + } + } + else // m_state == WINDOW_STATE_WINDOWED + { + if (isInFullscreen) + { + appView.ExitFullScreenMode(); + } + + int viewWidth = appView.VisibleBounds().Width; + int viewHeight = appView.VisibleBounds().Height; + + float dpi = DX::DeviceResources::Get()->GetDpi(); + int dipsWidth = round(DX::ConvertPixelsToDips(m_nWidth, dpi)); + int dipsHeight = round(DX::ConvertPixelsToDips(m_nHeight, dpi)); + + if (viewHeight != dipsHeight || viewWidth != dipsWidth) + { + if (!appView.TryResizeView(winrt::Windows::Foundation::Size(dipsWidth, dipsHeight))) + { + CLog::LogF(LOGDEBUG, __FUNCTION__, "resizing ApplicationView failed."); + } + } + + ApplicationView::PreferredLaunchViewSize(winrt::Windows::Foundation::Size(dipsWidth, dipsHeight)); + ApplicationView::PreferredLaunchWindowingMode(ApplicationViewWindowingMode::PreferredLaunchViewSize); + } +} + +bool CWinSystemWin10::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + CWinSystemWin10::UpdateStates(fullScreen); + WINDOW_STATE state = GetState(fullScreen); + + CLog::Log(LOGDEBUG, "{} ({}) with size {}x{}, refresh {:f}{}", __FUNCTION__, + window_state_names[state], res.iWidth, res.iHeight, res.fRefreshRate, + (res.dwFlags & D3DPRESENTFLAG_INTERLACED) ? "i" : ""); + + bool forceChange = false; // resolution/display is changed but window state isn't changed + bool stereoChange = IsStereoEnabled() != (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_HARDWAREBASED); + + if ( m_nWidth != res.iWidth || m_nHeight != res.iHeight || m_fRefreshRate != res.fRefreshRate || + stereoChange || m_bFirstResChange) + { + forceChange = true; + } + + if (state == m_state && !forceChange) + return true; + + // entering to stereo mode, limit resolution to 1080p@23.976 + if (stereoChange && !IsStereoEnabled() && res.iWidth > 1280) + { + res = CDisplaySettings::GetInstance().GetResolutionInfo(CResolutionUtils::ChooseBestResolution(24.f / 1.001f, 1920, 1080, true)); + } + + if (m_state == WINDOW_STATE_WINDOWED) + { + if (m_coreWindow) + { + m_nLeft = m_coreWindow.Bounds().X; + m_nTop = m_coreWindow.Bounds().Y; + m_ValidWindowedPosition = true; + } + } + + m_IsAlteringWindow = true; + ReleaseBackBuffer(); + + m_bFirstResChange = false; + m_bFullScreen = fullScreen; + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bBlankOtherDisplay = blankOtherDisplays; + m_fRefreshRate = res.fRefreshRate; + + if (state == WINDOW_STATE_FULLSCREEN) + { + // isn't allowed in UWP + } + else if (m_state == WINDOW_STATE_FULLSCREEN || m_state == WINDOW_STATE_FULLSCREEN_WINDOW) // we're in fullscreen state now + { + if (state == WINDOW_STATE_WINDOWED) // go to a windowed state + { + // need to restore resolution if it was changed to not native + // because we do not support resolution change in windowed mode + RestoreDesktopResolution(); + } + else if (state == WINDOW_STATE_FULLSCREEN_WINDOW) // enter fullscreen window instead + { + ChangeResolution(res, stereoChange); + } + + m_state = state; + AdjustWindow(); + } + else // we're in windowed state now + { + if (state == WINDOW_STATE_FULLSCREEN_WINDOW) + { + ChangeResolution(res, stereoChange); + + m_state = state; + AdjustWindow(); + } + } + + CreateBackBuffer(); + m_IsAlteringWindow = false; + return true; +} + +bool CWinSystemWin10::DPIChanged(WORD dpi, RECT windowRect) const +{ + (void)dpi; + return true; +} + +void CWinSystemWin10::RestoreDesktopResolution() +{ + CLog::Log(LOGDEBUG, __FUNCTION__": restoring default desktop resolution"); + ChangeResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP)); +} + +const MONITOR_DETAILS* CWinSystemWin10::GetDefaultMonitor() const +{ + if (m_displays.empty()) + return nullptr; + + return &m_displays.front(); +} + +bool CWinSystemWin10::ChangeResolution(const RESOLUTION_INFO& res, bool forceChange /*= false*/) +{ + const MONITOR_DETAILS* details = GetDefaultMonitor(); + + if (!details) + return false; + + if (ApiInformation::IsTypePresent(L"Windows.Graphics.Display.Core.HdmiDisplayInformation")) + { + bool changed = false; + auto hdmiInfo = HdmiDisplayInformation::GetForCurrentView(); + if (hdmiInfo != nullptr) + { + // default mode not in list of supported display modes + if (res.iScreenWidth == details->ScreenWidth && res.iScreenHeight == details->ScreenHeight + && fabs(res.fRefreshRate - details->RefreshRate) <= 0.00001) + { + Wait(hdmiInfo.SetDefaultDisplayModeAsync()); + changed = true; + } + else + { + bool needStereo = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_HARDWAREBASED; + auto hdmiModes = hdmiInfo.GetSupportedDisplayModes(); + + HdmiDisplayMode selected = nullptr; + for (const auto& mode : hdmiModes) + { + if (res.iScreenWidth == mode.ResolutionWidthInRawPixels() && res.iScreenHeight == mode.ResolutionHeightInRawPixels() + && fabs(res.fRefreshRate - mode.RefreshRate()) <= 0.00001) + { + selected = mode; + if (needStereo == mode.StereoEnabled()) + break; + } + } + + if (selected != nullptr) + { + changed = Wait(hdmiInfo.RequestSetCurrentDisplayModeAsync(selected)); + } + } + } + + // changing display mode doesn't fire CoreWindow::SizeChanged event + if (changed && m_bWindowCreated) + { + // dispatch all events currently pending in the queue to change window's content + m_coreWindow.Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending); + + float dpi = DisplayInformation::GetForCurrentView().LogicalDpi(); + float dipsW = DX::ConvertPixelsToDips(m_nWidth, dpi); + float dipsH = DX::ConvertPixelsToDips(m_nHeight, dpi); + + dynamic_cast<CWinEventsWin10&>(*m_winEvents).OnResize(dipsW, dipsH); + } + return changed; + } + + CLog::LogF(LOGDEBUG, "Not supported."); + return false; +} + +void CWinSystemWin10::UpdateResolutions() +{ + m_displays.clear(); + + CWinSystemBase::UpdateResolutions(); + GetConnectedDisplays(m_displays); + + const MONITOR_DETAILS* details = GetDefaultMonitor(); + if (!details) + return; + + float refreshRate; + int w = details->ScreenWidth; + int h = details->ScreenHeight; + uint32_t dwFlags = details->Interlaced ? D3DPRESENTFLAG_INTERLACED : 0;; + + if (details->RefreshRate == 59 || details->RefreshRate == 29 || details->RefreshRate == 23) + refreshRate = static_cast<float>(details->RefreshRate + 1) / 1.001f; + else + refreshRate = static_cast<float>(details->RefreshRate); + + RESOLUTION_INFO& primary_info = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP); + UpdateDesktopResolution(primary_info, "Default", w, h, refreshRate, dwFlags); + CLog::Log(LOGINFO, "Primary mode: {}", primary_info.strMode); + + // erase previous stored modes + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + if (ApiInformation::IsTypePresent(L"Windows.Graphics.Display.Core.HdmiDisplayInformation")) + { + auto hdmiInfo = HdmiDisplayInformation::GetForCurrentView(); + if (hdmiInfo != nullptr) + { + auto hdmiModes = hdmiInfo.GetSupportedDisplayModes(); + for (const auto& mode : hdmiModes) + { + RESOLUTION_INFO res; + res.iWidth = mode.ResolutionWidthInRawPixels(); + res.iHeight = mode.ResolutionHeightInRawPixels(); + res.bFullScreen = true; + res.dwFlags = 0; + res.fRefreshRate = mode.RefreshRate(); + res.fPixelRatio = 1.0f; + res.iScreenWidth = res.iWidth; + res.iScreenHeight = res.iHeight; + res.iSubtitles = res.iHeight; + res.strMode = StringUtils::Format("Default: {}x{} @ {:.2f}Hz", res.iWidth, res.iHeight, + res.fRefreshRate); + GetGfxContext().ResetOverscan(res); + + if (AddResolution(res)) + CLog::Log(LOGINFO, "Additional mode: {} {}", res.strMode, + mode.Is2086MetadataSupported() ? "(HDR)" : ""); + } + } + } + + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +bool CWinSystemWin10::AddResolution(const RESOLUTION_INFO &res) +{ + for (unsigned int i = RES_CUSTOM; i < CDisplaySettings::GetInstance().ResolutionInfoSize(); i++) + { + RESOLUTION_INFO& info = CDisplaySettings::GetInstance().GetResolutionInfo(i); + if ( info.iWidth == res.iWidth + && info.iHeight == res.iHeight + && info.iScreenWidth == res.iScreenWidth + && info.iScreenHeight == res.iScreenHeight + && info.fRefreshRate == res.fRefreshRate + && info.dwFlags == res.dwFlags) + return false; // already have this resolution + } + + CDisplaySettings::GetInstance().AddResolutionInfo(res); + return true; +} + +void CWinSystemWin10::GetConnectedDisplays(std::vector<MONITOR_DETAILS>& outputs) +{ + auto dispatcher = m_coreWindow.Dispatcher(); + DispatchedHandler handler([&]() + { + MONITOR_DETAILS md = {}; + + auto displayInfo = DisplayInformation::GetForCurrentView(); + bool flipResolution = false; + switch (displayInfo.NativeOrientation()) + { + case DisplayOrientations::Landscape: + switch (displayInfo.CurrentOrientation()) + { + case DisplayOrientations::Portrait: + case DisplayOrientations::PortraitFlipped: + flipResolution = true; + break; + } + break; + case DisplayOrientations::Portrait: + switch (displayInfo.CurrentOrientation()) + { + case DisplayOrientations::Landscape: + case DisplayOrientations::LandscapeFlipped: + flipResolution = true; + break; + } + break; + } + md.ScreenWidth = flipResolution ? displayInfo.ScreenHeightInRawPixels() : displayInfo.ScreenWidthInRawPixels(); + md.ScreenHeight = flipResolution ? displayInfo.ScreenWidthInRawPixels() : displayInfo.ScreenHeightInRawPixels(); + + if (ApiInformation::IsTypePresent(L"Windows.Graphics.Display.Core.HdmiDisplayInformation")) + { + auto hdmiInfo = HdmiDisplayInformation::GetForCurrentView(); + if (hdmiInfo != nullptr) + { + auto currentMode = hdmiInfo.GetCurrentDisplayMode(); + // On Xbox, 4K resolutions only are reported by HdmiDisplayInformation API + // so ScreenHeight & ScreenWidth are updated with info provided here + md.ScreenHeight = currentMode.ResolutionHeightInRawPixels(); + md.ScreenWidth = currentMode.ResolutionWidthInRawPixels(); + md.RefreshRate = currentMode.RefreshRate(); + md.Bpp = currentMode.BitsPerPixel(); + } + else + { + md.RefreshRate = 60.0; + md.Bpp = 24; + } + } + else + { + // note that refresh rate information is not available on Win10 UWP + md.RefreshRate = 60.0; + md.Bpp = 24; + } + md.Interlaced = false; + + outputs.push_back(md); + }); + + if (dispatcher.HasThreadAccess()) + handler(); + else + Wait(dispatcher.RunAsync(CoreDispatcherPriority::High, handler)); +} + +void CWinSystemWin10::ShowOSMouse(bool show) +{ + if (!m_coreWindow) + return; + + DispatchedHandler handler([this, show]() + { + CoreCursor cursor = nullptr; + if (show) + cursor = CoreCursor(CoreCursorType::Arrow, 1); + m_coreWindow.PointerCursor(cursor); + }); + + if (m_coreWindow.Dispatcher().HasThreadAccess()) + handler(); + else + m_coreWindow.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, handler); +} + +bool CWinSystemWin10::Minimize() +{ + CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__); + return true; +} +bool CWinSystemWin10::Restore() +{ + CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__); + return true; +} +bool CWinSystemWin10::Hide() +{ + CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__); + return true; +} +bool CWinSystemWin10::Show(bool raise) +{ + CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__); + return true; +} + +void CWinSystemWin10::Register(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemWin10::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +void CWinSystemWin10::OnDisplayLost() +{ + CLog::Log(LOGDEBUG, "{} - notify display lost event", __FUNCTION__); + + { + std::unique_lock<CCriticalSection> lock(m_resourceSection); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnLostDisplay(); + } +} + +void CWinSystemWin10::OnDisplayReset() +{ + if (!m_delayDispReset) + { + CLog::Log(LOGDEBUG, "{} - notify display reset event", __FUNCTION__); + std::unique_lock<CCriticalSection> lock(m_resourceSection); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); + } +} + +void CWinSystemWin10::OnDisplayBack() +{ + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + if (delay > 0ms) + { + m_delayDispReset = true; + m_dispResetTimer.Set(delay); + } + OnDisplayReset(); +} + +void CWinSystemWin10::ResolutionChanged() +{ + OnDisplayLost(); + OnDisplayBack(); +} + +std::unique_ptr<CVideoSync> CWinSystemWin10::GetVideoSync(void *clock) +{ + std::unique_ptr<CVideoSync> pVSync(new CVideoSyncD3D(clock)); + return pVSync; +} + +std::string CWinSystemWin10::GetClipboardText() +{ + std::wstring unicode_text; + + auto contentView = Clipboard::GetContent(); + if (contentView.Contains(StandardDataFormats::Text())) + { + auto text = Wait(contentView.GetTextAsync()); + unicode_text.append(text.c_str()); + } + + return KODI::PLATFORM::WINDOWS::FromW(unicode_text); +} + +bool CWinSystemWin10::UseLimitedColor() +{ + return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE); +} + +void CWinSystemWin10::NotifyAppFocusChange(bool bGaining) +{ + m_inFocus = bGaining; +} + +void CWinSystemWin10::UpdateStates(bool fullScreen) +{ + m_fullscreenState = WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW; // currently only this allowed + m_windowState = WINDOW_WINDOW_STATE_WINDOWED; // currently only this allowed +} + +WINDOW_STATE CWinSystemWin10::GetState(bool fullScreen) const +{ + return static_cast<WINDOW_STATE>(fullScreen ? m_fullscreenState : m_windowState); +} + +bool CWinSystemWin10::MessagePump() +{ + return m_winEvents->MessagePump(); +} + +int CWinSystemWin10::GetGuiSdrPeakLuminance() const +{ + const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + + return settings->GetInt(CSettings::SETTING_VIDEOSCREEN_GUISDRPEAKLUMINANCE); +} + +#pragma pack(pop) diff --git a/xbmc/windowing/win10/WinSystemWin10.h b/xbmc/windowing/win10/WinSystemWin10.h new file mode 100644 index 0000000..0f2b41e --- /dev/null +++ b/xbmc/windowing/win10/WinSystemWin10.h @@ -0,0 +1,156 @@ +/* + * 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 + +#include "guilib/DispResource.h" +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "windowing/WinSystem.h" + +#include <vector> + +#pragma pack(push) +#pragma pack(8) + +/* Controls the way the window appears and behaves. */ +enum WINDOW_STATE +{ + WINDOW_STATE_FULLSCREEN = 1, // Exclusive fullscreen + WINDOW_STATE_FULLSCREEN_WINDOW, // Non-exclusive fullscreen window + WINDOW_STATE_WINDOWED, //Movable window with border + WINDOW_STATE_BORDERLESS //Non-movable window with no border +}; + +static const char* window_state_names[] = +{ + "unknown", + "true fullscreen", + "windowed fullscreen", + "windowed", + "borderless" +}; + +/* WINDOW_STATE restricted to fullscreen modes. */ +enum WINDOW_FULLSCREEN_STATE +{ + WINDOW_FULLSCREEN_STATE_FULLSCREEN = WINDOW_STATE_FULLSCREEN, + WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW = WINDOW_STATE_FULLSCREEN_WINDOW +}; + +/* WINDOW_STATE restricted to windowed modes. */ +enum WINDOW_WINDOW_STATE +{ + WINDOW_WINDOW_STATE_WINDOWED = WINDOW_STATE_WINDOWED, + WINDOW_WINDOW_STATE_BORDERLESS = WINDOW_STATE_BORDERLESS +}; + +struct MONITOR_DETAILS +{ + // Windows desktop info + int ScreenWidth; + int ScreenHeight; + float RefreshRate; + int Bpp; + bool Interlaced; +}; + +class CWinSystemWin10 : public CWinSystemBase +{ +public: + CWinSystemWin10(); + virtual ~CWinSystemWin10(); + + // CWinSystemBase overrides + bool InitWindowSystem() override; + bool DestroyWindowSystem() override; + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + void FinishWindowResize(int newWidth, int newHeight) override; + void UpdateResolutions() override; + void NotifyAppFocusChange(bool bGaining) override; + void ShowOSMouse(bool show) override; + bool HasInertialGestures() override { return true; }//if win32 has touchscreen - it uses the win32 gesture api for inertial scrolling + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + std::string GetClipboardText() override; + bool UseLimitedColor() override; + + // videosync + std::unique_ptr<CVideoSync> GetVideoSync(void *clock) override; + + bool WindowedMode() const { return m_state != WINDOW_STATE_FULLSCREEN; } + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + + // CWinSystemWin10 + bool IsAlteringWindow() const { return m_IsAlteringWindow; } + void SetAlteringWindow(bool altering) { m_IsAlteringWindow = altering; } + bool IsTogglingHDR() const { return false; } + void SetTogglingHDR(bool toggling) {} + virtual bool DPIChanged(WORD dpi, RECT windowRect) const; + bool IsMinimized() const { return m_bMinimized; } + void SetMinimized(bool minimized) { m_bMinimized = minimized; } + int GetGuiSdrPeakLuminance() const; + + bool CanDoWindowed() override; + + // winevents override + bool MessagePump() override; + +protected: + bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override = 0; + virtual void UpdateStates(bool fullScreen); + WINDOW_STATE GetState(bool fullScreen) const; + virtual void SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) = 0; + virtual void ReleaseBackBuffer() = 0; + virtual void CreateBackBuffer() = 0; + virtual void ResizeDeviceBuffers() = 0; + virtual bool IsStereoEnabled() = 0; + virtual void AdjustWindow(); + + virtual void Register(IDispResource *resource); + virtual void Unregister(IDispResource *resource); + + bool ChangeResolution(const RESOLUTION_INFO& res, bool forceChange = false); + const MONITOR_DETAILS* GetDefaultMonitor() const; + void RestoreDesktopResolution(); + void GetConnectedDisplays(std::vector<MONITOR_DETAILS>& outputs); + + /*! + \brief Adds a resolution to the list of resolutions if we don't already have it + \param res resolution to add. + */ + static bool AddResolution(const RESOLUTION_INFO &res); + + void OnDisplayLost(); + void OnDisplayReset(); + void OnDisplayBack(); + void ResolutionChanged(); + + std::vector<MONITOR_DETAILS> m_displays; + bool m_ValidWindowedPosition; + bool m_IsAlteringWindow; + + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + bool m_delayDispReset; + XbmcThreads::EndTime<> m_dispResetTimer; + + WINDOW_STATE m_state; // the state of the window + WINDOW_FULLSCREEN_STATE m_fullscreenState; // the state of the window when in fullscreen + WINDOW_WINDOW_STATE m_windowState; // the state of the window when in windowed + bool m_inFocus; + bool m_bMinimized; + bool m_bFirstResChange = true; + + winrt::Windows::UI::Core::CoreWindow m_coreWindow = nullptr; +}; + +#pragma pack(pop) + diff --git a/xbmc/windowing/win10/WinSystemWin10DX.cpp b/xbmc/windowing/win10/WinSystemWin10DX.cpp new file mode 100644 index 0000000..c3d2fca --- /dev/null +++ b/xbmc/windowing/win10/WinSystemWin10DX.cpp @@ -0,0 +1,205 @@ +/* + * 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 "WinSystemWin10DX.h" + +#include "input/touch/generic/GenericTouchActionHandler.h" +#include "input/touch/generic/GenericTouchInputHandler.h" +#include "rendering/dx/DirectXHelper.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "windowing/WindowSystemFactory.h" + +#include "platform/win32/WIN32Util.h" + +using namespace std::chrono_literals; + +void CWinSystemWin10DX::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem); +} + +std::unique_ptr<CWinSystemBase> CWinSystemWin10DX::CreateWinSystem() +{ + return std::make_unique<CWinSystemWin10DX>(); +} + +CWinSystemWin10DX::CWinSystemWin10DX() : CRenderSystemDX() +{ +} + +CWinSystemWin10DX::~CWinSystemWin10DX() +{ +} + +void CWinSystemWin10DX::PresentRenderImpl(bool rendered) +{ + if (rendered) + m_deviceResources->Present(); + + if (m_delayDispReset && m_dispResetTimer.IsTimePast()) + { + m_delayDispReset = false; + OnDisplayReset(); + } + + if (!rendered) + KODI::TIME::Sleep(40ms); +} + +bool CWinSystemWin10DX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + const MONITOR_DETAILS* monitor = GetDefaultMonitor(); + if (!monitor) + return false; + + m_deviceResources = DX::DeviceResources::Get(); + m_deviceResources->SetWindow(m_coreWindow); + + if (CWinSystemWin10::CreateNewWindow(name, fullScreen, res) && m_deviceResources->HasValidDevice()) + { + CGenericTouchInputHandler::GetInstance().RegisterHandler(&CGenericTouchActionHandler::GetInstance()); + CGenericTouchInputHandler::GetInstance().SetScreenDPI(m_deviceResources->GetDpi()); + return true; + } + return false; +} + +bool CWinSystemWin10DX::DestroyRenderSystem() +{ + CRenderSystemDX::DestroyRenderSystem(); + + m_deviceResources->Release(); + m_deviceResources.reset(); + return true; +} + +void CWinSystemWin10DX::ShowSplash(const std::string & message) +{ + CRenderSystemBase::ShowSplash(message); + + // this will prevent killing the app by watchdog timeout during loading + if (m_coreWindow != nullptr) + m_coreWindow.Dispatcher().ProcessEvents(winrt::Windows::UI::Core::CoreProcessEventsOption::ProcessAllIfPresent); +} + +void CWinSystemWin10DX::SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) +{ + m_deviceResources->SetFullScreen(fullScreen, res); +} + +bool CWinSystemWin10DX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + CWinSystemWin10::ResizeWindow(newWidth, newHeight, newLeft, newTop); + CRenderSystemDX::OnResize(); + + return true; +} + +void CWinSystemWin10DX::OnMove(int x, int y) +{ + m_deviceResources->SetWindowPos(m_coreWindow.Bounds()); +} + +bool CWinSystemWin10DX::DPIChanged(WORD dpi, RECT windowRect) const +{ + m_deviceResources->SetDpi(dpi); + if (!IsAlteringWindow()) + return CWinSystemWin10::DPIChanged(dpi, windowRect); + + return true; +} + +void CWinSystemWin10DX::ReleaseBackBuffer() +{ + m_deviceResources->ReleaseBackBuffer(); +} + +void CWinSystemWin10DX::CreateBackBuffer() +{ + m_deviceResources->CreateBackBuffer(); +} + +void CWinSystemWin10DX::ResizeDeviceBuffers() +{ + m_deviceResources->ResizeBuffers(); +} + +bool CWinSystemWin10DX::IsStereoEnabled() +{ + return m_deviceResources->IsStereoEnabled(); +} + +void CWinSystemWin10DX::OnResize(int width, int height) +{ + if (!m_deviceResources) + return; + + if (!m_IsAlteringWindow) + ReleaseBackBuffer(); + + m_deviceResources->SetLogicalSize(width, height); + + if (!m_IsAlteringWindow) + CreateBackBuffer(); +} + +bool CWinSystemWin10DX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + bool const result = CWinSystemWin10::SetFullScreen(fullScreen, res, blankOtherDisplays); + CRenderSystemDX::OnResize(); + return result; +} + +void CWinSystemWin10DX::UninitHooks() +{ +} + +void CWinSystemWin10DX::InitHooks(IDXGIOutput* pOutput) +{ +} + +bool CWinSystemWin10DX::IsHDRDisplay() +{ + return false; // use tone mapping by default on Xbox +} + +HDR_STATUS CWinSystemWin10DX::GetOSHDRStatus() +{ + return CWIN32Util::GetWindowsHDRStatus(); +} + +HDR_STATUS CWinSystemWin10DX::ToggleHDR() +{ + return m_deviceResources->ToggleHDR(); +} + +bool CWinSystemWin10DX::IsHDROutput() const +{ + return m_deviceResources->IsHDROutput(); +} + +bool CWinSystemWin10DX::IsTransferPQ() const +{ + return m_deviceResources->IsTransferPQ(); +} + +void CWinSystemWin10DX::SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const +{ + m_deviceResources->SetHdrMetaData(hdr10); +} + +void CWinSystemWin10DX::SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace) const +{ + m_deviceResources->SetHdrColorSpace(colorSpace); +} + +DEBUG_INFO_RENDER CWinSystemWin10DX::GetDebugInfo() +{ + return m_deviceResources->GetDebugInfo(); +} diff --git a/xbmc/windowing/win10/WinSystemWin10DX.h b/xbmc/windowing/win10/WinSystemWin10DX.h new file mode 100644 index 0000000..84ad660 --- /dev/null +++ b/xbmc/windowing/win10/WinSystemWin10DX.h @@ -0,0 +1,90 @@ +/* + * 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 + +#include "HDRStatus.h" +#include "WinSystemWin10.h" +#include "rendering/dx/RenderSystemDX.h" + +class CWinSystemWin10DX : public CWinSystemWin10, public CRenderSystemDX +{ +public: + CWinSystemWin10DX(); + ~CWinSystemWin10DX(); + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystemBase via CWinSystemWin10 + CRenderSystemBase *GetRenderSystem() override { return this; } + bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override; + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + void PresentRenderImpl(bool rendered) override; + bool DPIChanged(WORD dpi, RECT windowRect) const override; + bool DestroyRenderSystem() override; + void* GetHWContext() override { return m_deviceResources->GetD3DContext(); } + + void UninitHooks(); + void InitHooks(IDXGIOutput* pOutput); + + void OnMove(int x, int y) override; + void OnResize(int width, int height); + winrt::Windows::Foundation::Size GetOutputSize() const { return m_deviceResources->GetOutputSize(); } + void TrimDevice() const { m_deviceResources->Trim(); } + + /*! + \brief Register as a dependent of the DirectX Render System + Resources should call this on construction if they're dependent on the Render System + for survival. Any resources that registers will get callbacks on loss and reset of + device. In addition, callbacks for destruction and creation of the device are also called, + where any resources dependent on the DirectX device should be destroyed and recreated. + \sa Unregister, ID3DResource + */ + void Register(ID3DResource *resource) const + { + m_deviceResources->Register(resource); + }; + /*! + \brief Unregister as a dependent of the DirectX Render System + Resources should call this on destruction if they're a dependent on the Render System + \sa Register, ID3DResource + */ + void Unregister(ID3DResource *resource) const + { + m_deviceResources->Unregister(resource); + }; + + void Register(IDispResource* resource) override { CWinSystemWin10::Register(resource); } + void Unregister(IDispResource* resource) override { CWinSystemWin10::Unregister(resource); } + + void ShowSplash(const std::string& message) override; + + // HDR OS/display override + bool IsHDRDisplay() override; + HDR_STATUS ToggleHDR() override; + HDR_STATUS GetOSHDRStatus() override; + + // HDR support + bool IsHDROutput() const; + bool IsTransferPQ() const; + void SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const; + void SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace) const; + + // Get debug info from swapchain + DEBUG_INFO_RENDER GetDebugInfo() override; + +protected: + void SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) override; + void ReleaseBackBuffer() override; + void CreateBackBuffer() override; + void ResizeDeviceBuffers() override; + bool IsStereoEnabled() override; +}; + diff --git a/xbmc/windowing/windows/CMakeLists.txt b/xbmc/windowing/windows/CMakeLists.txt new file mode 100644 index 0000000..2b84cee --- /dev/null +++ b/xbmc/windowing/windows/CMakeLists.txt @@ -0,0 +1,14 @@ +set(SOURCES VideoSyncD3D.cpp + Win32DPMSSupport.cpp + WinEventsWin32.cpp + WinSystemWin32.cpp + WinSystemWin32DX.cpp) + +set(HEADERS VideoSyncD3D.h + Win32DPMSSupport.h + WinEventsWin32.h + WinSystemWin32.h + WinSystemWin32DX.h + WinKeyMap.h) + +core_add_library(windowing_windows) diff --git a/xbmc/windowing/windows/VideoSyncD3D.cpp b/xbmc/windowing/windows/VideoSyncD3D.cpp new file mode 100644 index 0000000..d95374f --- /dev/null +++ b/xbmc/windowing/windows/VideoSyncD3D.cpp @@ -0,0 +1,137 @@ +/* + * 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 "VideoSyncD3D.h" + +#include "Utils/MathUtils.h" +#include "Utils/TimeUtils.h" +#include "rendering/dx/DeviceResources.h" +#include "rendering/dx/RenderContext.h" +#include "utils/StringUtils.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" + +#include <mutex> + +using namespace std::chrono_literals; + +void CVideoSyncD3D::OnLostDisplay() +{ + if (!m_displayLost) + { + m_displayLost = true; + m_lostEvent.Wait(); + } +} + +void CVideoSyncD3D::OnResetDisplay() +{ + m_displayReset = true; +} + +void CVideoSyncD3D::RefreshChanged() +{ + m_displayReset = true; +} + +bool CVideoSyncD3D::Setup(PUPDATECLOCK func) +{ + CLog::Log(LOGDEBUG, "CVideoSyncD3D: Setting up Direct3d"); + std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext()); + DX::Windowing()->Register(this); + m_displayLost = false; + m_displayReset = false; + m_lostEvent.Reset(); + UpdateClock = func; + + // we need a high priority thread to get accurate timing + if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL)) + CLog::Log(LOGDEBUG, "CVideoSyncD3D: SetThreadPriority failed"); + + return true; +} + +void CVideoSyncD3D::Run(CEvent& stopEvent) +{ + int64_t Now; + int64_t LastVBlankTime; + int NrVBlanks; + double VBlankTime; + int64_t systemFrequency = CurrentHostFrequency(); + + // init the vblanktime + Now = CurrentHostCounter(); + LastVBlankTime = Now; + m_lastUpdateTime = Now - systemFrequency; + while (!stopEvent.Signaled() && !m_displayLost && !m_displayReset) + { + // sleep until vblank + Microsoft::WRL::ComPtr<IDXGIOutput> pOutput; + DX::DeviceResources::Get()->GetOutput(&pOutput); + HRESULT hr = pOutput->WaitForVBlank(); + + // calculate how many vblanks happened + Now = CurrentHostCounter(); + VBlankTime = (double)(Now - LastVBlankTime) / (double)systemFrequency; + NrVBlanks = MathUtils::round_int(VBlankTime * m_fps); + + // update the vblank timestamp, update the clock and send a signal that we got a vblank + UpdateClock(NrVBlanks, Now, m_refClock); + + // save the timestamp of this vblank so we can calculate how many vblanks happened next time + LastVBlankTime = Now; + + if ((Now - m_lastUpdateTime) >= systemFrequency) + { + float fps = m_fps; + if (fps != GetFps()) + break; + } + + // because we had a vblank, sleep until half the refreshrate period because i think WaitForVBlank block any rendering stuf + // without sleeping we have freeze rendering + int SleepTime = (int)((LastVBlankTime + (systemFrequency / MathUtils::round_int(m_fps) / 2) - Now) * 1000 / systemFrequency); + if (SleepTime > 50) + SleepTime = 50; //failsafe + if (SleepTime > 0) + ::Sleep(SleepTime); + } + + m_lostEvent.Set(); + while (!stopEvent.Signaled() && m_displayLost && !m_displayReset) + { + KODI::TIME::Sleep(10ms); + } +} + +void CVideoSyncD3D::Cleanup() +{ + CLog::Log(LOGDEBUG, "CVideoSyncD3D: Cleaning up Direct3d"); + + m_lostEvent.Set(); + DX::Windowing()->Unregister(this); +} + +float CVideoSyncD3D::GetFps() +{ + DXGI_MODE_DESC DisplayMode = {}; + DX::DeviceResources::Get()->GetDisplayMode(&DisplayMode); + + m_fps = (DisplayMode.RefreshRate.Denominator != 0) ? (float)DisplayMode.RefreshRate.Numerator / (float)DisplayMode.RefreshRate.Denominator : 0.0f; + + if (m_fps == 0.0) + m_fps = 60.0f; + + if (DX::Windowing()->Interlaced()) + { + m_fps *= 2; + } + return m_fps; +} + diff --git a/xbmc/windowing/windows/VideoSyncD3D.h b/xbmc/windowing/windows/VideoSyncD3D.h new file mode 100644 index 0000000..e241650 --- /dev/null +++ b/xbmc/windowing/windows/VideoSyncD3D.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 + +#include "guilib/DispResource.h" +#include "threads/Event.h" +#include "windowing/VideoSync.h" + +class CVideoSyncD3D : public CVideoSync, IDispResource +{ +public: + CVideoSyncD3D(void* clock) + : CVideoSync(clock), m_displayLost(false), m_displayReset(false), m_lastUpdateTime(0) + { + } + bool Setup(PUPDATECLOCK func) override; + void Run(CEvent& stopEvent) override; + void Cleanup() override; + float GetFps() override; + void RefreshChanged() override; + // IDispResource overrides + void OnLostDisplay() override; + void OnResetDisplay() override; + +private: + volatile bool m_displayLost; + volatile bool m_displayReset; + CEvent m_lostEvent; + int64_t m_lastUpdateTime; +}; + diff --git a/xbmc/windowing/windows/Win32DPMSSupport.cpp b/xbmc/windowing/windows/Win32DPMSSupport.cpp new file mode 100644 index 0000000..99adc10 --- /dev/null +++ b/xbmc/windowing/windows/Win32DPMSSupport.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2009-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 "Win32DPMSSupport.h" + +#include "ServiceBroker.h" +#include "rendering/dx/RenderContext.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/windows/WinSystemWin32.h" + +CWin32DPMSSupport::CWin32DPMSSupport() +{ + m_supportedModes.push_back(OFF); + m_supportedModes.push_back(STANDBY); +} + +bool CWin32DPMSSupport::EnablePowerSaving(PowerSavingMode mode) +{ + auto winSystem = dynamic_cast<CWinSystemWin32*>(CServiceBroker::GetWinSystem()); + if (!winSystem) + return false; + + if (!winSystem->GetGfxContext().IsFullScreenRoot()) + { + CLog::Log(LOGDEBUG, "DPMS: not in fullscreen, power saving disabled"); + return false; + } + + switch (mode) + { + case OFF: + // Turn off display + return SendMessage(DX::Windowing()->GetHwnd(), WM_SYSCOMMAND, SC_MONITORPOWER, static_cast<LPARAM>(2)) == 0; + case STANDBY: + // Set display to low power + return SendMessage(DX::Windowing()->GetHwnd(), WM_SYSCOMMAND, SC_MONITORPOWER, static_cast<LPARAM>(1)) == 0; + default: + return true; + } +} + +bool CWin32DPMSSupport::DisablePowerSaving() +{ + // Turn display on + return SendMessage(DX::Windowing()->GetHwnd(), WM_SYSCOMMAND, SC_MONITORPOWER, static_cast<LPARAM>(-1)) == 0; +} diff --git a/xbmc/windowing/windows/Win32DPMSSupport.h b/xbmc/windowing/windows/Win32DPMSSupport.h new file mode 100644 index 0000000..75c192f --- /dev/null +++ b/xbmc/windowing/windows/Win32DPMSSupport.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009-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 "xbmc/powermanagement/DPMSSupport.h" + +class CWin32DPMSSupport : public CDPMSSupport +{ +public: + CWin32DPMSSupport(); + ~CWin32DPMSSupport() = default; + +protected: + bool EnablePowerSaving(PowerSavingMode mode) override; + bool DisablePowerSaving() override; +}; diff --git a/xbmc/windowing/windows/WinEventsWin32.cpp b/xbmc/windowing/windows/WinEventsWin32.cpp new file mode 100644 index 0000000..7b4cba3 --- /dev/null +++ b/xbmc/windowing/windows/WinEventsWin32.cpp @@ -0,0 +1,1074 @@ +/* + * 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. + */ + +#ifndef _USE_MATH_DEFINES +#define _USE_MATH_DEFINES +#endif +#include "WinEventsWin32.h" + +#include "ServiceBroker.h" +#include "Util.h" +#include "WinKeyMap.h" +#include "application/AppInboundProtocol.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPowerHandling.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIControl.h" // for EVENT_RESULT +#include "guilib/GUIWindowManager.h" +#include "input/InputManager.h" +#include "input/mouse/MouseStat.h" +#include "input/touch/generic/GenericTouchActionHandler.h" +#include "input/touch/generic/GenericTouchSwipeDetector.h" +#include "messaging/ApplicationMessenger.h" +#include "network/Zeroconf.h" +#include "network/ZeroconfBrowser.h" +#include "peripherals/Peripherals.h" +#include "rendering/dx/RenderContext.h" +#include "storage/MediaManager.h" +#include "utils/JobManager.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include "platform/win32/CharsetConverter.h" +#include "platform/win32/WIN32Util.h" +#include "platform/win32/powermanagement/Win32PowerSyscall.h" +#include "platform/win32/storage/Win32StorageProvider.h" + +#include <array> +#include <math.h> + +#include <Shlobj.h> +#include <dbt.h> + +HWND g_hWnd = nullptr; + +#ifndef LODWORD +#define LODWORD(longval) ((DWORD)((DWORDLONG)(longval))) +#endif + +#define ROTATE_ANGLE_DEGREE(arg) GID_ROTATE_ANGLE_FROM_ARGUMENT(LODWORD(arg)) * 180 / M_PI + +#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp)) +#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp)) + +/* Masks for processing the windows KEYDOWN and KEYUP messages */ +#define REPEATED_KEYMASK (1<<30) +#define EXTENDED_KEYMASK (1<<24) +#define EXTKEYPAD(keypad) ((scancode & 0x100)?(mvke):(keypad)) + +static GUID USB_HID_GUID = { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } }; + +uint32_t g_uQueryCancelAutoPlay = 0; +bool g_sizeMoveSizing = false; +bool g_sizeMoveMoving = false; +int g_sizeMoveWidth = 0; +int g_sizeMoveHight = 0; +int g_sizeMoveX = -10000; +int g_sizeMoveY = -10000; + +int XBMC_TranslateUNICODE = 1; + +int CWinEventsWin32::m_originalZoomDistance = 0; +Pointer CWinEventsWin32::m_touchPointer; +CGenericTouchSwipeDetector* CWinEventsWin32::m_touchSwipeDetector = nullptr; + +// register to receive SD card events (insert/remove) +// seen at http://www.codeproject.com/Messages/2897423/Re-No-message-triggered-on-SD-card-insertion-remov.aspx +#define WM_MEDIA_CHANGE (WM_USER + 666) +SHChangeNotifyEntry shcne; + +static int XBMC_MapVirtualKey(int scancode, WPARAM vkey) +{ + int mvke = MapVirtualKeyEx(scancode & 0xFF, 1, nullptr); + + switch (vkey) + { /* These are always correct */ + case VK_DIVIDE: + case VK_MULTIPLY: + case VK_SUBTRACT: + case VK_ADD: + case VK_LWIN: + case VK_RWIN: + case VK_APPS: + /* These are already handled */ + case VK_LCONTROL: + case VK_RCONTROL: + case VK_LSHIFT: + case VK_RSHIFT: + case VK_LMENU: + case VK_RMENU: + case VK_SNAPSHOT: + case VK_PAUSE: + /* Multimedia keys are already handled */ + case VK_BROWSER_BACK: + case VK_BROWSER_FORWARD: + case VK_BROWSER_REFRESH: + case VK_BROWSER_STOP: + case VK_BROWSER_SEARCH: + case VK_BROWSER_FAVORITES: + case VK_BROWSER_HOME: + case VK_VOLUME_MUTE: + case VK_VOLUME_DOWN: + case VK_VOLUME_UP: + case VK_MEDIA_NEXT_TRACK: + case VK_MEDIA_PREV_TRACK: + case VK_MEDIA_STOP: + case VK_MEDIA_PLAY_PAUSE: + case VK_LAUNCH_MAIL: + case VK_LAUNCH_MEDIA_SELECT: + case VK_LAUNCH_APP1: + case VK_LAUNCH_APP2: + return static_cast<int>(vkey); + default:; + } + switch (mvke) + { /* Distinguish between keypad and extended keys */ + case VK_INSERT: return EXTKEYPAD(VK_NUMPAD0); + case VK_DELETE: return EXTKEYPAD(VK_DECIMAL); + case VK_END: return EXTKEYPAD(VK_NUMPAD1); + case VK_DOWN: return EXTKEYPAD(VK_NUMPAD2); + case VK_NEXT: return EXTKEYPAD(VK_NUMPAD3); + case VK_LEFT: return EXTKEYPAD(VK_NUMPAD4); + case VK_CLEAR: return EXTKEYPAD(VK_NUMPAD5); + case VK_RIGHT: return EXTKEYPAD(VK_NUMPAD6); + case VK_HOME: return EXTKEYPAD(VK_NUMPAD7); + case VK_UP: return EXTKEYPAD(VK_NUMPAD8); + case VK_PRIOR: return EXTKEYPAD(VK_NUMPAD9); + default:; + } + return mvke ? mvke : static_cast<int>(vkey); +} + + +static XBMC_keysym *TranslateKey(WPARAM vkey, UINT scancode, XBMC_keysym *keysym, int pressed) +{ + using namespace KODI::WINDOWING::WINDOWS; + + uint8_t keystate[256]; + + /* Set the keysym information */ + keysym->scancode = static_cast<unsigned char>(scancode); + keysym->unicode = 0; + + if ((vkey == VK_RETURN) && (scancode & 0x100)) + { + /* No VK_ code for the keypad enter key */ + keysym->sym = XBMCK_KP_ENTER; + } + else + { + keysym->sym = VK_keymap[XBMC_MapVirtualKey(scancode, vkey)]; + } + + // Attempt to convert the keypress to a UNICODE character + GetKeyboardState(keystate); + + if (pressed && XBMC_TranslateUNICODE) + { + std::array<uint16_t, 2> wchars; + + /* Numlock isn't taken into account in ToUnicode, + * so we handle it as a special case here */ + if ((keystate[VK_NUMLOCK] & 1) && vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9) + { + keysym->unicode = static_cast<uint16_t>(vkey - VK_NUMPAD0 + '0'); + } + else if (ToUnicode(static_cast<UINT>(vkey), scancode, keystate, + reinterpret_cast<LPWSTR>(wchars.data()), wchars.size(), 0) > 0) + { + keysym->unicode = wchars[0]; + } + } + + // Set the modifier bitmap + + uint16_t mod = static_cast<uint16_t>(XBMCKMOD_NONE); + + // If left control and right alt are down this usually means that + // AltGr is down + if ((keystate[VK_LCONTROL] & 0x80) && (keystate[VK_RMENU] & 0x80)) + { + mod |= XBMCKMOD_MODE; + } + else + { + if (keystate[VK_LCONTROL] & 0x80) mod |= XBMCKMOD_LCTRL; + if (keystate[VK_RMENU] & 0x80) mod |= XBMCKMOD_RALT; + } + + // Check the remaining modifiers + if (keystate[VK_LSHIFT] & 0x80) mod |= XBMCKMOD_LSHIFT; + if (keystate[VK_RSHIFT] & 0x80) mod |= XBMCKMOD_RSHIFT; + if (keystate[VK_RCONTROL] & 0x80) mod |= XBMCKMOD_RCTRL; + if (keystate[VK_LMENU] & 0x80) mod |= XBMCKMOD_LALT; + if (keystate[VK_LWIN] & 0x80) mod |= XBMCKMOD_LSUPER; + if (keystate[VK_RWIN] & 0x80) mod |= XBMCKMOD_LSUPER; + keysym->mod = static_cast<XBMCMod>(mod); + + // Return the updated keysym + return(keysym); +} + + +bool CWinEventsWin32::MessagePump() +{ + MSG msg; + while( PeekMessage( &msg, nullptr, 0U, 0U, PM_REMOVE ) ) + { + TranslateMessage( &msg ); + DispatchMessage( &msg ); + } + return true; +} + +LRESULT CALLBACK CWinEventsWin32::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + using KODI::PLATFORM::WINDOWS::FromW; + + XBMC_Event newEvent = {}; + static HDEVNOTIFY hDeviceNotify; + +#if 0 + if (uMsg == WM_NCCREATE) + { + // if available, enable DPI scaling of non-client portion of window (title bar, etc.) + if (g_Windowing.PtrEnableNonClientDpiScaling != NULL) + { + g_Windowing.PtrEnableNonClientDpiScaling(hWnd); + } + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } +#endif + + if (uMsg == WM_CREATE) + { + g_hWnd = hWnd; + // need to set windows handle before WM_SIZE processing + DX::Windowing()->SetWindow(hWnd); + + KODI::WINDOWING::WINDOWS::DIB_InitOSKeymap(); + + g_uQueryCancelAutoPlay = RegisterWindowMessage(TEXT("QueryCancelAutoPlay")); + shcne.pidl = nullptr; + shcne.fRecursive = TRUE; + long fEvents = SHCNE_DRIVEADD | SHCNE_DRIVEREMOVED | SHCNE_MEDIAREMOVED | SHCNE_MEDIAINSERTED; + SHChangeNotifyRegister(hWnd, SHCNRF_ShellLevel | SHCNRF_NewDelivery, fEvents, WM_MEDIA_CHANGE, 1, &shcne); + RegisterDeviceInterfaceToHwnd(USB_HID_GUID, hWnd, &hDeviceNotify); + return 0; + } + + if (uMsg == WM_DESTROY) + g_hWnd = nullptr; + + if(g_uQueryCancelAutoPlay != 0 && uMsg == g_uQueryCancelAutoPlay) + return S_FALSE; + + std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort(); + + switch (uMsg) + { + case WM_CLOSE: + case WM_QUIT: + case WM_DESTROY: + if (hDeviceNotify) + { + if (UnregisterDeviceNotification(hDeviceNotify)) + hDeviceNotify = nullptr; + else + CLog::LogF(LOGINFO, "UnregisterDeviceNotification failed ({})", GetLastError()); + } + newEvent.type = XBMC_QUIT; + if (appPort) + appPort->OnEvent(newEvent); + break; + case WM_SHOWWINDOW: + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + bool active = appPower->GetRenderGUI(); + if (appPort) + appPort->SetRenderGUI(wParam != 0); + if (appPower->GetRenderGUI() != active) + DX::Windowing()->NotifyAppActiveChange(appPower->GetRenderGUI()); + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "WM_SHOWWINDOW -> window is {}", + wParam != 0 ? "shown" : "hidden"); + } + break; + case WM_ACTIVATE: + { + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "WM_ACTIVATE -> window is {}", + LOWORD(wParam) != WA_INACTIVE ? "active" : "inactive"); + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + bool active = appPower->GetRenderGUI(); + if (HIWORD(wParam)) + { + if (appPort) + appPort->SetRenderGUI(false); + } + else + { + WINDOWPLACEMENT lpwndpl; + lpwndpl.length = sizeof(lpwndpl); + if (LOWORD(wParam) != WA_INACTIVE) + { + if (GetWindowPlacement(hWnd, &lpwndpl)) + { + if (appPort) + appPort->SetRenderGUI(lpwndpl.showCmd != SW_HIDE); + } + } + else + { + + } + } + if (appPower->GetRenderGUI() != active) + DX::Windowing()->NotifyAppActiveChange(appPower->GetRenderGUI()); + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "window is {}", + appPower->GetRenderGUI() ? "active" : "inactive"); + } + break; + case WM_SETFOCUS: + case WM_KILLFOCUS: + g_application.m_AppFocused = uMsg == WM_SETFOCUS; + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "window focus {}", + g_application.m_AppFocused ? "set" : "lost"); + + DX::Windowing()->NotifyAppFocusChange(g_application.m_AppFocused); + if (uMsg == WM_KILLFOCUS) + { + std::string procfile; + if (CWIN32Util::GetFocussedProcess(procfile)) + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "Focus switched to process {}", procfile); + } + break; + /* needs to be reviewed after frodo. we reset the system idle timer + and the display timer directly now (see m_screenSaverTimer). + case WM_SYSCOMMAND: + switch( wParam&0xFFF0 ) + { + case SC_MONITORPOWER: + if (g_application.GetAppPlayer().IsPlaying() || g_application.GetAppPlayer().IsPausedPlayback()) + return 0; + else if(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_DISPLAYSOFF) == 0) + return 0; + break; + case SC_SCREENSAVE: + return 0; + } + break;*/ + case WM_SYSKEYDOWN: + switch (wParam) + { + case VK_F4: //alt-f4, default event quit. + return(DefWindowProc(hWnd, uMsg, wParam, lParam)); + case VK_RETURN: //alt-return + if ((lParam & REPEATED_KEYMASK) == 0) + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN); + return 0; + default:; + } + //deliberate fallthrough + case WM_KEYDOWN: + { + switch (wParam) + { + case VK_CONTROL: + if ( lParam & EXTENDED_KEYMASK ) + wParam = VK_RCONTROL; + else + wParam = VK_LCONTROL; + break; + case VK_SHIFT: + /* EXTENDED trick doesn't work here */ + if (GetKeyState(VK_LSHIFT) & 0x8000) + wParam = VK_LSHIFT; + else if (GetKeyState(VK_RSHIFT) & 0x8000) + wParam = VK_RSHIFT; + break; + case VK_MENU: + if ( lParam & EXTENDED_KEYMASK ) + wParam = VK_RMENU; + else + wParam = VK_LMENU; + break; + default:; + } + XBMC_keysym keysym; + TranslateKey(wParam, HIWORD(lParam), &keysym, 1); + + newEvent.type = XBMC_KEYDOWN; + newEvent.key.keysym = keysym; + if (appPort) + appPort->OnEvent(newEvent); + } + return(0); + + case WM_SYSKEYUP: + case WM_KEYUP: + { + switch (wParam) + { + case VK_CONTROL: + if ( lParam&EXTENDED_KEYMASK ) + wParam = VK_RCONTROL; + else + wParam = VK_LCONTROL; + break; + case VK_SHIFT: + { + uint32_t scanCodeL = MapVirtualKey(VK_LSHIFT, MAPVK_VK_TO_VSC); + uint32_t scanCodeR = MapVirtualKey(VK_RSHIFT, MAPVK_VK_TO_VSC); + uint32_t keyCode = static_cast<uint32_t>((lParam & 0xFF0000) >> 16); + if (keyCode == scanCodeL) + wParam = VK_LSHIFT; + else if (keyCode == scanCodeR) + wParam = VK_RSHIFT; + } + break; + case VK_MENU: + if ( lParam&EXTENDED_KEYMASK ) + wParam = VK_RMENU; + else + wParam = VK_LMENU; + break; + default:; + } + XBMC_keysym keysym; + TranslateKey(wParam, HIWORD(lParam), &keysym, 1); + + if (wParam == VK_SNAPSHOT) + newEvent.type = XBMC_KEYDOWN; + else + newEvent.type = XBMC_KEYUP; + newEvent.key.keysym = keysym; + if (appPort) + appPort->OnEvent(newEvent); + } + return(0); + case WM_APPCOMMAND: // MULTIMEDIA keys are mapped to APPCOMMANDS + { + const unsigned int appcmd = GET_APPCOMMAND_LPARAM(lParam); + + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "APPCOMMAND {}", appcmd); + + // Reset the screen saver + auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + appPower->ResetScreenSaver(); + + // If we were currently in the screen saver wake up and don't process the + // appcommand + if (appPower->WakeUpScreenSaverAndDPMS()) + return true; + + // Retrieve the action associated with this appcommand from the mapping table + CKey key(appcmd | KEY_APPCOMMAND, 0U); + int iWin = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(); + + CAction appcmdaction = CServiceBroker::GetInputManager().GetAction(iWin, key); + if (appcmdaction.GetID()) + { + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "appcommand {}, action {}", appcmd, + appcmdaction.GetName()); + CServiceBroker::GetInputManager().QueueAction(appcmdaction); + return true; + } + + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "unknown appcommand {}", appcmd); + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } + case WM_GESTURENOTIFY: + { + OnGestureNotify(hWnd, lParam); + return DefWindowProc(hWnd, WM_GESTURENOTIFY, wParam, lParam); + } + case WM_GESTURE: + { + OnGesture(hWnd, lParam); + return 0; + } + case WM_SYSCHAR: + if (wParam == VK_RETURN) //stop system beep on alt-return + return 0; + break; + case WM_SETCURSOR: + if (HTCLIENT != LOWORD(lParam)) + DX::Windowing()->ShowOSMouse(true); + break; + case WM_MOUSEMOVE: + newEvent.type = XBMC_MOUSEMOTION; + newEvent.motion.x = GET_X_LPARAM(lParam); + newEvent.motion.y = GET_Y_LPARAM(lParam); + if (appPort) + appPort->OnEvent(newEvent); + return(0); + case WM_LBUTTONDOWN: + case WM_MBUTTONDOWN: + case WM_RBUTTONDOWN: + newEvent.type = XBMC_MOUSEBUTTONDOWN; + newEvent.button.x = GET_X_LPARAM(lParam); + newEvent.button.y = GET_Y_LPARAM(lParam); + newEvent.button.button = 0; + if (uMsg == WM_LBUTTONDOWN) newEvent.button.button = XBMC_BUTTON_LEFT; + else if (uMsg == WM_MBUTTONDOWN) newEvent.button.button = XBMC_BUTTON_MIDDLE; + else if (uMsg == WM_RBUTTONDOWN) newEvent.button.button = XBMC_BUTTON_RIGHT; + if (appPort) + appPort->OnEvent(newEvent); + return(0); + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + newEvent.type = XBMC_MOUSEBUTTONUP; + newEvent.button.x = GET_X_LPARAM(lParam); + newEvent.button.y = GET_Y_LPARAM(lParam); + newEvent.button.button = 0; + if (uMsg == WM_LBUTTONUP) newEvent.button.button = XBMC_BUTTON_LEFT; + else if (uMsg == WM_MBUTTONUP) newEvent.button.button = XBMC_BUTTON_MIDDLE; + else if (uMsg == WM_RBUTTONUP) newEvent.button.button = XBMC_BUTTON_RIGHT; + if (appPort) + appPort->OnEvent(newEvent); + return(0); + case WM_MOUSEWHEEL: + { + // SDL, which our events system is based off, sends a MOUSEBUTTONDOWN message + // followed by a MOUSEBUTTONUP message. As this is a momentary event, we just + // react on the MOUSEBUTTONUP message, resetting the state after processing. + newEvent.type = XBMC_MOUSEBUTTONDOWN; + // the coordinates in WM_MOUSEWHEEL are screen, not client coordinates + POINT point; + point.x = GET_X_LPARAM(lParam); + point.y = GET_Y_LPARAM(lParam); + WindowFromScreenCoords(hWnd, &point); + newEvent.button.x = static_cast<uint16_t>(point.x); + newEvent.button.y = static_cast<uint16_t>(point.y); + newEvent.button.button = GET_Y_LPARAM(wParam) > 0 ? XBMC_BUTTON_WHEELUP : XBMC_BUTTON_WHEELDOWN; + if (appPort) + { + appPort->OnEvent(newEvent); + newEvent.type = XBMC_MOUSEBUTTONUP; + appPort->OnEvent(newEvent); + } + } + return(0); + case WM_DPICHANGED: + // This message tells the program that most of its window is on a + // monitor with a new DPI. The wParam contains the new DPI, and the + // lParam contains a rect which defines the window rectangle scaled + // the new DPI. + { + // get the suggested size of the window on the new display with a different DPI + uint16_t dpi = HIWORD(wParam); + RECT rc = *reinterpret_cast<RECT*>(lParam); + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "dpi changed event -> {} ({}, {}, {}, {})", dpi, rc.left, + rc.top, rc.right, rc.bottom); + DX::Windowing()->DPIChanged(dpi, rc); + return(0); + } + case WM_DISPLAYCHANGE: + { + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "display change event"); + if (DX::Windowing()->IsTogglingHDR() || DX::Windowing()->IsAlteringWindow()) + return (0); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (appPower->GetRenderGUI() && GET_X_LPARAM(lParam) > 0 && GET_Y_LPARAM(lParam) > 0) + { + DX::Windowing()->UpdateResolutions(); + } + return(0); + } + case WM_ENTERSIZEMOVE: + { + DX::Windowing()->SetSizeMoveMode(true); + } + return(0); + case WM_EXITSIZEMOVE: + { + DX::Windowing()->SetSizeMoveMode(false); + if (g_sizeMoveMoving) + { + g_sizeMoveMoving = false; + newEvent.type = XBMC_VIDEOMOVE; + newEvent.move.x = g_sizeMoveX; + newEvent.move.y = g_sizeMoveY; + + // tell the device about new position + DX::Windowing()->OnMove(newEvent.move.x, newEvent.move.y); + // tell the application about new position + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (appPower->GetRenderGUI() && !DX::Windowing()->IsAlteringWindow()) + { + if (appPort) + appPort->OnEvent(newEvent); + } + } + if (g_sizeMoveSizing) + { + g_sizeMoveSizing = false; + newEvent.type = XBMC_VIDEORESIZE; + newEvent.resize.w = g_sizeMoveWidth; + newEvent.resize.h = g_sizeMoveHight; + + // tell the device about new size + DX::Windowing()->OnResize(newEvent.resize.w, newEvent.resize.h); + // tell the application about new size + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (appPower->GetRenderGUI() && !DX::Windowing()->IsAlteringWindow() && + newEvent.resize.w > 0 && newEvent.resize.h > 0) + { + if (appPort) + appPort->OnEvent(newEvent); + } + } + } + return(0); + case WM_SIZE: + if (wParam == SIZE_MINIMIZED) + { + if (!DX::Windowing()->IsMinimized()) + { + DX::Windowing()->SetMinimized(true); + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (appPower->GetRenderGUI()) + { + if (appPort) + appPort->SetRenderGUI(false); + } + } + } + else if (DX::Windowing()->IsMinimized()) + { + DX::Windowing()->SetMinimized(false); + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (!appPower->GetRenderGUI()) + { + if (appPort) + appPort->SetRenderGUI(true); + } + } + else + { + g_sizeMoveWidth = GET_X_LPARAM(lParam); + g_sizeMoveHight = GET_Y_LPARAM(lParam); + if (DX::Windowing()->IsInSizeMoveMode()) + { + // If an user is dragging the resize bars, we don't resize + // the buffers and don't rise XBMC_VIDEORESIZE here because + // as the user continuously resize the window, a lot of WM_SIZE + // messages are sent to the proc, and it'd be pointless (and slow) + // to resize for each WM_SIZE message received from dragging. + // So instead, we reset after the user is done resizing the + // window and releases the resize bars, which ends with WM_EXITSIZEMOVE. + g_sizeMoveSizing = true; + } + else + { + // API call such as SetWindowPos or SwapChain->SetFullscreenState + newEvent.type = XBMC_VIDEORESIZE; + newEvent.resize.w = g_sizeMoveWidth; + newEvent.resize.h = g_sizeMoveHight; + + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "window resize event {} x {}", newEvent.resize.w, + newEvent.resize.h); + // tell device about new size + DX::Windowing()->OnResize(newEvent.resize.w, newEvent.resize.h); + // tell application about size changes + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (appPower->GetRenderGUI() && !DX::Windowing()->IsAlteringWindow() && + newEvent.resize.w > 0 && newEvent.resize.h > 0) + { + if (appPort) + appPort->OnEvent(newEvent); + } + } + } + return(0); + case WM_MOVE: + { + g_sizeMoveX = GET_X_LPARAM(lParam); + g_sizeMoveY = GET_Y_LPARAM(lParam); + if (DX::Windowing()->IsInSizeMoveMode()) + { + // the same as WM_SIZE + g_sizeMoveMoving = true; + } + else + { + newEvent.type = XBMC_VIDEOMOVE; + newEvent.move.x = g_sizeMoveX; + newEvent.move.y = g_sizeMoveY; + + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "window move event"); + + // tell the device about new position + DX::Windowing()->OnMove(newEvent.move.x, newEvent.move.y); + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (appPower->GetRenderGUI() && !DX::Windowing()->IsAlteringWindow()) + { + if (appPort) + appPort->OnEvent(newEvent); + } + } + } + return(0); + case WM_MEDIA_CHANGE: + { + // This event detects media changes of usb, sd card and optical media. + // It only works if the explorer.exe process is started. Because this + // isn't the case for all setups we use WM_DEVICECHANGE for usb and + // optical media because this event is also triggered without the + // explorer process. Since WM_DEVICECHANGE doesn't detect sd card changes + // we still use this event only for sd. + long lEvent; + PIDLIST_ABSOLUTE *ppidl; + HANDLE hLock = SHChangeNotification_Lock(reinterpret_cast<HANDLE>(wParam), static_cast<DWORD>(lParam), &ppidl, &lEvent); + + if (hLock) + { + wchar_t drivePath[MAX_PATH+1]; + if (!SHGetPathFromIDList(ppidl[0], drivePath)) + break; + + switch(lEvent) + { + case SHCNE_DRIVEADD: + case SHCNE_MEDIAINSERTED: + if (GetDriveType(drivePath) != DRIVE_CDROM) + { + CLog::LogF(LOGDEBUG, "Drive {} Media has arrived.", FromW(drivePath)); + CWin32StorageProvider::SetEvent(); + } + break; + + case SHCNE_DRIVEREMOVED: + case SHCNE_MEDIAREMOVED: + if (GetDriveType(drivePath) != DRIVE_CDROM) + { + CLog::LogF(LOGDEBUG, "Drive {} Media was removed.", FromW(drivePath)); + CWin32StorageProvider::SetEvent(); + } + break; + default:; + } + SHChangeNotification_Unlock(hLock); + } + break; + } + case WM_POWERBROADCAST: + if (wParam==PBT_APMSUSPEND) + { + CLog::Log(LOGDEBUG,"WM_POWERBROADCAST: PBT_APMSUSPEND event was sent"); + CWin32PowerSyscall::SetOnSuspend(); + } + else if(wParam==PBT_APMRESUMEAUTOMATIC) + { + CLog::Log(LOGDEBUG,"WM_POWERBROADCAST: PBT_APMRESUMEAUTOMATIC event was sent"); + CWin32PowerSyscall::SetOnResume(); + } + break; + case WM_DEVICECHANGE: + { + switch(wParam) + { + case DBT_DEVNODES_CHANGED: + CServiceBroker::GetPeripherals().TriggerDeviceScan(PERIPHERALS::PERIPHERAL_BUS_USB); + break; + case DBT_DEVICEARRIVAL: + case DBT_DEVICEREMOVECOMPLETE: + if (((_DEV_BROADCAST_HEADER*) lParam)->dbcd_devicetype == DBT_DEVTYP_DEVICEINTERFACE) + { + CServiceBroker::GetPeripherals().TriggerDeviceScan(PERIPHERALS::PERIPHERAL_BUS_USB); + } + // check if an usb or optical media was inserted or removed + if (((_DEV_BROADCAST_HEADER*) lParam)->dbcd_devicetype == DBT_DEVTYP_VOLUME) + { + PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)((_DEV_BROADCAST_HEADER*) lParam); + // optical medium + if (lpdbv -> dbcv_flags & DBTF_MEDIA) + { + std::string strdrive = StringUtils::Format( + "{}:", CWIN32Util::FirstDriveFromMask(lpdbv->dbcv_unitmask)); + if(wParam == DBT_DEVICEARRIVAL) + { + CLog::LogF(LOGDEBUG, "Drive {} Media has arrived.", strdrive); + CServiceBroker::GetJobManager()->AddJob(new CDetectDisc(strdrive, true), nullptr); + } + else + { + CLog::LogF(LOGDEBUG, "Drive {} Media was removed.", strdrive); + CMediaSource share; + share.strPath = strdrive; + share.strName = share.strPath; + CServiceBroker::GetMediaManager().RemoveAutoSource(share); + } + } + else + CWin32StorageProvider::SetEvent(); + } + default:; + } + break; + } + case WM_PAINT: + { + //some other app has painted over our window, mark everything as dirty + CGUIComponent* component = CServiceBroker::GetGUI(); + if (component) + component->GetWindowManager().MarkDirty(); + break; + } + case BONJOUR_EVENT: + CZeroconf::GetInstance()->ProcessResults(); + break; + case BONJOUR_BROWSER_EVENT: + CZeroconfBrowser::GetInstance()->ProcessResults(); + break; + case TRAY_ICON_NOTIFY: + { + switch (LOWORD(lParam)) + { + case WM_LBUTTONDBLCLK: + { + DX::Windowing()->SetMinimized(false); + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (!appPower->GetRenderGUI()) + { + if (appPort) + appPort->SetRenderGUI(true); + } + break; + } + } + break; + } + case WM_TIMER: + { + if (wParam == ID_TIMER_HDR) + { + CLog::LogFC(LOGDEBUG, LOGWINDOWING, "finish toggling HDR event"); + DX::Windowing()->SetTogglingHDR(false); + KillTimer(hWnd, wParam); + } + break; + } + case WM_INITMENU: + { + HMENU hm = GetSystemMenu(hWnd, FALSE); + if (hm) + { + if (DX::Windowing()->IsFullScreen()) + EnableMenuItem(hm, SC_MOVE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); + else + EnableMenuItem(hm, SC_MOVE, MF_BYCOMMAND | MF_ENABLED); + } + break; + } + default:; + } + return(DefWindowProc(hWnd, uMsg, wParam, lParam)); +} + +void CWinEventsWin32::RegisterDeviceInterfaceToHwnd(GUID InterfaceClassGuid, HWND hWnd, HDEVNOTIFY *hDeviceNotify) +{ + DEV_BROADCAST_DEVICEINTERFACE NotificationFilter = {}; + + NotificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE); + NotificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; + NotificationFilter.dbcc_classguid = InterfaceClassGuid; + + *hDeviceNotify = RegisterDeviceNotification( + hWnd, // events recipient + &NotificationFilter, // type of device + DEVICE_NOTIFY_WINDOW_HANDLE // type of recipient handle + ); +} + +void CWinEventsWin32::WindowFromScreenCoords(HWND hWnd, POINT *point) +{ + if (!point) return; + RECT clientRect; + GetClientRect(hWnd, &clientRect); + POINT windowPos; + windowPos.x = clientRect.left; + windowPos.y = clientRect.top; + ClientToScreen(hWnd, &windowPos); + point->x -= windowPos.x; + point->y -= windowPos.y; +} + +void CWinEventsWin32::OnGestureNotify(HWND hWnd, LPARAM lParam) +{ + // convert to window coordinates + PGESTURENOTIFYSTRUCT gn = reinterpret_cast<PGESTURENOTIFYSTRUCT>(lParam); + POINT point = { gn->ptsLocation.x, gn->ptsLocation.y }; + WindowFromScreenCoords(hWnd, &point); + + // by default we only want twofingertap and pressandtap gestures + // the other gestures are enabled best on supported gestures + GESTURECONFIG gc[] = {{ GID_ZOOM, 0, GC_ZOOM}, + { GID_ROTATE, 0, GC_ROTATE}, + { GID_PAN, 0, GC_PAN}, + { GID_TWOFINGERTAP, GC_TWOFINGERTAP, GC_TWOFINGERTAP }, + { GID_PRESSANDTAP, GC_PRESSANDTAP, GC_PRESSANDTAP }}; + + // send a message to see if a control wants any + int gestures; + if ((gestures = CGenericTouchActionHandler::GetInstance().QuerySupportedGestures(static_cast<float>(point.x), static_cast<float>(point.y))) != EVENT_RESULT_UNHANDLED) + { + if (gestures & EVENT_RESULT_ZOOM) + gc[0].dwWant |= GC_ZOOM; + if (gestures & EVENT_RESULT_ROTATE) + gc[1].dwWant |= GC_ROTATE; + if (gestures & EVENT_RESULT_PAN_VERTICAL) + gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | GC_PAN_WITH_GUTTER | GC_PAN_WITH_INERTIA; + if (gestures & EVENT_RESULT_PAN_VERTICAL_WITHOUT_INERTIA) + gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_VERTICALLY; + if (gestures & EVENT_RESULT_PAN_HORIZONTAL) + gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY | GC_PAN_WITH_GUTTER | GC_PAN_WITH_INERTIA; + if (gestures & EVENT_RESULT_PAN_HORIZONTAL_WITHOUT_INERTIA) + gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY; + if (gestures & EVENT_RESULT_SWIPE) + { + gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY | GC_PAN_WITH_GUTTER; + + // create a new touch swipe detector + m_touchSwipeDetector = new CGenericTouchSwipeDetector(&CGenericTouchActionHandler::GetInstance(), 160.0f); + } + + gc[0].dwBlock = gc[0].dwWant ^ 0x01; + gc[1].dwBlock = gc[1].dwWant ^ 0x01; + gc[2].dwBlock = gc[2].dwWant ^ 0x1F; + } + if (DX::Windowing()->PtrSetGestureConfig) + DX::Windowing()->PtrSetGestureConfig(hWnd, 0, 5, gc, sizeof(GESTURECONFIG)); +} + +void CWinEventsWin32::OnGesture(HWND hWnd, LPARAM lParam) +{ + if (!DX::Windowing()->PtrGetGestureInfo) + return; + + GESTUREINFO gi = {}; + gi.cbSize = sizeof(gi); + DX::Windowing()->PtrGetGestureInfo(reinterpret_cast<HGESTUREINFO>(lParam), &gi); + + // convert to window coordinates + POINT point = { gi.ptsLocation.x, gi.ptsLocation.y }; + WindowFromScreenCoords(hWnd, &point); + + if (gi.dwID == GID_BEGIN) + m_touchPointer.reset(); + + // if there's a "current" touch from a previous event, copy it to "last" + if (m_touchPointer.current.valid()) + m_touchPointer.last = m_touchPointer.current; + + // set the "current" touch + m_touchPointer.current.x = static_cast<float>(point.x); + m_touchPointer.current.y = static_cast<float>(point.y); + m_touchPointer.current.time = time(nullptr); + + switch (gi.dwID) + { + case GID_BEGIN: + { + // set the "down" touch + m_touchPointer.down = m_touchPointer.current; + m_originalZoomDistance = 0; + + CGenericTouchActionHandler::GetInstance().OnTouchGestureStart(static_cast<float>(point.x), static_cast<float>(point.y)); + } + break; + + case GID_END: + CGenericTouchActionHandler::GetInstance().OnTouchGestureEnd(static_cast<float>(point.x), static_cast<float>(point.y), 0.0f, 0.0f, 0.0f, 0.0f); + break; + + case GID_PAN: + { + if (!m_touchPointer.moving) + m_touchPointer.moving = true; + + // calculate the velocity of the pan gesture + float velocityX, velocityY; + m_touchPointer.velocity(velocityX, velocityY); + + CGenericTouchActionHandler::GetInstance().OnTouchGesturePan(m_touchPointer.current.x, m_touchPointer.current.y, + m_touchPointer.current.x - m_touchPointer.last.x, m_touchPointer.current.y - m_touchPointer.last.y, + velocityX, velocityY); + + if (m_touchSwipeDetector != nullptr) + { + if (gi.dwFlags & GF_BEGIN) + { + m_touchPointer.down = m_touchPointer.current; + m_touchSwipeDetector->OnTouchDown(0, m_touchPointer); + } + else if (gi.dwFlags & GF_END) + { + m_touchSwipeDetector->OnTouchUp(0, m_touchPointer); + + delete m_touchSwipeDetector; + m_touchSwipeDetector = nullptr; + } + else + m_touchSwipeDetector->OnTouchMove(0, m_touchPointer); + } + } + break; + + case GID_ROTATE: + { + if (gi.dwFlags == GF_BEGIN) + break; + + CGenericTouchActionHandler::GetInstance().OnRotate(static_cast<float>(point.x), static_cast<float>(point.y), + -static_cast<float>(ROTATE_ANGLE_DEGREE(gi.ullArguments))); + } + break; + + case GID_ZOOM: + { + if (gi.dwFlags == GF_BEGIN) + { + m_originalZoomDistance = static_cast<int>(LODWORD(gi.ullArguments)); + break; + } + + // avoid division by 0 + if (m_originalZoomDistance == 0) + break; + + CGenericTouchActionHandler::GetInstance().OnZoomPinch(static_cast<float>(point.x), static_cast<float>(point.y), + static_cast<float>(LODWORD(gi.ullArguments)) / static_cast<float>(m_originalZoomDistance)); + } + break; + + case GID_TWOFINGERTAP: + CGenericTouchActionHandler::GetInstance().OnTap(static_cast<float>(point.x), static_cast<float>(point.y), 2); + break; + + case GID_PRESSANDTAP: + default: + // You have encountered an unknown gesture + break; + } + if(DX::Windowing()->PtrCloseGestureInfoHandle) + DX::Windowing()->PtrCloseGestureInfoHandle(reinterpret_cast<HGESTUREINFO>(lParam)); +} diff --git a/xbmc/windowing/windows/WinEventsWin32.h b/xbmc/windowing/windows/WinEventsWin32.h new file mode 100644 index 0000000..466c755 --- /dev/null +++ b/xbmc/windowing/windows/WinEventsWin32.h @@ -0,0 +1,32 @@ +/* + * 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 + +#include "input/touch/TouchTypes.h" +#include "windowing/WinEvents.h" + +class CGenericTouchSwipeDetector; + +class CWinEventsWin32 : public IWinEvents +{ +public: + bool MessagePump() override; + static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + +private: + static void RegisterDeviceInterfaceToHwnd(GUID InterfaceClassGuid, HWND hWnd, HDEVNOTIFY *hDeviceNotify); + static void WindowFromScreenCoords(HWND hWnd, POINT *point); + static void OnGestureNotify(HWND hWnd, LPARAM lParam); + static void OnGesture(HWND hWnd, LPARAM lParam); + + static int m_originalZoomDistance; + static Pointer m_touchPointer; + static CGenericTouchSwipeDetector *m_touchSwipeDetector; +}; + diff --git a/xbmc/windowing/windows/WinKeyMap.h b/xbmc/windowing/windows/WinKeyMap.h new file mode 100644 index 0000000..c163747 --- /dev/null +++ b/xbmc/windowing/windows/WinKeyMap.h @@ -0,0 +1,181 @@ +/* + * 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 + +#include "ServiceBroker.h" +#include "Util.h" +#include "input/XBMC_keysym.h" +#include "input/XBMC_vkeys.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" + +#include <array> + +namespace KODI +{ +namespace WINDOWING +{ +namespace WINDOWS +{ + +static std::array<XBMCKey, XBMCK_LAST> VK_keymap; + +static void DIB_InitOSKeymap() +{ + /* Map the VK keysyms */ + VK_keymap.fill(XBMCK_UNKNOWN); + + VK_keymap[VK_BACK] = XBMCK_BACKSPACE; + VK_keymap[VK_TAB] = XBMCK_TAB; + VK_keymap[VK_CLEAR] = XBMCK_CLEAR; + VK_keymap[VK_RETURN] = XBMCK_RETURN; + VK_keymap[VK_PAUSE] = XBMCK_PAUSE; + VK_keymap[VK_ESCAPE] = XBMCK_ESCAPE; + VK_keymap[VK_SPACE] = XBMCK_SPACE; + VK_keymap[VK_APOSTROPHE] = XBMCK_QUOTE; + VK_keymap[VK_COMMA] = XBMCK_COMMA; + VK_keymap[VK_MINUS] = XBMCK_MINUS; + VK_keymap[VK_PERIOD] = XBMCK_PERIOD; + VK_keymap[VK_SLASH] = XBMCK_SLASH; + VK_keymap[VK_0] = XBMCK_0; + VK_keymap[VK_1] = XBMCK_1; + VK_keymap[VK_2] = XBMCK_2; + VK_keymap[VK_3] = XBMCK_3; + VK_keymap[VK_4] = XBMCK_4; + VK_keymap[VK_5] = XBMCK_5; + VK_keymap[VK_6] = XBMCK_6; + VK_keymap[VK_7] = XBMCK_7; + VK_keymap[VK_8] = XBMCK_8; + VK_keymap[VK_9] = XBMCK_9; + VK_keymap[VK_SEMICOLON] = XBMCK_SEMICOLON; + VK_keymap[VK_EQUALS] = XBMCK_EQUALS; + VK_keymap[VK_LBRACKET] = XBMCK_LEFTBRACKET; + VK_keymap[VK_BACKSLASH] = XBMCK_BACKSLASH; + VK_keymap[VK_OEM_102] = XBMCK_BACKSLASH; + VK_keymap[VK_RBRACKET] = XBMCK_RIGHTBRACKET; + VK_keymap[VK_GRAVE] = XBMCK_BACKQUOTE; + VK_keymap[VK_BACKTICK] = XBMCK_BACKQUOTE; + VK_keymap[VK_A] = XBMCK_a; + VK_keymap[VK_B] = XBMCK_b; + VK_keymap[VK_C] = XBMCK_c; + VK_keymap[VK_D] = XBMCK_d; + VK_keymap[VK_E] = XBMCK_e; + VK_keymap[VK_F] = XBMCK_f; + VK_keymap[VK_G] = XBMCK_g; + VK_keymap[VK_H] = XBMCK_h; + VK_keymap[VK_I] = XBMCK_i; + VK_keymap[VK_J] = XBMCK_j; + VK_keymap[VK_K] = XBMCK_k; + VK_keymap[VK_L] = XBMCK_l; + VK_keymap[VK_M] = XBMCK_m; + VK_keymap[VK_N] = XBMCK_n; + VK_keymap[VK_O] = XBMCK_o; + VK_keymap[VK_P] = XBMCK_p; + VK_keymap[VK_Q] = XBMCK_q; + VK_keymap[VK_R] = XBMCK_r; + VK_keymap[VK_S] = XBMCK_s; + VK_keymap[VK_T] = XBMCK_t; + VK_keymap[VK_U] = XBMCK_u; + VK_keymap[VK_V] = XBMCK_v; + VK_keymap[VK_W] = XBMCK_w; + VK_keymap[VK_X] = XBMCK_x; + VK_keymap[VK_Y] = XBMCK_y; + VK_keymap[VK_Z] = XBMCK_z; + VK_keymap[VK_DELETE] = XBMCK_DELETE; + + VK_keymap[VK_NUMPAD0] = XBMCK_KP0; + VK_keymap[VK_NUMPAD1] = XBMCK_KP1; + VK_keymap[VK_NUMPAD2] = XBMCK_KP2; + VK_keymap[VK_NUMPAD3] = XBMCK_KP3; + VK_keymap[VK_NUMPAD4] = XBMCK_KP4; + VK_keymap[VK_NUMPAD5] = XBMCK_KP5; + VK_keymap[VK_NUMPAD6] = XBMCK_KP6; + VK_keymap[VK_NUMPAD7] = XBMCK_KP7; + VK_keymap[VK_NUMPAD8] = XBMCK_KP8; + VK_keymap[VK_NUMPAD9] = XBMCK_KP9; + VK_keymap[VK_DECIMAL] = XBMCK_KP_PERIOD; + VK_keymap[VK_DIVIDE] = XBMCK_KP_DIVIDE; + VK_keymap[VK_MULTIPLY] = XBMCK_KP_MULTIPLY; + VK_keymap[VK_SUBTRACT] = XBMCK_KP_MINUS; + VK_keymap[VK_ADD] = XBMCK_KP_PLUS; + + VK_keymap[VK_UP] = XBMCK_UP; + VK_keymap[VK_DOWN] = XBMCK_DOWN; + VK_keymap[VK_RIGHT] = XBMCK_RIGHT; + VK_keymap[VK_LEFT] = XBMCK_LEFT; + VK_keymap[VK_INSERT] = XBMCK_INSERT; + VK_keymap[VK_HOME] = XBMCK_HOME; + VK_keymap[VK_END] = XBMCK_END; + VK_keymap[VK_PRIOR] = XBMCK_PAGEUP; + VK_keymap[VK_NEXT] = XBMCK_PAGEDOWN; + + VK_keymap[VK_F1] = XBMCK_F1; + VK_keymap[VK_F2] = XBMCK_F2; + VK_keymap[VK_F3] = XBMCK_F3; + VK_keymap[VK_F4] = XBMCK_F4; + VK_keymap[VK_F5] = XBMCK_F5; + VK_keymap[VK_F6] = XBMCK_F6; + VK_keymap[VK_F7] = XBMCK_F7; + VK_keymap[VK_F8] = XBMCK_F8; + VK_keymap[VK_F9] = XBMCK_F9; + VK_keymap[VK_F10] = XBMCK_F10; + VK_keymap[VK_F11] = XBMCK_F11; + VK_keymap[VK_F12] = XBMCK_F12; + VK_keymap[VK_F13] = XBMCK_F13; + VK_keymap[VK_F14] = XBMCK_F14; + VK_keymap[VK_F15] = XBMCK_F15; + + VK_keymap[VK_NUMLOCK] = XBMCK_NUMLOCK; + VK_keymap[VK_CAPITAL] = XBMCK_CAPSLOCK; + VK_keymap[VK_SCROLL] = XBMCK_SCROLLOCK; + VK_keymap[VK_RSHIFT] = XBMCK_RSHIFT; + VK_keymap[VK_LSHIFT] = XBMCK_LSHIFT; + VK_keymap[VK_RCONTROL] = XBMCK_RCTRL; + VK_keymap[VK_LCONTROL] = XBMCK_LCTRL; + VK_keymap[VK_RMENU] = XBMCK_RALT; + VK_keymap[VK_LMENU] = XBMCK_LALT; + VK_keymap[VK_RWIN] = XBMCK_RSUPER; + VK_keymap[VK_LWIN] = XBMCK_LSUPER; + + VK_keymap[VK_HELP] = XBMCK_HELP; +#ifdef VK_PRINT + VK_keymap[VK_PRINT] = XBMCK_PRINT; +#endif + VK_keymap[VK_SNAPSHOT] = XBMCK_PRINT; + VK_keymap[VK_CANCEL] = XBMCK_BREAK; + VK_keymap[VK_APPS] = XBMCK_MENU; + + // Only include the multimedia keys if they have been enabled in the + // advanced settings + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_enableMultimediaKeys) + { + VK_keymap[VK_BROWSER_BACK] = XBMCK_BROWSER_BACK; + VK_keymap[VK_BROWSER_FORWARD] = XBMCK_BROWSER_FORWARD; + VK_keymap[VK_BROWSER_REFRESH] = XBMCK_BROWSER_REFRESH; + VK_keymap[VK_BROWSER_STOP] = XBMCK_BROWSER_STOP; + VK_keymap[VK_BROWSER_SEARCH] = XBMCK_BROWSER_SEARCH; + VK_keymap[VK_BROWSER_FAVORITES] = XBMCK_BROWSER_FAVORITES; + VK_keymap[VK_BROWSER_HOME] = XBMCK_BROWSER_HOME; + VK_keymap[VK_VOLUME_MUTE] = XBMCK_VOLUME_MUTE; + VK_keymap[VK_VOLUME_DOWN] = XBMCK_VOLUME_DOWN; + VK_keymap[VK_VOLUME_UP] = XBMCK_VOLUME_UP; + VK_keymap[VK_MEDIA_NEXT_TRACK] = XBMCK_MEDIA_NEXT_TRACK; + VK_keymap[VK_MEDIA_PREV_TRACK] = XBMCK_MEDIA_PREV_TRACK; + VK_keymap[VK_MEDIA_STOP] = XBMCK_MEDIA_STOP; + VK_keymap[VK_MEDIA_PLAY_PAUSE] = XBMCK_MEDIA_PLAY_PAUSE; + VK_keymap[VK_LAUNCH_MAIL] = XBMCK_LAUNCH_MAIL; + VK_keymap[VK_LAUNCH_MEDIA_SELECT] = XBMCK_LAUNCH_MEDIA_SELECT; + VK_keymap[VK_LAUNCH_APP1] = XBMCK_LAUNCH_APP1; + VK_keymap[VK_LAUNCH_APP2] = XBMCK_LAUNCH_APP2; + } +} + +} // namespace WINDOWS +} // namespace WINDOWING +} // namespace KODI diff --git a/xbmc/windowing/windows/WinSystemWin32.cpp b/xbmc/windowing/windows/WinSystemWin32.cpp new file mode 100644 index 0000000..9394b4b --- /dev/null +++ b/xbmc/windowing/windows/WinSystemWin32.cpp @@ -0,0 +1,1368 @@ +/* + * 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 "WinSystemWin32.h" + +#include "ServiceBroker.h" +#include "VideoSyncD3D.h" +#include "WIN32Util.h" +#include "WinEventsWin32.h" +#include "application/Application.h" +#include "cores/AudioEngine/AESinkFactory.h" +#include "cores/AudioEngine/Sinks/AESinkDirectSound.h" +#include "cores/AudioEngine/Sinks/AESinkWASAPI.h" +#include "filesystem/File.h" +#include "filesystem/SpecialProtocol.h" +#include "messaging/ApplicationMessenger.h" +#include "platform/Environment.h" +#include "rendering/dx/ScreenshotSurfaceWindows.h" +#include "resource.h" +#include "settings/AdvancedSettings.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" +#include "utils/SystemInfo.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/windows/Win32DPMSSupport.h" + +#include "platform/win32/CharsetConverter.h" +#include "platform/win32/input/IRServerSuite.h" + +#include <algorithm> +#include <mutex> + +#include <tpcshrd.h> + +using namespace std::chrono_literals; + +const char* CWinSystemWin32::SETTING_WINDOW_TOP = "window.top"; +const char* CWinSystemWin32::SETTING_WINDOW_LEFT = "window.left"; + +CWinSystemWin32::CWinSystemWin32() + : CWinSystemBase() + , PtrGetGestureInfo(nullptr) + , PtrSetGestureConfig(nullptr) + , PtrCloseGestureInfoHandle(nullptr) + , PtrEnableNonClientDpiScaling(nullptr) + , m_hWnd(nullptr) + , m_hMonitor(nullptr) + , m_hInstance(nullptr) + , m_hIcon(nullptr) + , m_ValidWindowedPosition(false) + , m_IsAlteringWindow(false) + , m_delayDispReset(false) + , m_state(WINDOW_STATE_WINDOWED) + , m_fullscreenState(WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW) + , m_windowState(WINDOW_WINDOW_STATE_WINDOWED) + , m_windowStyle(WINDOWED_STYLE) + , m_windowExStyle(WINDOWED_EX_STYLE) + , m_inFocus(false) + , m_bMinimized(false) +{ + std::string cacert = CEnvironment::getenv("SSL_CERT_FILE"); + if (cacert.empty() || !XFILE::CFile::Exists(cacert)) + { + cacert = CSpecialProtocol::TranslatePath("special://xbmc/system/certs/cacert.pem"); + if (XFILE::CFile::Exists(cacert)) + CEnvironment::setenv("SSL_CERT_FILE", cacert.c_str(), 1); + } + + m_winEvents.reset(new CWinEventsWin32()); + AE::CAESinkFactory::ClearSinks(); + CAESinkDirectSound::Register(); + CAESinkWASAPI::Register(); + CScreenshotSurfaceWindows::Register(); + + if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bScanIRServer) + { + m_irss.reset(new CIRServerSuite()); + m_irss->Initialize(); + } + m_dpms = std::make_shared<CWin32DPMSSupport>(); +} + +CWinSystemWin32::~CWinSystemWin32() +{ + if (m_hIcon) + { + DestroyIcon(m_hIcon); + m_hIcon = nullptr; + } +}; + +bool CWinSystemWin32::InitWindowSystem() +{ + if(!CWinSystemBase::InitWindowSystem()) + return false; + + return true; +} + +bool CWinSystemWin32::DestroyWindowSystem() +{ + if (m_hMonitor) + { + MONITOR_DETAILS* details = GetDisplayDetails(m_hMonitor); + if (details) + RestoreDesktopResolution(details); + } + return true; +} + +bool CWinSystemWin32::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + using KODI::PLATFORM::WINDOWS::ToW; + auto nameW = ToW(name); + + m_hInstance = static_cast<HINSTANCE>(GetModuleHandle(nullptr)); + if(m_hInstance == nullptr) + CLog::LogF(LOGDEBUG, " GetModuleHandle failed with {}", GetLastError()); + + // Load Win32 procs if available + HMODULE hUser32 = GetModuleHandle(L"user32"); + if (hUser32) + { + PtrGetGestureInfo = reinterpret_cast<pGetGestureInfo>(GetProcAddress(hUser32, "GetGestureInfo")); + PtrSetGestureConfig = reinterpret_cast<pSetGestureConfig>(GetProcAddress(hUser32, "SetGestureConfig")); + PtrCloseGestureInfoHandle = reinterpret_cast<pCloseGestureInfoHandle>(GetProcAddress(hUser32, "CloseGestureInfoHandle")); + // if available, enable automatic DPI scaling of the non-client area portions of the window. + PtrEnableNonClientDpiScaling = reinterpret_cast<pEnableNonClientDpiScaling>(GetProcAddress(hUser32, "EnableNonClientDpiScaling")); + } + + UpdateStates(fullScreen); + // initialize the state + WINDOW_STATE state = GetState(fullScreen); + + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bFullScreen = fullScreen; + m_fRefreshRate = res.fRefreshRate; + + m_hIcon = LoadIcon(m_hInstance, MAKEINTRESOURCE(IDI_MAIN_ICON)); + + // Register the windows class + WNDCLASSEX wndClass = {}; + wndClass.cbSize = sizeof(wndClass); + wndClass.style = CS_HREDRAW | CS_VREDRAW; + wndClass.lpfnWndProc = CWinEventsWin32::WndProc; + wndClass.cbClsExtra = 0; + wndClass.cbWndExtra = 0; + wndClass.hInstance = m_hInstance; + wndClass.hIcon = m_hIcon; + wndClass.hCursor = LoadCursor(nullptr, IDC_ARROW ); + wndClass.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)); + wndClass.lpszMenuName = nullptr; + wndClass.lpszClassName = nameW.c_str(); + + if( !RegisterClassExW( &wndClass ) ) + { + CLog::LogF(LOGERROR, " RegisterClassExW failed with {}", GetLastError()); + return false; + } + + // put the window at desired display + RECT screenRect = ScreenRect(m_hMonitor); + m_nLeft = screenRect.left; + m_nTop = screenRect.top; + + if (state == WINDOW_STATE_WINDOWED) + { + const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + const int top = settings->GetInt(SETTING_WINDOW_TOP); + const int left = settings->GetInt(SETTING_WINDOW_LEFT); + const RECT vsRect = GetVirtualScreenRect(); + + // we check that window is inside of virtual screen rect (sum of all monitors) + // top 0 left 0 is a special position that centers the window on the screen + if ((top != 0 || left != 0) && top >= vsRect.top && top + m_nHeight <= vsRect.bottom && + left >= vsRect.left && left + m_nWidth <= vsRect.right) + { + // restore previous window position + m_nLeft = left; + m_nTop = top; + } + else + { + // Windowed mode: position and size in settings and most places in Kodi + // are for the client part of the window. + RECT rcWorkArea = GetScreenWorkArea(m_hMonitor); + + int workAreaWidth = rcWorkArea.right - rcWorkArea.left; + int workAreaHeight = rcWorkArea.bottom - rcWorkArea.top; + + RECT rcNcArea = GetNcAreaOffsets(m_windowStyle, false, m_windowExStyle); + int maxClientWidth = (rcWorkArea.right - rcNcArea.right) - (rcWorkArea.left - rcNcArea.left); + int maxClientHeight = (rcWorkArea.bottom - rcNcArea.bottom) - (rcWorkArea.top - rcNcArea.top); + + m_nWidth = std::min(m_nWidth, maxClientWidth); + m_nHeight = std::min(m_nHeight, maxClientHeight); + CWinSystemBase::SetWindowResolution(m_nWidth, m_nHeight); + + // center window on desktop + m_nLeft = rcWorkArea.left - rcNcArea.left + (maxClientWidth - m_nWidth) / 2; + m_nTop = rcWorkArea.top - rcNcArea.top + (maxClientHeight - m_nHeight) / 2; + } + m_ValidWindowedPosition = true; + } + + HWND hWnd = CreateWindowExW( + m_windowExStyle, + nameW.c_str(), + nameW.c_str(), + m_windowStyle, + m_nLeft, + m_nTop, + m_nWidth, + m_nHeight, + nullptr, + nullptr, + m_hInstance, + nullptr + ); + + if( hWnd == nullptr ) + { + CLog::LogF(LOGERROR, " CreateWindow failed with {}", GetLastError()); + return false; + } + + m_inFocus = true; + + DWORD dwHwndTabletProperty = + TABLET_DISABLE_PENBARRELFEEDBACK | // disables UI feedback on pen button down (circle) + TABLET_DISABLE_FLICKS; // disables pen flicks (back, forward, drag down, drag up) + + SetProp(hWnd, MICROSOFT_TABLETPENSERVICE_PROPERTY, &dwHwndTabletProperty); + + m_hWnd = hWnd; + m_bWindowCreated = true; + + CreateBlankWindows(); + + m_state = state; + AdjustWindow(true); + + // Show the window + ShowWindow( m_hWnd, SW_SHOWDEFAULT ); + UpdateWindow( m_hWnd ); + + // Configure the tray icon. + m_trayIcon.cbSize = sizeof(m_trayIcon); + m_trayIcon.hWnd = m_hWnd; + m_trayIcon.hIcon = m_hIcon; + wcsncpy(m_trayIcon.szTip, nameW.c_str(), sizeof(m_trayIcon.szTip) / sizeof(WCHAR)); + m_trayIcon.uCallbackMessage = TRAY_ICON_NOTIFY; + m_trayIcon.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE; + + return true; +} + +bool CWinSystemWin32::CreateBlankWindows() +{ + WNDCLASSEX wcex; + + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style= CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc= DefWindowProc; + wcex.cbClsExtra= 0; + wcex.cbWndExtra= 0; + wcex.hInstance= nullptr; + wcex.hIcon= nullptr; + wcex.hCursor= nullptr; + wcex.hbrBackground= static_cast<HBRUSH>(CreateSolidBrush(RGB(0, 0, 0))); + wcex.lpszMenuName= nullptr; + wcex.lpszClassName= L"BlankWindowClass"; + wcex.hIconSm= nullptr; + + // Now we can go ahead and register our new window class + if(!RegisterClassEx(&wcex)) + { + CLog::LogF(LOGERROR, "RegisterClass failed with {}", GetLastError()); + return false; + } + + // We need as many blank windows as there are screens (minus 1) + for (size_t i = 0; i < m_displays.size() - 1; i++) + { + HWND hBlankWindow = CreateWindowEx(WS_EX_TOPMOST, L"BlankWindowClass", L"", WS_POPUP | WS_DISABLED, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, nullptr, nullptr); + + if (hBlankWindow == nullptr) + { + CLog::LogF(LOGERROR, "CreateWindowEx failed with {}", GetLastError()); + return false; + } + + m_hBlankWindows.push_back(hBlankWindow); + } + + return true; +} + +bool CWinSystemWin32::BlankNonActiveMonitors(bool bBlank) +{ + if (m_hBlankWindows.empty()) + return false; + + if (bBlank == false) + { + for (unsigned int i=0; i < m_hBlankWindows.size(); i++) + ShowWindow(m_hBlankWindows[i], SW_HIDE); + return true; + } + + // Move a blank window in front of every display, except the current display. + for (size_t i = 0, j = 0; i < m_displays.size(); ++i) + { + MONITOR_DETAILS& details = m_displays[i]; + if (details.hMonitor == m_hMonitor) + continue; + + RECT rBounds = ScreenRect(details.hMonitor); + // move and resize the window + SetWindowPos(m_hBlankWindows[j], nullptr, rBounds.left, rBounds.top, + rBounds.right - rBounds.left, rBounds.bottom - rBounds.top, + SWP_NOACTIVATE); + + ShowWindow(m_hBlankWindows[j], SW_SHOW | SW_SHOWNOACTIVATE); + j++; + } + + if (m_hWnd) + SetForegroundWindow(m_hWnd); + + return true; +} + +bool CWinSystemWin32::CenterWindow() +{ + RESOLUTION_INFO DesktopRes = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP); + + m_nLeft = (DesktopRes.iWidth / 2) - (m_nWidth / 2); + m_nTop = (DesktopRes.iHeight / 2) - (m_nHeight / 2); + + RECT rc; + rc.left = m_nLeft; + rc.top = m_nTop; + rc.right = rc.left + m_nWidth; + rc.bottom = rc.top + m_nHeight; + AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, false ); + + SetWindowPos(m_hWnd, nullptr, rc.left, rc.top, 0, 0, SWP_NOSIZE); + + return true; +} + +bool CWinSystemWin32::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + m_nWidth = newWidth; + m_nHeight = newHeight; + + if (newLeft > 0) + m_nLeft = newLeft; + + if (newTop > 0) + m_nTop = newTop; + + AdjustWindow(); + + return true; +} + +void CWinSystemWin32::FinishWindowResize(int newWidth, int newHeight) +{ + m_nWidth = newWidth; + m_nHeight = newHeight; +} + +void CWinSystemWin32::AdjustWindow(bool forceResize) +{ + CLog::LogF(LOGDEBUG, "adjusting window if required."); + + HWND windowAfter; + RECT rc; + + if (m_state == WINDOW_STATE_FULLSCREEN_WINDOW || m_state == WINDOW_STATE_FULLSCREEN) + { + windowAfter = HWND_TOP; + rc = ScreenRect(m_hMonitor); + } + else // m_state == WINDOW_STATE_WINDOWED + { + windowAfter = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST; + + if (!m_ValidWindowedPosition) + { + const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + const int top = settings->GetInt(SETTING_WINDOW_TOP); + const int left = settings->GetInt(SETTING_WINDOW_LEFT); + const RECT vsRect = GetVirtualScreenRect(); + + // we check that window is inside of virtual screen rect (sum of all monitors) + // top 0 left 0 is a special position that centers the window on the screen + if ((top != 0 || left != 0) && top >= vsRect.top && top + m_nHeight <= vsRect.bottom && + left >= vsRect.left && left + m_nWidth <= vsRect.right) + { + // restore previous window position + m_nTop = top; + m_nLeft = left; + } + else + { + // Windowed mode: position and size in settings and most places in Kodi + // are for the client part of the window. + RECT rcWorkArea = GetScreenWorkArea(m_hMonitor); + + int workAreaWidth = rcWorkArea.right - rcWorkArea.left; + int workAreaHeight = rcWorkArea.bottom - rcWorkArea.top; + + RECT rcNcArea = GetNcAreaOffsets(m_windowStyle, false, m_windowExStyle); + int maxClientWidth = + (rcWorkArea.right - rcNcArea.right) - (rcWorkArea.left - rcNcArea.left); + int maxClientHeight = + (rcWorkArea.bottom - rcNcArea.bottom) - (rcWorkArea.top - rcNcArea.top); + + m_nWidth = std::min(m_nWidth, maxClientWidth); + m_nHeight = std::min(m_nHeight, maxClientHeight); + CWinSystemBase::SetWindowResolution(m_nWidth, m_nHeight); + + // center window on desktop + m_nLeft = rcWorkArea.left - rcNcArea.left + (maxClientWidth - m_nWidth) / 2; + m_nTop = rcWorkArea.top - rcNcArea.top + (maxClientHeight - m_nHeight) / 2; + } + m_ValidWindowedPosition = true; + } + + rc.left = m_nLeft; + rc.right = m_nLeft + m_nWidth; + rc.top = m_nTop; + rc.bottom = m_nTop + m_nHeight; + + HMONITOR hMon = MonitorFromRect(&rc, MONITOR_DEFAULTTONULL); + HMONITOR hMon2 = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTOPRIMARY); + + if (!m_ValidWindowedPosition || hMon == nullptr || hMon != hMon2) + { + // centering window at desktop + RECT newScreenRect = ScreenRect(hMon2); + rc.left = m_nLeft = newScreenRect.left + ((newScreenRect.right - newScreenRect.left) / 2) - (m_nWidth / 2); + rc.top = m_nTop = newScreenRect.top + ((newScreenRect.bottom - newScreenRect.top) / 2) - (m_nHeight / 2); + rc.right = m_nLeft + m_nWidth; + rc.bottom = m_nTop + m_nHeight; + m_ValidWindowedPosition = true; + } + AdjustWindowRectEx(&rc, m_windowStyle, false, m_windowExStyle); + } + + WINDOWINFO wi; + wi.cbSize = sizeof(WINDOWINFO); + if (!GetWindowInfo(m_hWnd, &wi)) + { + CLog::LogF(LOGERROR, "GetWindowInfo failed with {}", GetLastError()); + return; + } + RECT wr = wi.rcWindow; + + if ( wr.bottom - wr.top == rc.bottom - rc.top + && wr.right - wr.left == rc.right - rc.left + && (wi.dwStyle & WS_CAPTION) == (m_windowStyle & WS_CAPTION) + && !forceResize) + { + return; + } + + //Sets the window style + SetLastError(0); + SetWindowLongPtr( m_hWnd, GWL_STYLE, m_windowStyle ); + + //Sets the window ex style + SetLastError(0); + SetWindowLongPtr( m_hWnd, GWL_EXSTYLE, m_windowExStyle ); + + // resize window + CLog::LogF(LOGDEBUG, "resizing due to size change ({},{},{},{}{})->({},{},{},{}{})", wr.left, + wr.top, wr.right, wr.bottom, (wi.dwStyle & WS_CAPTION) ? "" : " fullscreen", rc.left, + rc.top, rc.right, rc.bottom, (m_windowStyle & WS_CAPTION) ? "" : " fullscreen"); + SetWindowPos( + m_hWnd, + windowAfter, + rc.left, + rc.top, + rc.right - rc.left, + rc.bottom - rc.top, + SWP_SHOWWINDOW | SWP_DRAWFRAME + ); +} + +void CWinSystemWin32::CenterCursor() const +{ + RECT rect; + POINT point = {}; + + //Gets the client rect, then translates it to screen coordinates + //so that SetCursorPos isn't called with relative x and y values + GetClientRect(m_hWnd, &rect); + ClientToScreen(m_hWnd, &point); + + rect.left += point.x; + rect.right += point.x; + rect.top += point.y; + rect.bottom += point.y; + + int x = rect.left + (rect.right - rect.left) / 2; + int y = rect.top + (rect.bottom - rect.top) / 2; + + SetCursorPos(x, y); +} + +bool CWinSystemWin32::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + CWinSystemWin32::UpdateStates(fullScreen); + WINDOW_STATE state = GetState(fullScreen); + + CLog::LogF(LOGDEBUG, "({}) with size {}x{}, refresh {:f}{}", window_state_names[state], + res.iWidth, res.iHeight, res.fRefreshRate, + (res.dwFlags & D3DPRESENTFLAG_INTERLACED) ? "i" : ""); + + // oldMonitor may be NULL if it's powered off or not available due windows settings + MONITOR_DETAILS* oldMonitor = GetDisplayDetails(m_hMonitor); + MONITOR_DETAILS* newMonitor = GetDisplayDetails(res.strOutput); + + bool forceChange = false; // resolution/display is changed but window state isn't changed + bool changeScreen = false; // display is changed + bool stereoChange = IsStereoEnabled() != (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_HARDWAREBASED); + + if (m_nWidth != res.iWidth || m_nHeight != res.iHeight || m_fRefreshRate != res.fRefreshRate || + !oldMonitor || oldMonitor->hMonitor != newMonitor->hMonitor || stereoChange || + m_bFirstResChange) + { + if (!oldMonitor || oldMonitor->hMonitor != newMonitor->hMonitor) + changeScreen = true; + forceChange = true; + } + + if (state == m_state && !forceChange) + { + m_bBlankOtherDisplay = blankOtherDisplays; + BlankNonActiveMonitors(m_bBlankOtherDisplay); + return true; + } + + // entering to stereo mode, limit resolution to 1080p@23.976 + if (stereoChange && !IsStereoEnabled() && res.iWidth > 1280) + { + res = CDisplaySettings::GetInstance().GetResolutionInfo( + CResolutionUtils::ChooseBestResolution(24.f / 1.001f, 1920, 1080, true)); + } + + if (m_state == WINDOW_STATE_WINDOWED) + { + WINDOWINFO wi = {}; + wi.cbSize = sizeof(WINDOWINFO); + if (GetWindowInfo(m_hWnd, &wi) && wi.rcClient.top > 0) + { + m_nLeft = wi.rcClient.left; + m_nTop = wi.rcClient.top; + m_ValidWindowedPosition = true; + } + } + + m_IsAlteringWindow = true; + ReleaseBackBuffer(); + + if (changeScreen) + { + // before we changing display we have to leave exclusive mode on "old" display + if (m_state == WINDOW_STATE_FULLSCREEN) + SetDeviceFullScreen(false, res); + + // restoring native resolution on "old" display + RestoreDesktopResolution(oldMonitor); + + // notify about screen change (it may require recreate rendering device) + m_fRefreshRate = res.fRefreshRate; // use desired refresh for driver hook + OnScreenChange(newMonitor->hMonitor); + } + + m_bFirstResChange = false; + m_bFullScreen = fullScreen; + m_hMonitor = newMonitor->hMonitor; + m_nWidth = res.iWidth; + m_nHeight = res.iHeight; + m_bBlankOtherDisplay = blankOtherDisplays; + m_fRefreshRate = res.fRefreshRate; + + if (state == WINDOW_STATE_FULLSCREEN) + { + SetForegroundWindowInternal(m_hWnd); + + m_state = state; + AdjustWindow(changeScreen); + + // enter in exclusive mode, this will change resolution if we already in + SetDeviceFullScreen(true, res); + } + else if (m_state == WINDOW_STATE_FULLSCREEN || m_state == WINDOW_STATE_FULLSCREEN_WINDOW) // we're in fullscreen state now + { + // guess we are leaving exclusive mode, this will not an effect if we already not in + SetDeviceFullScreen(false, res); + + if (state == WINDOW_STATE_WINDOWED) // go to a windowed state + { + // need to restore resolution if it was changed to not native + // because we do not support resolution change in windowed mode + RestoreDesktopResolution(newMonitor); + } + else if (state == WINDOW_STATE_FULLSCREEN_WINDOW) // enter fullscreen window instead + { + ChangeResolution(res, stereoChange); + } + + m_state = state; + AdjustWindow(changeScreen); + } + else // we're in windowed state now + { + if (state == WINDOW_STATE_FULLSCREEN_WINDOW) + { + ChangeResolution(res, stereoChange); + + m_state = state; + AdjustWindow(changeScreen); + } + } + + if (changeScreen) + CenterCursor(); + + CreateBackBuffer(); + + BlankNonActiveMonitors(m_bBlankOtherDisplay); + m_IsAlteringWindow = false; + + return true; +} + +bool CWinSystemWin32::DPIChanged(WORD dpi, RECT windowRect) const +{ + (void)dpi; + RECT resizeRect = windowRect; + HMONITOR hMon = MonitorFromRect(&resizeRect, MONITOR_DEFAULTTONULL); + if (hMon == nullptr) + { + hMon = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTOPRIMARY); + } + + if (hMon) + { + MONITORINFOEX monitorInfo; + monitorInfo.cbSize = sizeof(MONITORINFOEX); + GetMonitorInfoW(hMon, &monitorInfo); + + if (m_state == WINDOW_STATE_FULLSCREEN_WINDOW || + m_state == WINDOW_STATE_FULLSCREEN) + { + resizeRect = monitorInfo.rcMonitor; // the whole screen + } + else + { + RECT wr = monitorInfo.rcWork; // it excludes task bar + long wrWidth = wr.right - wr.left; + long wrHeight = wr.bottom - wr.top; + long resizeWidth = resizeRect.right - resizeRect.left; + long resizeHeight = resizeRect.bottom - resizeRect.top; + + if (resizeWidth > wrWidth) + { + resizeRect.right = resizeRect.left + wrWidth; + } + + // make sure suggested windows size is not taller or wider than working area of new monitor (considers the toolbar) + if (resizeHeight > wrHeight) + { + resizeRect.bottom = resizeRect.top + wrHeight; + } + } + } + + // resize the window to the suggested size. Will generate a WM_SIZE event + SetWindowPos(m_hWnd, + nullptr, + resizeRect.left, + resizeRect.top, + resizeRect.right - resizeRect.left, + resizeRect.bottom - resizeRect.top, + SWP_NOZORDER | SWP_NOACTIVATE); + + return true; +} + +void CWinSystemWin32::SetMinimized(bool minimized) +{ + const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings(); + + if (advancedSettings->m_minimizeToTray) + { + if (minimized) + { + Shell_NotifyIcon(NIM_ADD, &m_trayIcon); + ShowWindow(m_hWnd, SW_HIDE); + } + else + { + Shell_NotifyIcon(NIM_DELETE, &m_trayIcon); + ShowWindow(m_hWnd, SW_RESTORE); + } + } + + m_bMinimized = minimized; +} + +std::vector<std::string> CWinSystemWin32::GetConnectedOutputs() +{ + std::vector<std::string> outputs; + + for (auto& display : m_displays) + { + outputs.emplace_back(KODI::PLATFORM::WINDOWS::FromW(display.MonitorNameW)); + } + + return outputs; +} + +void CWinSystemWin32::RestoreDesktopResolution(MONITOR_DETAILS* details) +{ + if (!details) + return; + + RESOLUTION_INFO info; + info.iWidth = details->ScreenWidth; + info.iHeight = details->ScreenHeight; + if ((details->RefreshRate + 1) % 24 == 0 || (details->RefreshRate + 1) % 30 == 0) + info.fRefreshRate = static_cast<float>(details->RefreshRate + 1) / 1.001f; + else + info.fRefreshRate = static_cast<float>(details->RefreshRate); + info.strOutput = KODI::PLATFORM::WINDOWS::FromW(details->DeviceNameW); + info.dwFlags = details->Interlaced ? D3DPRESENTFLAG_INTERLACED : 0; + + CLog::LogF(LOGDEBUG, "restoring desktop resolution for '{}'.", KODI::PLATFORM::WINDOWS::FromW(details->MonitorNameW)); + ChangeResolution(info); +} + +MONITOR_DETAILS* CWinSystemWin32::GetDisplayDetails(const std::string& name) +{ + using KODI::PLATFORM::WINDOWS::ToW; + + if (!name.empty() && name != "Default") + { + std::wstring nameW = ToW(name); + auto it = std::find_if(m_displays.begin(), m_displays.end(), [&nameW](MONITOR_DETAILS& m) + { + if (nameW[0] == '\\') // name is device name + return m.DeviceNameW == nameW; + return m.MonitorNameW == nameW; + }); + if (it != m_displays.end()) + return &(*it); + } + + // fallback to primary + auto it = std::find_if(m_displays.begin(), m_displays.end(), [](MONITOR_DETAILS& m) + { + return m.IsPrimary; + }); + if (it != m_displays.end()) + return &(*it); + + // nothing found + return nullptr; +} + +MONITOR_DETAILS* CWinSystemWin32::GetDisplayDetails(HMONITOR handle) +{ + auto it = std::find_if(m_displays.begin(), m_displays.end(), [&handle](MONITOR_DETAILS& m) + { + return m.hMonitor == handle; + }); + if (it != m_displays.end()) + return &(*it); + + return nullptr; +} + +RECT CWinSystemWin32::ScreenRect(HMONITOR handle) +{ + const MONITOR_DETAILS* details = GetDisplayDetails(handle); + if (!details) + { + CLog::LogF(LOGERROR, "no monitor found for handle"); + return RECT(); + } + + DEVMODEW sDevMode = {}; + sDevMode.dmSize = sizeof(sDevMode); + if(!EnumDisplaySettingsW(details->DeviceNameW.c_str(), ENUM_CURRENT_SETTINGS, &sDevMode)) + CLog::LogF(LOGERROR, " EnumDisplaySettings failed with {}", GetLastError()); + + RECT rc; + rc.left = sDevMode.dmPosition.x; + rc.right = sDevMode.dmPosition.x + sDevMode.dmPelsWidth; + rc.top = sDevMode.dmPosition.y; + rc.bottom = sDevMode.dmPosition.y + sDevMode.dmPelsHeight; + + return rc; +} + +void CWinSystemWin32::GetConnectedDisplays(std::vector<MONITOR_DETAILS>& outputs) +{ + using KODI::PLATFORM::WINDOWS::FromW; + + const POINT ptZero = { 0, 0 }; + HMONITOR hmPrimary = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY); + + DISPLAY_DEVICEW ddAdapter = {}; + ddAdapter.cb = sizeof(ddAdapter); + + for (DWORD adapter = 0; EnumDisplayDevicesW(nullptr, adapter, &ddAdapter, 0); ++adapter) + { + // Exclude displays that are not part of the windows desktop. Using them is too different: no windows, + // direct access with GDI CreateDC() or DirectDraw for example. So it may be possible to play video, but GUI? + if ((ddAdapter.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) + || !(ddAdapter.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP)) + continue; + + DISPLAY_DEVICEW ddMon = {}; + ddMon.cb = sizeof(ddMon); + bool foundScreen = false; + + DWORD screen = 0; + // Just look for the first active output, we're actually only interested in the information at the adapter level. + for (; EnumDisplayDevicesW(ddAdapter.DeviceName, screen, &ddMon, 0); ++screen) + { + if (ddMon.StateFlags & (DISPLAY_DEVICE_ACTIVE | DISPLAY_DEVICE_ATTACHED)) + { + foundScreen = true; + break; + } + } + + // Remoting returns no screens. Handle with a dummy screen. + if (!foundScreen && screen == 0) + { + lstrcpyW(ddMon.DeviceString, L"Dummy Monitor"); // safe: large static array + foundScreen = true; + } + + if (foundScreen) + { + // get information about the display's current position and display mode + DEVMODEW dm = {}; + dm.dmSize = sizeof(dm); + if (EnumDisplaySettingsExW(ddAdapter.DeviceName, ENUM_CURRENT_SETTINGS, &dm, 0) == FALSE) + EnumDisplaySettingsExW(ddAdapter.DeviceName, ENUM_REGISTRY_SETTINGS, &dm, 0); + + POINT pt = { dm.dmPosition.x, dm.dmPosition.y }; + HMONITOR hm = MonitorFromPoint(pt, MONITOR_DEFAULTTONULL); + + MONITOR_DETAILS md = {}; + uint8_t num = 1; + do + { + // `Monitor #N` + md.MonitorNameW = std::wstring(ddMon.DeviceString) + L" #" + std::to_wstring(num++); + } + while(std::any_of(outputs.begin(), outputs.end(), [&](MONITOR_DETAILS& m) { return m.MonitorNameW == md.MonitorNameW; })); + + md.CardNameW = ddAdapter.DeviceString; + md.DeviceNameW = ddAdapter.DeviceName; + md.ScreenWidth = dm.dmPelsWidth; + md.ScreenHeight = dm.dmPelsHeight; + md.hMonitor = hm; + md.RefreshRate = dm.dmDisplayFrequency; + md.Bpp = dm.dmBitsPerPel; + md.Interlaced = (dm.dmDisplayFlags & DM_INTERLACED) ? true : false; + + MONITORINFO mi = {}; + mi.cbSize = sizeof(mi); + if (GetMonitorInfoW(hm, &mi) && (mi.dwFlags & MONITORINFOF_PRIMARY)) + md.IsPrimary = true; + + outputs.push_back(md); + } + } +} + +bool CWinSystemWin32::ChangeResolution(const RESOLUTION_INFO& res, bool forceChange /*= false*/) +{ + using KODI::PLATFORM::WINDOWS::ToW; + std::wstring outputW = ToW(res.strOutput); + + DEVMODEW sDevMode = {}; + sDevMode.dmSize = sizeof(sDevMode); + + // If we can't read the current resolution or any detail of the resolution is different than res + if (!EnumDisplaySettingsW(outputW.c_str(), ENUM_CURRENT_SETTINGS, &sDevMode) || + sDevMode.dmPelsWidth != res.iWidth || sDevMode.dmPelsHeight != res.iHeight || + sDevMode.dmDisplayFrequency != static_cast<int>(res.fRefreshRate) || + ((sDevMode.dmDisplayFlags & DM_INTERLACED) && !(res.dwFlags & D3DPRESENTFLAG_INTERLACED)) || + (!(sDevMode.dmDisplayFlags & DM_INTERLACED) && (res.dwFlags & D3DPRESENTFLAG_INTERLACED)) + || forceChange) + { + ZeroMemory(&sDevMode, sizeof(sDevMode)); + sDevMode.dmSize = sizeof(sDevMode); + sDevMode.dmDriverExtra = 0; + sDevMode.dmPelsWidth = res.iWidth; + sDevMode.dmPelsHeight = res.iHeight; + sDevMode.dmDisplayFrequency = static_cast<int>(res.fRefreshRate); + sDevMode.dmDisplayFlags = (res.dwFlags & D3DPRESENTFLAG_INTERLACED) ? DM_INTERLACED : 0; + sDevMode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; + + LONG rc; + bool bResChanged = false; + + // Windows 8 refresh rate workaround for 24.0, 48.0 and 60.0 Hz + if ( CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin8) + && (res.fRefreshRate == 24.0 || res.fRefreshRate == 48.0 || res.fRefreshRate == 60.0)) + { + CLog::LogF(LOGDEBUG, "Using Windows 8+ workaround for refresh rate {} Hz", + static_cast<int>(res.fRefreshRate)); + + // Get current resolution stored in registry + DEVMODEW sDevModeRegistry = {}; + sDevModeRegistry.dmSize = sizeof(sDevModeRegistry); + if (EnumDisplaySettingsW(outputW.c_str(), ENUM_REGISTRY_SETTINGS, &sDevModeRegistry)) + { + // Set requested mode in registry without actually changing resolution + rc = ChangeDisplaySettingsExW(outputW.c_str(), &sDevMode, nullptr, CDS_UPDATEREGISTRY | CDS_NORESET, nullptr); + if (rc == DISP_CHANGE_SUCCESSFUL) + { + // Change resolution based on registry setting + rc = ChangeDisplaySettingsExW(outputW.c_str(), nullptr, nullptr, CDS_FULLSCREEN, nullptr); + if (rc == DISP_CHANGE_SUCCESSFUL) + bResChanged = true; + else + CLog::LogF( + LOGERROR, + "ChangeDisplaySettingsEx (W8+ change resolution) failed with {}, using fallback", + rc); + + // Restore registry with original values + sDevModeRegistry.dmSize = sizeof(sDevModeRegistry); + sDevModeRegistry.dmDriverExtra = 0; + sDevModeRegistry.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; + rc = ChangeDisplaySettingsExW(outputW.c_str(), &sDevModeRegistry, nullptr, CDS_UPDATEREGISTRY | CDS_NORESET, nullptr); + if (rc != DISP_CHANGE_SUCCESSFUL) + CLog::LogF(LOGERROR, "ChangeDisplaySettingsEx (W8+ restore registry) failed with {}", + rc); + } + else + CLog::LogF(LOGERROR, + "ChangeDisplaySettingsEx (W8+ set registry) failed with {}, using fallback", + rc); + } + else + CLog::LogF(LOGERROR, "Unable to retrieve registry settings for Windows 8+ workaround, using fallback"); + } + + // Standard resolution change/fallback for Windows 8+ workaround + if (!bResChanged) + { + // CDS_FULLSCREEN is for temporary fullscreen mode and prevents icons and windows from moving + // to fit within the new dimensions of the desktop + rc = ChangeDisplaySettingsExW(outputW.c_str(), &sDevMode, nullptr, CDS_FULLSCREEN, nullptr); + if (rc == DISP_CHANGE_SUCCESSFUL) + bResChanged = true; + else + CLog::LogF(LOGERROR, "ChangeDisplaySettingsEx failed with {}", rc); + } + + if (bResChanged) + ResolutionChanged(); + + return bResChanged; + } + + // nothing to do, return success + return true; +} + +void CWinSystemWin32::UpdateResolutions() +{ + using KODI::PLATFORM::WINDOWS::FromW; + + m_displays.clear(); + + CWinSystemBase::UpdateResolutions(); + GetConnectedDisplays(m_displays); + + MONITOR_DETAILS* details = GetDisplayDetails(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + if (!details) + return; + + float refreshRate; + int w = details->ScreenWidth; + int h = details->ScreenHeight; + if ((details->RefreshRate + 1) % 24 == 0 || (details->RefreshRate + 1) % 30 == 0) + refreshRate = static_cast<float>(details->RefreshRate + 1) / 1.001f; + else + refreshRate = static_cast<float>(details->RefreshRate); + + std::string strOutput = FromW(details->DeviceNameW); + std::string monitorName = FromW(details->MonitorNameW); + + uint32_t dwFlags = details->Interlaced ? D3DPRESENTFLAG_INTERLACED : 0; + + RESOLUTION_INFO& info = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP); + UpdateDesktopResolution(info, monitorName, w, h, refreshRate, dwFlags); + info.strOutput = strOutput; + + CLog::Log(LOGINFO, "Primary mode: {}", info.strMode); + + // erase previous stored modes + CDisplaySettings::GetInstance().ClearCustomResolutions(); + + for(int mode = 0;; mode++) + { + DEVMODEW devmode = {}; + devmode.dmSize = sizeof(devmode); + if(EnumDisplaySettingsW(details->DeviceNameW.c_str(), mode, &devmode) == 0) + break; + if(devmode.dmBitsPerPel != 32) + continue; + + float refresh; + if ((devmode.dmDisplayFrequency + 1) % 24 == 0 || (devmode.dmDisplayFrequency + 1) % 30 == 0) + refresh = static_cast<float>(devmode.dmDisplayFrequency + 1) / 1.001f; + else + refresh = static_cast<float>(devmode.dmDisplayFrequency); + dwFlags = (devmode.dmDisplayFlags & DM_INTERLACED) ? D3DPRESENTFLAG_INTERLACED : 0; + + RESOLUTION_INFO res; + res.iWidth = devmode.dmPelsWidth; + res.iHeight = devmode.dmPelsHeight; + res.bFullScreen = true; + res.dwFlags = dwFlags; + res.fRefreshRate = refresh; + res.fPixelRatio = 1.0f; + res.iScreenWidth = res.iWidth; + res.iScreenHeight = res.iHeight; + res.iSubtitles = res.iHeight; + res.strMode = StringUtils::Format("{}: {}x{} @ {:.2f}Hz", monitorName, res.iWidth, res.iHeight, + res.fRefreshRate); + GetGfxContext().ResetOverscan(res); + res.strOutput = strOutput; + + if (AddResolution(res)) + CLog::Log(LOGINFO, "Additional mode: {}", res.strMode); + } + + CDisplaySettings::GetInstance().ApplyCalibrations(); +} + +bool CWinSystemWin32::AddResolution(const RESOLUTION_INFO &res) +{ + for (unsigned int i = RES_CUSTOM; i < CDisplaySettings::GetInstance().ResolutionInfoSize(); i++) + { + RESOLUTION_INFO& info = CDisplaySettings::GetInstance().GetResolutionInfo(i); + if (info.iWidth == res.iWidth + && info.iHeight == res.iHeight + && info.iScreenWidth == res.iScreenWidth + && info.iScreenHeight == res.iScreenHeight + && info.fRefreshRate == res.fRefreshRate + && info.dwFlags == res.dwFlags) + return false; // already have this resolution + } + + CDisplaySettings::GetInstance().AddResolutionInfo(res); + return true; +} + +void CWinSystemWin32::ShowOSMouse(bool show) +{ + static int counter = 0; + if ((counter < 0 && show) || (counter >= 0 && !show)) + counter = ShowCursor(show); +} + +bool CWinSystemWin32::Minimize() +{ + ShowWindow(m_hWnd, SW_MINIMIZE); + return true; +} +bool CWinSystemWin32::Restore() +{ + ShowWindow(m_hWnd, SW_RESTORE); + return true; +} +bool CWinSystemWin32::Hide() +{ + ShowWindow(m_hWnd, SW_HIDE); + return true; +} +bool CWinSystemWin32::Show(bool raise) +{ + HWND windowAfter = HWND_BOTTOM; + if (raise) + { + if (m_bFullScreen) + windowAfter = HWND_TOP; + else + windowAfter = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST; + } + + SetWindowPos(m_hWnd, windowAfter, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_ASYNCWINDOWPOS); + UpdateWindow(m_hWnd); + + if (raise) + { + SetForegroundWindow(m_hWnd); + SetFocus(m_hWnd); + } + return true; +} + +void CWinSystemWin32::Register(IDispResource *resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + m_resources.push_back(resource); +} + +void CWinSystemWin32::Unregister(IDispResource* resource) +{ + std::unique_lock<CCriticalSection> lock(m_resourceSection); + std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource); + if (i != m_resources.end()) + m_resources.erase(i); +} + +void CWinSystemWin32::OnDisplayLost() +{ + CLog::LogF(LOGDEBUG, "notify display lost event"); + + { + std::unique_lock<CCriticalSection> lock(m_resourceSection); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnLostDisplay(); + } +} + +void CWinSystemWin32::OnDisplayReset() +{ + if (!m_delayDispReset) + { + CLog::LogF(LOGDEBUG, "notify display reset event"); + std::unique_lock<CCriticalSection> lock(m_resourceSection); + for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i) + (*i)->OnResetDisplay(); + } +} + +void CWinSystemWin32::OnDisplayBack() +{ + auto delay = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + "videoscreen.delayrefreshchange") * + 100); + if (delay > 0ms) + { + m_delayDispReset = true; + m_dispResetTimer.Set(delay); + } + OnDisplayReset(); +} + +void CWinSystemWin32::ResolutionChanged() +{ + OnDisplayLost(); + OnDisplayBack(); +} + +void CWinSystemWin32::SetForegroundWindowInternal(HWND hWnd) +{ + if (!IsWindow(hWnd)) return; + + // if the window isn't focused, bring it to front or SetFullScreen will fail + BYTE keyState[256] = {}; + // to unlock SetForegroundWindow we need to imitate Alt pressing + if (GetKeyboardState(reinterpret_cast<LPBYTE>(&keyState)) && !(keyState[VK_MENU] & 0x80)) + keybd_event(VK_MENU, 0, KEYEVENTF_EXTENDEDKEY | 0, 0); + + BOOL res = SetForegroundWindow(hWnd); + + if (GetKeyboardState(reinterpret_cast<LPBYTE>(&keyState)) && !(keyState[VK_MENU] & 0x80)) + keybd_event(VK_MENU, 0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0); + + if (!res) + { + //relation time of SetForegroundWindow lock + DWORD lockTimeOut = 0; + HWND hCurrWnd = GetForegroundWindow(); + DWORD dwThisTID = GetCurrentThreadId(), + dwCurrTID = GetWindowThreadProcessId(hCurrWnd, nullptr); + + // we need to bypass some limitations from Microsoft + if (dwThisTID != dwCurrTID) + { + AttachThreadInput(dwThisTID, dwCurrTID, TRUE); + SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, &lockTimeOut, 0); + SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, nullptr, SPIF_SENDWININICHANGE | SPIF_UPDATEINIFILE); + AllowSetForegroundWindow(ASFW_ANY); + } + + SetForegroundWindow(hWnd); + + if (dwThisTID != dwCurrTID) + { + SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, &lockTimeOut, SPIF_SENDWININICHANGE | SPIF_UPDATEINIFILE); + AttachThreadInput(dwThisTID, dwCurrTID, FALSE); + } + } +} + +std::unique_ptr<CVideoSync> CWinSystemWin32::GetVideoSync(void *clock) +{ + std::unique_ptr<CVideoSync> pVSync(new CVideoSyncD3D(clock)); + return pVSync; +} + +std::string CWinSystemWin32::GetClipboardText() +{ + std::wstring unicode_text; + std::string utf8_text; + + if (OpenClipboard(nullptr)) + { + HGLOBAL hglb = GetClipboardData(CF_UNICODETEXT); + if (hglb != nullptr) + { + LPWSTR lpwstr = static_cast<LPWSTR>(GlobalLock(hglb)); + if (lpwstr != nullptr) + { + unicode_text = lpwstr; + GlobalUnlock(hglb); + } + } + CloseClipboard(); + } + + return KODI::PLATFORM::WINDOWS::FromW(unicode_text); +} + +bool CWinSystemWin32::UseLimitedColor() +{ + return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE); +} + +void CWinSystemWin32::NotifyAppFocusChange(bool bGaining) +{ + if (m_state == WINDOW_STATE_FULLSCREEN && !m_IsAlteringWindow) + { + m_IsAlteringWindow = true; + ReleaseBackBuffer(); + + if (bGaining) + SetWindowPos(m_hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOREDRAW); + + RESOLUTION_INFO res = {}; + const RESOLUTION resolution = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); + if (bGaining && resolution > RES_INVALID) + res = CDisplaySettings::GetInstance().GetResolutionInfo(resolution); + + SetDeviceFullScreen(bGaining, res); + + if (!bGaining) + SetWindowPos(m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOREDRAW); + + CreateBackBuffer(); + m_IsAlteringWindow = false; + } + m_inFocus = bGaining; +} + +void CWinSystemWin32::UpdateStates(bool fullScreen) +{ + m_fullscreenState = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_FAKEFULLSCREEN) + ? WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW + : WINDOW_FULLSCREEN_STATE_FULLSCREEN; + m_windowState = WINDOW_WINDOW_STATE_WINDOWED; // currently only this allowed + + // set the appropriate window style + if (fullScreen) + { + m_windowStyle = FULLSCREEN_WINDOW_STYLE; + m_windowExStyle = FULLSCREEN_WINDOW_EX_STYLE; + } + else + { + m_windowStyle = WINDOWED_STYLE; + m_windowExStyle = WINDOWED_EX_STYLE; + } +} + +WINDOW_STATE CWinSystemWin32::GetState(bool fullScreen) const +{ + return static_cast<WINDOW_STATE>(fullScreen ? m_fullscreenState : m_windowState); +} + +bool CWinSystemWin32::MessagePump() +{ + return m_winEvents->MessagePump(); +} + +void CWinSystemWin32::SetTogglingHDR(bool toggling) +{ + if (toggling) + SetTimer(m_hWnd, ID_TIMER_HDR, 6000U, nullptr); + + m_IsTogglingHDR = toggling; +} + +RECT CWinSystemWin32::GetVirtualScreenRect() +{ + RECT rect = {}; + rect.left = GetSystemMetrics(SM_XVIRTUALSCREEN); + rect.right = GetSystemMetrics(SM_CXVIRTUALSCREEN) + rect.left; + rect.top = GetSystemMetrics(SM_YVIRTUALSCREEN); + rect.bottom = GetSystemMetrics(SM_CYVIRTUALSCREEN) + rect.top; + + return rect; +} + +int CWinSystemWin32::GetGuiSdrPeakLuminance() const +{ + const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + + return settings->GetInt(CSettings::SETTING_VIDEOSCREEN_GUISDRPEAKLUMINANCE); +} + +RECT CWinSystemWin32::GetScreenWorkArea(HMONITOR handle) const +{ + MONITORINFO monitorInfo{}; + monitorInfo.cbSize = sizeof(MONITORINFO); + if (!GetMonitorInfoW(handle, &monitorInfo)) + { + CLog::LogF(LOGERROR, "GetMonitorInfoW failed with {}", GetLastError()); + return RECT(); + } + return monitorInfo.rcWork; +} + +RECT CWinSystemWin32::GetNcAreaOffsets(DWORD dwStyle, BOOL bMenu, DWORD dwExStyle) const +{ + RECT rcNcArea{}; + SetRectEmpty(&rcNcArea); + + if (!AdjustWindowRectEx(&rcNcArea, dwStyle, false, dwExStyle)) + { + CLog::LogF(LOGERROR, "AdjustWindowRectEx failed with {}", GetLastError()); + return RECT(); + } + return rcNcArea; +} diff --git a/xbmc/windowing/windows/WinSystemWin32.h b/xbmc/windowing/windows/WinSystemWin32.h new file mode 100644 index 0000000..0573295 --- /dev/null +++ b/xbmc/windowing/windows/WinSystemWin32.h @@ -0,0 +1,215 @@ +/* + * 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 + +#include "guilib/DispResource.h" +#include "threads/CriticalSection.h" +#include "threads/SystemClock.h" +#include "windowing/WinSystem.h" + +#include <shellapi.h> +#include <vector> + +static const DWORD WINDOWED_STYLE = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN; +static const DWORD WINDOWED_EX_STYLE = NULL; +static const DWORD FULLSCREEN_WINDOW_STYLE = WS_POPUP | WS_SYSMENU | WS_CLIPCHILDREN; +static const DWORD FULLSCREEN_WINDOW_EX_STYLE = WS_EX_APPWINDOW; +static const UINT ID_TIMER_HDR = 34U; + +/* Controls the way the window appears and behaves. */ +enum WINDOW_STATE +{ + WINDOW_STATE_FULLSCREEN = 1, // Exclusive fullscreen + WINDOW_STATE_FULLSCREEN_WINDOW, // Non-exclusive fullscreen window + WINDOW_STATE_WINDOWED, //Movable window with border + WINDOW_STATE_BORDERLESS //Non-movable window with no border +}; + +static const char* window_state_names[] = +{ + "unknown", + "true fullscreen", + "windowed fullscreen", + "windowed", + "borderless" +}; + +/* WINDOW_STATE restricted to fullscreen modes. */ +enum WINDOW_FULLSCREEN_STATE +{ + WINDOW_FULLSCREEN_STATE_FULLSCREEN = WINDOW_STATE_FULLSCREEN, + WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW = WINDOW_STATE_FULLSCREEN_WINDOW +}; + +/* WINDOW_STATE restricted to windowed modes. */ +enum WINDOW_WINDOW_STATE +{ + WINDOW_WINDOW_STATE_WINDOWED = WINDOW_STATE_WINDOWED, + WINDOW_WINDOW_STATE_BORDERLESS = WINDOW_STATE_BORDERLESS +}; + +struct MONITOR_DETAILS +{ + int ScreenWidth; + int ScreenHeight; + int RefreshRate; + int Bpp; + int DisplayId; + bool Interlaced; + bool IsPrimary; + + HMONITOR hMonitor; + std::wstring MonitorNameW; + std::wstring CardNameW; + std::wstring DeviceNameW; +}; + +class CIRServerSuite; + +class CWinSystemWin32 : public CWinSystemBase +{ +public: + CWinSystemWin32(); + virtual ~CWinSystemWin32(); + + // CWinSystemBase overrides + bool InitWindowSystem() override; + bool DestroyWindowSystem() override; + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + void FinishWindowResize(int newWidth, int newHeight) override; + void UpdateResolutions() override; + bool CenterWindow() override; + virtual void NotifyAppFocusChange(bool bGaining) override; + void ShowOSMouse(bool show) override; + bool HasInertialGestures() override { return true; }//if win32 has touchscreen - it uses the win32 gesture api for inertial scrolling + bool Minimize() override; + bool Restore() override; + bool Hide() override; + bool Show(bool raise = true) override; + std::string GetClipboardText() override; + bool UseLimitedColor() override; + + // videosync + std::unique_ptr<CVideoSync> GetVideoSync(void *clock) override; + + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + + std::vector<std::string> GetConnectedOutputs() override; + + // CWinSystemWin32 + HWND GetHwnd() const { return m_hWnd; } + bool IsAlteringWindow() const { return m_IsAlteringWindow; } + void SetAlteringWindow(bool altering) { m_IsAlteringWindow = altering; } + bool IsTogglingHDR() const { return m_IsTogglingHDR; } + void SetTogglingHDR(bool toggling); + virtual bool DPIChanged(WORD dpi, RECT windowRect) const; + bool IsMinimized() const { return m_bMinimized; } + void SetMinimized(bool minimized); + int GetGuiSdrPeakLuminance() const; + + // touchscreen support + typedef BOOL(WINAPI *pGetGestureInfo)(HGESTUREINFO, PGESTUREINFO); + typedef BOOL(WINAPI *pSetGestureConfig)(HWND, DWORD, UINT, PGESTURECONFIG, UINT); + typedef BOOL(WINAPI *pCloseGestureInfoHandle)(HGESTUREINFO); + typedef BOOL(WINAPI *pEnableNonClientDpiScaling)(HWND); + pGetGestureInfo PtrGetGestureInfo; + pSetGestureConfig PtrSetGestureConfig; + pCloseGestureInfoHandle PtrCloseGestureInfoHandle; + pEnableNonClientDpiScaling PtrEnableNonClientDpiScaling; + + void SetSizeMoveMode(bool mode) { m_bSizeMoveEnabled = mode; } + bool IsInSizeMoveMode() const { return m_bSizeMoveEnabled; } + + // winevents override + bool MessagePump() override; + +protected: + bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override = 0; + virtual void UpdateStates(bool fullScreen); + WINDOW_STATE GetState(bool fullScreen) const; + virtual void SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) = 0; + virtual void ReleaseBackBuffer() = 0; + virtual void CreateBackBuffer() = 0; + virtual void ResizeDeviceBuffers() = 0; + virtual bool IsStereoEnabled() = 0; + virtual void OnScreenChange(HMONITOR monitor) = 0; + virtual void AdjustWindow(bool forceResize = false); + void CenterCursor() const; + + virtual void Register(IDispResource *resource); + virtual void Unregister(IDispResource *resource); + + virtual bool ChangeResolution(const RESOLUTION_INFO& res, bool forceChange = false); + virtual bool CreateBlankWindows(); + virtual bool BlankNonActiveMonitors(bool bBlank); + MONITOR_DETAILS* GetDisplayDetails(const std::string& name); + MONITOR_DETAILS* GetDisplayDetails(HMONITOR handle); + void RestoreDesktopResolution(MONITOR_DETAILS* details); + RECT ScreenRect(HMONITOR handle); + void GetConnectedDisplays(std::vector<MONITOR_DETAILS>& outputs); + + + /*! + \brief Adds a resolution to the list of resolutions if we don't already have it + \param res resolution to add. + */ + static bool AddResolution(const RESOLUTION_INFO &res); + + void OnDisplayLost(); + void OnDisplayReset(); + void OnDisplayBack(); + void ResolutionChanged(); + static void SetForegroundWindowInternal(HWND hWnd); + static RECT GetVirtualScreenRect(); + /*! + * Retrieve the work area of the screen (exclude task bar and other occlusions) + */ + RECT GetScreenWorkArea(HMONITOR handle) const; + /*! + * Retrieve size of the title bar and borders + * Add to coordinates to convert client coordinates to window coordinates + * Substract from coordinates to convert from window coordinates to client coordinates + */ + RECT GetNcAreaOffsets(DWORD dwStyle, BOOL bMenu, DWORD dwExStyle) const; + + HWND m_hWnd; + HMONITOR m_hMonitor; + + std::vector<HWND> m_hBlankWindows; + HINSTANCE m_hInstance; + HICON m_hIcon; + bool m_ValidWindowedPosition; + bool m_IsAlteringWindow; + bool m_IsTogglingHDR{false}; + + CCriticalSection m_resourceSection; + std::vector<IDispResource*> m_resources; + bool m_delayDispReset; + XbmcThreads::EndTime<> m_dispResetTimer; + + WINDOW_STATE m_state; // the state of the window + WINDOW_FULLSCREEN_STATE m_fullscreenState; // the state of the window when in fullscreen + WINDOW_WINDOW_STATE m_windowState; // the state of the window when in windowed + DWORD m_windowStyle; // the style of the window + DWORD m_windowExStyle; // the ex style of the window + bool m_inFocus; + bool m_bMinimized; + bool m_bSizeMoveEnabled = false; + bool m_bFirstResChange = true; + std::unique_ptr<CIRServerSuite> m_irss; + std::vector<MONITOR_DETAILS> m_displays; + + NOTIFYICONDATA m_trayIcon = {}; + + static const char* SETTING_WINDOW_TOP; + static const char* SETTING_WINDOW_LEFT; +}; + +extern HWND g_hWnd; + diff --git a/xbmc/windowing/windows/WinSystemWin32DX.cpp b/xbmc/windowing/windows/WinSystemWin32DX.cpp new file mode 100644 index 0000000..6d568fc --- /dev/null +++ b/xbmc/windowing/windows/WinSystemWin32DX.cpp @@ -0,0 +1,447 @@ +/* + * 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 "WinSystemWin32DX.h" + +#include "commons/ilog.h" +#include "rendering/dx/RenderContext.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/SystemInfo.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "windowing/GraphicContext.h" +#include "windowing/WindowSystemFactory.h" + +#include "platform/win32/CharsetConverter.h" +#include "platform/win32/WIN32Util.h" + +#ifndef _M_X64 +#include "utils/SystemInfo.h" +#endif +#if _DEBUG +#pragma comment(lib, "detoursd.lib") +#else +#pragma comment(lib, "detours.lib") +#endif +#pragma comment(lib, "dxgi.lib") +#include <windows.h> +#include <winnt.h> +#include <winternl.h> +#pragma warning(disable: 4091) +#include <d3d10umddi.h> +#pragma warning(default: 4091) +#include <detours.h> + +using KODI::PLATFORM::WINDOWS::FromW; + +using namespace std::chrono_literals; + +// User Mode Driver hooks definitions +void APIENTRY HookCreateResource(D3D10DDI_HDEVICE hDevice, const D3D10DDIARG_CREATERESOURCE* pResource, D3D10DDI_HRESOURCE hResource, D3D10DDI_HRTRESOURCE hRtResource); +HRESULT APIENTRY HookCreateDevice(D3D10DDI_HADAPTER hAdapter, D3D10DDIARG_CREATEDEVICE* pCreateData); +HRESULT APIENTRY HookOpenAdapter10_2(D3D10DDIARG_OPENADAPTER *pOpenData); +static PFND3D10DDI_OPENADAPTER s_fnOpenAdapter10_2{ nullptr }; +static PFND3D10DDI_CREATEDEVICE s_fnCreateDeviceOrig{ nullptr }; +static PFND3D10DDI_CREATERESOURCE s_fnCreateResourceOrig{ nullptr }; + +void CWinSystemWin32DX::Register() +{ + KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem); +} + +std::unique_ptr<CWinSystemBase> CWinSystemWin32DX::CreateWinSystem() +{ + return std::make_unique<CWinSystemWin32DX>(); +} + +CWinSystemWin32DX::CWinSystemWin32DX() : CRenderSystemDX() + , m_hDriverModule(nullptr) +{ +} + +CWinSystemWin32DX::~CWinSystemWin32DX() +{ +} + +void CWinSystemWin32DX::PresentRenderImpl(bool rendered) +{ + if (rendered) + m_deviceResources->Present(); + + if (m_delayDispReset && m_dispResetTimer.IsTimePast()) + { + m_delayDispReset = false; + OnDisplayReset(); + } + + if (!rendered) + KODI::TIME::Sleep(40ms); +} + +bool CWinSystemWin32DX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) +{ + const MONITOR_DETAILS* monitor = GetDisplayDetails(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR)); + if (!monitor) + return false; + + m_hMonitor = monitor->hMonitor; + m_deviceResources = DX::DeviceResources::Get(); + // setting monitor before creating window for proper hooking into a driver + m_deviceResources->SetMonitor(m_hMonitor); + + return CWinSystemWin32::CreateNewWindow(name, fullScreen, res) && m_deviceResources->HasValidDevice(); +} + +void CWinSystemWin32DX::SetWindow(HWND hWnd) const +{ + m_deviceResources->SetWindow(hWnd); +} + +bool CWinSystemWin32DX::DestroyRenderSystem() +{ + CRenderSystemDX::DestroyRenderSystem(); + + m_deviceResources->Release(); + m_deviceResources.reset(); + return true; +} + +void CWinSystemWin32DX::SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) +{ + if (m_deviceResources->SetFullScreen(fullScreen, res)) + { + ResolutionChanged(); + } +} + +bool CWinSystemWin32DX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) +{ + CWinSystemWin32::ResizeWindow(newWidth, newHeight, newLeft, newTop); + CRenderSystemDX::OnResize(); + + return true; +} + +void CWinSystemWin32DX::OnMove(int x, int y) +{ + // do not handle moving at window creation because MonitorFromWindow + // returns default system monitor in case of m_hWnd is null + if (!m_hWnd) + return; + + HMONITOR newMonitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST); + if (newMonitor != m_hMonitor) + { + MONITOR_DETAILS* details = GetDisplayDetails(newMonitor); + + if (!details) + return; + + CDisplaySettings::GetInstance().SetMonitor(KODI::PLATFORM::WINDOWS::FromW(details->MonitorNameW)); + m_deviceResources->SetMonitor(newMonitor); + m_hMonitor = newMonitor; + } + + // Save window position if not fullscreen + if (!IsFullScreen() && (m_nLeft != x || m_nTop != y)) + { + m_nLeft = x; + m_nTop = y; + const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->SetInt(SETTING_WINDOW_LEFT, x); + settings->SetInt(SETTING_WINDOW_TOP, y); + settings->Save(); + } +} + +bool CWinSystemWin32DX::DPIChanged(WORD dpi, RECT windowRect) const +{ + // on Win10 FCU the OS keeps window size exactly the same size as it was + if (CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin10_1709)) + return true; + + m_deviceResources->SetDpi(dpi); + if (!IsAlteringWindow()) + return __super::DPIChanged(dpi, windowRect); + + return true; +} + +void CWinSystemWin32DX::ReleaseBackBuffer() +{ + m_deviceResources->ReleaseBackBuffer(); +} + +void CWinSystemWin32DX::CreateBackBuffer() +{ + m_deviceResources->CreateBackBuffer(); +} + +void CWinSystemWin32DX::ResizeDeviceBuffers() +{ + m_deviceResources->ResizeBuffers(); +} + +bool CWinSystemWin32DX::IsStereoEnabled() +{ + return m_deviceResources->IsStereoEnabled(); +} + +void CWinSystemWin32DX::OnScreenChange(HMONITOR monitor) +{ + m_deviceResources->SetMonitor(monitor); +} + +bool CWinSystemWin32DX::ChangeResolution(const RESOLUTION_INFO &res, bool forceChange) +{ + bool changed = CWinSystemWin32::ChangeResolution(res, forceChange); + // this is a try to fix FCU issue after changing resolution + if (m_deviceResources && changed) + m_deviceResources->ResizeBuffers(); + return changed; +} + +void CWinSystemWin32DX::OnResize(int width, int height) +{ + if (!m_IsAlteringWindow) + ReleaseBackBuffer(); + + m_deviceResources->SetLogicalSize(static_cast<float>(width), static_cast<float>(height)); + + if (!m_IsAlteringWindow) + CreateBackBuffer(); +} + +bool CWinSystemWin32DX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) +{ + bool const result = CWinSystemWin32::SetFullScreen(fullScreen, res, blankOtherDisplays); + CRenderSystemDX::OnResize(); + return result; +} + +void CWinSystemWin32DX::UninitHooks() +{ + // uninstall + if (!s_fnOpenAdapter10_2) + return; + + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + DetourDetach(reinterpret_cast<void**>(&s_fnOpenAdapter10_2), HookOpenAdapter10_2); + DetourTransactionCommit(); + if (m_hDriverModule) + { + FreeLibrary(m_hDriverModule); + m_hDriverModule = nullptr; + } +} + +void CWinSystemWin32DX::InitHooks(IDXGIOutput* pOutput) +{ + DXGI_OUTPUT_DESC outputDesc; + if (!pOutput || FAILED(pOutput->GetDesc(&outputDesc))) + return; + + DISPLAY_DEVICEW displayDevice; + displayDevice.cb = sizeof(DISPLAY_DEVICEW); + DWORD adapter = 0; + bool deviceFound = false; + + // delete exiting hooks. + if (s_fnOpenAdapter10_2) + UninitHooks(); + + // enum devices to find matched + while (EnumDisplayDevicesW(nullptr, adapter, &displayDevice, 0)) + { + if (wcscmp(displayDevice.DeviceName, outputDesc.DeviceName) == 0) + { + deviceFound = true; + break; + } + adapter++; + } + if (!deviceFound) + return; + + CLog::LogF(LOGDEBUG, "Hooking into UserModeDriver on device {}. ", + FromW(displayDevice.DeviceKey)); + const wchar_t* keyName = +#ifndef _M_X64 + // on x64 system and x32 build use UserModeDriverNameWow key + CSysInfo::GetKernelBitness() == 64 ? keyName = L"UserModeDriverNameWow" : +#endif // !_WIN64 + L"UserModeDriverName"; + + DWORD dwType = REG_MULTI_SZ; + HKEY hKey = nullptr; + wchar_t value[1024]; + DWORD valueLength = sizeof(value); + LSTATUS lstat; + + // to void \Registry\Machine at the beginning, we use shifted pointer at 18 + if (ERROR_SUCCESS == (lstat = RegOpenKeyExW(HKEY_LOCAL_MACHINE, displayDevice.DeviceKey + 18, 0, KEY_READ, &hKey)) + && ERROR_SUCCESS == (lstat = RegQueryValueExW(hKey, keyName, nullptr, &dwType, (LPBYTE)&value, &valueLength))) + { + // 1. registry value has a list of drivers for each API with the following format: dx9\0dx10\0dx11\0dx12\0\0 + // 2. we split the value by \0 + std::vector<std::wstring> drivers; + const wchar_t* pValue = value; + while (*pValue) + { + drivers.push_back(std::wstring(pValue)); + pValue += drivers.back().size() + 1; + } + // no entries in the registry + if (drivers.empty()) + return; + // 3. we take only first three values (dx12 driver isn't needed if it exists ofc) + if (drivers.size() > 3) + drivers = std::vector<std::wstring>(drivers.begin(), drivers.begin() + 3); + // 4. and then iterate with reverse order to start iterate with the best candidate for d3d11 driver + for (auto it = drivers.rbegin(); it != drivers.rend(); ++it) + { + m_hDriverModule = LoadLibraryW(it->c_str()); + if (m_hDriverModule != nullptr) + { + s_fnOpenAdapter10_2 = reinterpret_cast<PFND3D10DDI_OPENADAPTER>(GetProcAddress(m_hDriverModule, "OpenAdapter10_2")); + if (s_fnOpenAdapter10_2 != nullptr) + { + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + DetourAttach(reinterpret_cast<void**>(&s_fnOpenAdapter10_2), HookOpenAdapter10_2); + if (NO_ERROR == DetourTransactionCommit()) + // install and activate hook into a driver + { + CLog::LogF(LOGDEBUG, "D3D11 hook installed and activated."); + break; + } + else + { + CLog::Log(LOGDEBUG, __FUNCTION__": Unable to install and activate D3D11 hook."); + s_fnOpenAdapter10_2 = nullptr; + FreeLibrary(m_hDriverModule); + m_hDriverModule = nullptr; + } + } + } + } + } + + if (lstat != ERROR_SUCCESS) + CLog::LogF(LOGDEBUG, "error open registry key with error {}.", lstat); + + if (hKey != nullptr) + RegCloseKey(hKey); +} + +void CWinSystemWin32DX::FixRefreshRateIfNecessary(const D3D10DDIARG_CREATERESOURCE* pResource) const +{ + if (pResource && pResource->pPrimaryDesc) + { + float refreshRate = RATIONAL_TO_FLOAT(pResource->pPrimaryDesc->ModeDesc.RefreshRate); + if (refreshRate > 10.0f && refreshRate < 300.0f) + { + // interlaced + if (pResource->pPrimaryDesc->ModeDesc.ScanlineOrdering > DXGI_DDI_MODE_SCANLINE_ORDER_PROGRESSIVE) + refreshRate /= 2; + + uint32_t refreshNum, refreshDen; + DX::GetRefreshRatio(static_cast<uint32_t>(floor(m_fRefreshRate)), &refreshNum, &refreshDen); + float diff = fabs(refreshRate - static_cast<float>(refreshNum) / static_cast<float>(refreshDen)) / refreshRate; + CLog::LogF(LOGDEBUG, + "refreshRate: {:0.4f}, desired: {:0.4f}, deviation: {:.5f}, fixRequired: {}, {}", + refreshRate, m_fRefreshRate, diff, (diff > 0.0005 && diff < 0.1) ? "yes" : "no", + pResource->pPrimaryDesc->Flags); + if (diff > 0.0005 && diff < 0.1) + { + pResource->pPrimaryDesc->ModeDesc.RefreshRate.Numerator = refreshNum; + pResource->pPrimaryDesc->ModeDesc.RefreshRate.Denominator = refreshDen; + if (pResource->pPrimaryDesc->ModeDesc.ScanlineOrdering > DXGI_DDI_MODE_SCANLINE_ORDER_PROGRESSIVE) + pResource->pPrimaryDesc->ModeDesc.RefreshRate.Numerator *= 2; + CLog::LogF(LOGDEBUG, "refreshRate fix applied -> {:0.3f}", + RATIONAL_TO_FLOAT(pResource->pPrimaryDesc->ModeDesc.RefreshRate)); + } + } + } +} + +void APIENTRY HookCreateResource(D3D10DDI_HDEVICE hDevice, const D3D10DDIARG_CREATERESOURCE* pResource, D3D10DDI_HRESOURCE hResource, D3D10DDI_HRTRESOURCE hRtResource) +{ + if (pResource && pResource->pPrimaryDesc) + { + DX::Windowing()->FixRefreshRateIfNecessary(pResource); + } + s_fnCreateResourceOrig(hDevice, pResource, hResource, hRtResource); +} + +HRESULT APIENTRY HookCreateDevice(D3D10DDI_HADAPTER hAdapter, D3D10DDIARG_CREATEDEVICE* pCreateData) +{ + HRESULT hr = s_fnCreateDeviceOrig(hAdapter, pCreateData); + if (pCreateData->pDeviceFuncs->pfnCreateResource) + { + CLog::LogF(LOGDEBUG, "hook into pCreateData->pDeviceFuncs->pfnCreateResource"); + s_fnCreateResourceOrig = pCreateData->pDeviceFuncs->pfnCreateResource; + pCreateData->pDeviceFuncs->pfnCreateResource = HookCreateResource; + } + return hr; +} + +HRESULT APIENTRY HookOpenAdapter10_2(D3D10DDIARG_OPENADAPTER *pOpenData) +{ + HRESULT hr = s_fnOpenAdapter10_2(pOpenData); + if (pOpenData->pAdapterFuncs->pfnCreateDevice) + { + CLog::LogF(LOGDEBUG, "hook into pOpenData->pAdapterFuncs->pfnCreateDevice"); + s_fnCreateDeviceOrig = pOpenData->pAdapterFuncs->pfnCreateDevice; + pOpenData->pAdapterFuncs->pfnCreateDevice = HookCreateDevice; + } + return hr; +} + +bool CWinSystemWin32DX::IsHDRDisplay() +{ + return (CWIN32Util::GetWindowsHDRStatus() != HDR_STATUS::HDR_UNSUPPORTED); +} + +HDR_STATUS CWinSystemWin32DX::GetOSHDRStatus() +{ + return CWIN32Util::GetWindowsHDRStatus(); +} + +HDR_STATUS CWinSystemWin32DX::ToggleHDR() +{ + return m_deviceResources->ToggleHDR(); +} + +bool CWinSystemWin32DX::IsHDROutput() const +{ + return m_deviceResources->IsHDROutput(); +} + +bool CWinSystemWin32DX::IsTransferPQ() const +{ + return m_deviceResources->IsTransferPQ(); +} + +void CWinSystemWin32DX::SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const +{ + m_deviceResources->SetHdrMetaData(hdr10); +} + +void CWinSystemWin32DX::SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace) const +{ + m_deviceResources->SetHdrColorSpace(colorSpace); +} + +DEBUG_INFO_RENDER CWinSystemWin32DX::GetDebugInfo() +{ + return m_deviceResources->GetDebugInfo(); +} diff --git a/xbmc/windowing/windows/WinSystemWin32DX.h b/xbmc/windowing/windows/WinSystemWin32DX.h new file mode 100644 index 0000000..d3673de --- /dev/null +++ b/xbmc/windowing/windows/WinSystemWin32DX.h @@ -0,0 +1,96 @@ +/* + * 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 + +#include "HDRStatus.h" +#include "rendering/dx/RenderSystemDX.h" +#include "windowing/windows/WinSystemWin32.h" + +struct D3D10DDIARG_CREATERESOURCE; + +class CWinSystemWin32DX : public CWinSystemWin32, public CRenderSystemDX +{ + friend interface DX::IDeviceNotify; +public: + CWinSystemWin32DX(); + ~CWinSystemWin32DX(); + + static void Register(); + static std::unique_ptr<CWinSystemBase> CreateWinSystem(); + + // Implementation of CWinSystemBase via CWinSystemWin32 + CRenderSystemBase *GetRenderSystem() override { return this; } + bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override; + bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override; + bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override; + void PresentRenderImpl(bool rendered) override; + bool DPIChanged(WORD dpi, RECT windowRect) const override; + void SetWindow(HWND hWnd) const; + bool DestroyRenderSystem() override; + void* GetHWContext() override { return m_deviceResources->GetD3DContext(); } + + void UninitHooks(); + void InitHooks(IDXGIOutput* pOutput); + + void OnMove(int x, int y) override; + void OnResize(int width, int height); + + /*! + \brief Register as a dependent of the DirectX Render System + Resources should call this on construction if they're dependent on the Render System + for survival. Any resources that registers will get callbacks on loss and reset of + device. In addition, callbacks for destruction and creation of the device are also called, + where any resources dependent on the DirectX device should be destroyed and recreated. + \sa Unregister, ID3DResource + */ + void Register(ID3DResource *resource) const + { + m_deviceResources->Register(resource); + }; + /*! + \brief Unregister as a dependent of the DirectX Render System + Resources should call this on destruction if they're a dependent on the Render System + \sa Register, ID3DResource + */ + void Unregister(ID3DResource *resource) const + { + m_deviceResources->Unregister(resource); + }; + + void Register(IDispResource* resource) override { CWinSystemWin32::Register(resource); } + void Unregister(IDispResource* resource) override { CWinSystemWin32::Unregister(resource); } + + void FixRefreshRateIfNecessary(const D3D10DDIARG_CREATERESOURCE* pResource) const; + + // HDR OS/display override + bool IsHDRDisplay() override; + HDR_STATUS ToggleHDR() override; + HDR_STATUS GetOSHDRStatus() override; + + // HDR support + bool IsHDROutput() const; + bool IsTransferPQ() const; + void SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const; + void SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace) const; + + // Get debug info from swapchain + DEBUG_INFO_RENDER GetDebugInfo() override; + +protected: + void SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) override; + void ReleaseBackBuffer() override; + void CreateBackBuffer() override; + void ResizeDeviceBuffers() override; + bool IsStereoEnabled() override; + void OnScreenChange(HMONITOR monitor) override; + bool ChangeResolution(const RESOLUTION_INFO& res, bool forceChange = false) override; + + HMODULE m_hDriverModule; +}; + |