Index: ps/trunk/source/ps/VideoMode.cpp =================================================================== --- ps/trunk/source/ps/VideoMode.cpp (revision 26050) +++ ps/trunk/source/ps/VideoMode.cpp (revision 26051) @@ -1,760 +1,765 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "VideoMode.h" #include "graphics/GameView.h" #include "gui/GUIManager.h" #include "lib/config2.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/sysdep/gfx.h" #include "lib/tex/tex.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStr.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Pyrogenesis.h" #include "renderer/Renderer.h" namespace { int DEFAULT_WINDOW_W = 1024; int DEFAULT_WINDOW_H = 768; int DEFAULT_FULLSCREEN_W = 1024; int DEFAULT_FULLSCREEN_H = 768; const wchar_t DEFAULT_CURSOR_NAME[] = L"default-arrow"; } // anonymous namespace #if OS_WIN // We can't include wutil directly because GL headers conflict with Windows // until we use a proper GL loader. extern void wutil_SetAppWindow(SDL_Window* window); // After a proper HiDPI integration we should switch to manifest until // SDL has an implemented HiDPI on Windows. extern void wutil_EnableHiDPIOnWindows(); #endif CVideoMode g_VideoMode; class CVideoMode::CCursor { public: enum class CursorBackend { SDL, SYSTEM }; CCursor(); ~CCursor(); void SetCursor(const CStrW& name); void ResetCursor(); private: CursorBackend m_CursorBackend = CursorBackend::SYSTEM; SDL_Surface* m_CursorSurface = nullptr; SDL_Cursor* m_Cursor = nullptr; CStrW m_CursorName; }; CVideoMode::CCursor::CCursor() { std::string cursorBackend; CFG_GET_VAL("cursorbackend", cursorBackend); if (cursorBackend == "sdl") m_CursorBackend = CursorBackend::SDL; else m_CursorBackend = CursorBackend::SYSTEM; ResetCursor(); } CVideoMode::CCursor::~CCursor() { if (m_Cursor) SDL_FreeCursor(m_Cursor); if (m_CursorSurface) SDL_FreeSurface(m_CursorSurface); } void CVideoMode::CCursor::SetCursor(const CStrW& name) { if (m_CursorBackend == CursorBackend::SYSTEM || m_CursorName == name) return; m_CursorName = name; if (m_Cursor) SDL_FreeCursor(m_Cursor); if (m_CursorSurface) SDL_FreeSurface(m_CursorSurface); if (name.empty()) { SDL_ShowCursor(SDL_DISABLE); return; } const VfsPath pathBaseName(VfsPath(L"art/textures/cursors") / name); // Read pixel offset of the cursor's hotspot [the bit of it that's // drawn at (g_mouse_x,g_mouse_y)] from file. int hotspotX = 0, hotspotY = 0; { const VfsPath pathHotspotName = pathBaseName.ChangeExtension(L".txt"); std::shared_ptr buffer; size_t size; if (g_VFS->LoadFile(pathHotspotName, buffer, size) != INFO::OK) { LOGERROR("Can't load hotspot for cursor: %s", pathHotspotName.string8().c_str()); return; } std::wstringstream s(std::wstring(reinterpret_cast(buffer.get()), size)); s >> hotspotX >> hotspotY; } const VfsPath pathImageName = pathBaseName.ChangeExtension(L".png"); std::shared_ptr file; size_t fileSize; if (g_VFS->LoadFile(pathImageName, file, fileSize) != INFO::OK) { LOGERROR("Can't load image for cursor: %s", pathImageName.string8().c_str()); return; } Tex t; if (t.decode(file, fileSize) != INFO::OK) { LOGERROR("Can't decode image for cursor"); return; } // Convert to required BGRA format. const size_t flags = (t.m_Flags | TEX_BGR) & ~TEX_DXT; if (t.transform_to(flags) != INFO::OK) { LOGERROR("Can't transform image for cursor"); return; } void* imageBGRA = t.get_data(); if (!imageBGRA) { LOGERROR("Transformed image is empty for cursor"); return; } m_CursorSurface = SDL_CreateRGBSurfaceFrom(imageBGRA, static_cast(t.m_Width), static_cast(t.m_Height), 32, static_cast(t.m_Width * 4), 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); if (!m_CursorSurface) { LOGERROR("Can't create surface for cursor: %s", SDL_GetError()); return; } const float scale = g_VideoMode.GetScale(); if (scale != 1.0) { SDL_Surface* scaledSurface = SDL_CreateRGBSurface(0, m_CursorSurface->w * scale, m_CursorSurface->h * scale, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); if (!scaledSurface) { LOGERROR("Can't create scaled surface forcursor: %s", SDL_GetError()); return; } if (SDL_BlitScaled(m_CursorSurface, nullptr, scaledSurface, nullptr)) return; SDL_FreeSurface(m_CursorSurface); m_CursorSurface = scaledSurface; } m_Cursor = SDL_CreateColorCursor(m_CursorSurface, hotspotX, hotspotY); if (!m_Cursor) { LOGERROR("Can't create cursor: %s", SDL_GetError()); return; } SDL_SetCursor(m_Cursor); } void CVideoMode::CCursor::ResetCursor() { SetCursor(DEFAULT_CURSOR_NAME); } CVideoMode::CVideoMode() : m_WindowedW(DEFAULT_WINDOW_W), m_WindowedH(DEFAULT_WINDOW_H), m_WindowedX(0), m_WindowedY(0) { } CVideoMode::~CVideoMode() = default; void CVideoMode::ReadConfig() { bool windowed = !m_ConfigFullscreen; CFG_GET_VAL("windowed", windowed); m_ConfigFullscreen = !windowed; CFG_GET_VAL("gui.scale", m_Scale); CFG_GET_VAL("xres", m_ConfigW); CFG_GET_VAL("yres", m_ConfigH); CFG_GET_VAL("bpp", m_ConfigBPP); CFG_GET_VAL("display", m_ConfigDisplay); CFG_GET_VAL("hidpi", m_ConfigEnableHiDPI); CFG_GET_VAL("vsync", m_ConfigVSync); } bool CVideoMode::SetVideoMode(int w, int h, int bpp, bool fullscreen) { Uint32 flags = 0; if (fullscreen) flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; if (!m_Window) { #if OS_WIN if (m_ConfigEnableHiDPI) wutil_EnableHiDPIOnWindows(); #endif // Note: these flags only take affect in SDL_CreateWindow flags |= SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE; if (m_ConfigEnableHiDPI) flags |= SDL_WINDOW_ALLOW_HIGHDPI; m_WindowedX = m_WindowedY = SDL_WINDOWPOS_CENTERED_DISPLAY(m_ConfigDisplay); m_Window = SDL_CreateWindow(main_window_name, m_WindowedX, m_WindowedY, w, h, flags); if (!m_Window) { // If fullscreen fails, try windowed mode if (fullscreen) { LOGWARNING("Failed to set the video mode to fullscreen for the chosen resolution " "%dx%d:%d (\"%hs\"), falling back to windowed mode", w, h, bpp, SDL_GetError()); // Using default size for the window for now, as the attempted setting // could be as large, or larger than the screen size. return SetVideoMode(DEFAULT_WINDOW_W, DEFAULT_WINDOW_H, bpp, false); } else { LOGERROR("SetVideoMode failed in SDL_CreateWindow: %dx%d:%d %d (\"%s\")", w, h, bpp, fullscreen ? 1 : 0, SDL_GetError()); return false; } } if (SDL_SetWindowDisplayMode(m_Window, NULL) < 0) { LOGERROR("SetVideoMode failed in SDL_SetWindowDisplayMode: %dx%d:%d %d (\"%s\")", w, h, bpp, fullscreen ? 1 : 0, SDL_GetError()); return false; } m_Context = SDL_GL_CreateContext(m_Window); if (!m_Context) { LOGERROR("SetVideoMode failed in SDL_GL_CreateContext: %dx%d:%d %d (\"%s\")", w, h, bpp, fullscreen ? 1 : 0, SDL_GetError()); return false; } } else { if (m_IsFullscreen != fullscreen) { if (!fullscreen) { // For some reason, when switching from fullscreen to windowed mode, // we have to set the window size and position before and after switching SDL_SetWindowSize(m_Window, w, h); SDL_SetWindowPosition(m_Window, m_WindowedX, m_WindowedY); } if (SDL_SetWindowFullscreen(m_Window, flags) < 0) { LOGERROR("SetVideoMode failed in SDL_SetWindowFullscreen: %dx%d:%d %d (\"%s\")", w, h, bpp, fullscreen ? 1 : 0, SDL_GetError()); return false; } } if (!fullscreen) { SDL_SetWindowSize(m_Window, w, h); SDL_SetWindowPosition(m_Window, m_WindowedX, m_WindowedY); } } // Grab the current video settings SDL_GetWindowSize(m_Window, &m_CurrentW, &m_CurrentH); m_CurrentBPP = bpp; if (fullscreen) SDL_SetWindowGrab(m_Window, SDL_TRUE); else SDL_SetWindowGrab(m_Window, SDL_FALSE); #if OS_WIN // We need to set the window for an error dialog. wutil_SetAppWindow(m_Window); #endif m_IsFullscreen = fullscreen; g_xres = m_CurrentW; g_yres = m_CurrentH; return true; } bool CVideoMode::InitSDL() { ENSURE(!m_IsInitialised); ReadConfig(); // preferred video mode = current desktop settings // (command line params may override these) // TODO: handle multi-screen and HiDPI properly. SDL_DisplayMode mode; if (SDL_GetDesktopDisplayMode(0, &mode) == 0) { m_PreferredW = mode.w; m_PreferredH = mode.h; m_PreferredBPP = SDL_BITSPERPIXEL(mode.format); m_PreferredFreq = mode.refresh_rate; } int w = m_ConfigW; int h = m_ConfigH; if (m_ConfigFullscreen) { // If fullscreen and no explicit size set, default to the desktop resolution if (w == 0 || h == 0) { w = m_PreferredW; h = m_PreferredH; } } // If no size determined, default to something sensible if (w == 0 || h == 0) { w = DEFAULT_WINDOW_W; h = DEFAULT_WINDOW_H; } if (!m_ConfigFullscreen) { // Limit the window to the screen size (if known) if (m_PreferredW) w = std::min(w, m_PreferredW); if (m_PreferredH) h = std::min(h, m_PreferredH); } int bpp = GetBestBPP(); SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); bool forceGLVersion = false; CFG_GET_VAL("forceglversion", forceGLVersion); if (forceGLVersion) { CStr forceGLProfile = "compatibility"; int forceGLMajorVersion = 3; int forceGLMinorVersion = 0; CFG_GET_VAL("forceglprofile", forceGLProfile); CFG_GET_VAL("forceglmajorversion", forceGLMajorVersion); CFG_GET_VAL("forceglminorversion", forceGLMinorVersion); int profile = SDL_GL_CONTEXT_PROFILE_COMPATIBILITY; if (forceGLProfile == "es") profile = SDL_GL_CONTEXT_PROFILE_ES; else if (forceGLProfile == "core") profile = SDL_GL_CONTEXT_PROFILE_CORE; else if (forceGLProfile != "compatibility") LOGWARNING("Unknown force GL profile '%s', compatibility profile is used", forceGLProfile.c_str()); if (forceGLMajorVersion < 1 || forceGLMinorVersion < 0) { LOGERROR("Unsupported force GL version: %d.%d", forceGLMajorVersion, forceGLMinorVersion); } else { SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profile); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, forceGLMajorVersion); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, forceGLMinorVersion); } } else { #if CONFIG2_GLES // Require GLES 2.0 SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); #else + // Some macOS and MESA drivers might not create a context even if they can + // with the core profile. So disable it for a while until we can guarantee + // its creation. +#if OS_WIN SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); +#endif SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); #endif } if (!SetVideoMode(w, h, bpp, m_ConfigFullscreen)) { // Fall back to a smaller depth buffer // (The rendering may be ugly but this helps when running in VMware) SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); if (!SetVideoMode(w, h, bpp, m_ConfigFullscreen)) return false; } SDL_GL_SetSwapInterval(m_ConfigVSync ? 1 : 0); // Work around a bug in the proprietary Linux ATI driver (at least versions 8.16.20 and 8.14.13). // The driver appears to register its own atexit hook on context creation. // If this atexit hook is called before SDL_Quit destroys the OpenGL context, // some kind of double-free problem causes a crash and lockup in the driver. // Calling SDL_Quit twice appears to be harmless, though, and avoids the problem // by destroying the context *before* the driver's atexit hook is called. // (Note that atexit hooks are guaranteed to be called in reverse order of their registration.) atexit(SDL_Quit); // End work around. ogl_Init(); // required after each mode change // (TODO: does that mean we need to call this when toggling fullscreen later?) m_IsInitialised = true; if (!m_ConfigFullscreen) { m_WindowedW = w; m_WindowedH = h; } SetWindowIcon(); m_Cursor = std::make_unique(); return true; } bool CVideoMode::InitNonSDL() { ENSURE(!m_IsInitialised); ReadConfig(); m_IsInitialised = true; return true; } void CVideoMode::Shutdown() { ENSURE(m_IsInitialised); m_Cursor.reset(); m_IsFullscreen = false; m_IsInitialised = false; if (m_Context) { SDL_GL_DeleteContext(m_Context); m_Context = nullptr; } if (m_Window) { SDL_DestroyWindow(m_Window); m_Window = nullptr; } } bool CVideoMode::ResizeWindow(int w, int h) { ENSURE(m_IsInitialised); // Ignore if not windowed if (m_IsFullscreen) return true; // Ignore if the size hasn't changed if (w == m_WindowedW && h == m_WindowedH) return true; int bpp = GetBestBPP(); if (!SetVideoMode(w, h, bpp, false)) return false; m_WindowedW = w; m_WindowedH = h; UpdateRenderer(w, h); return true; } void CVideoMode::Rescale(float scale) { ENSURE(m_IsInitialised); m_Scale = scale; UpdateRenderer(m_CurrentW, m_CurrentH); } float CVideoMode::GetScale() const { return m_Scale; } bool CVideoMode::SetFullscreen(bool fullscreen) { // This might get called before initialisation by psDisplayError; // if so then silently fail if (!m_IsInitialised) return false; // Check whether this is actually a change if (fullscreen == m_IsFullscreen) return true; if (!m_IsFullscreen) { // Windowed -> fullscreen: int w = 0, h = 0; // If a fullscreen size was configured, use that; else use the desktop size; else use a default if (m_ConfigFullscreen) { w = m_ConfigW; h = m_ConfigH; } if (w == 0 || h == 0) { w = m_PreferredW; h = m_PreferredH; } if (w == 0 || h == 0) { w = DEFAULT_FULLSCREEN_W; h = DEFAULT_FULLSCREEN_H; } int bpp = GetBestBPP(); if (!SetVideoMode(w, h, bpp, fullscreen)) return false; UpdateRenderer(m_CurrentW, m_CurrentH); return true; } else { // Fullscreen -> windowed: // Go back to whatever the previous window size was int w = m_WindowedW, h = m_WindowedH; int bpp = GetBestBPP(); if (!SetVideoMode(w, h, bpp, fullscreen)) return false; UpdateRenderer(w, h); return true; } } bool CVideoMode::ToggleFullscreen() { return SetFullscreen(!m_IsFullscreen); } bool CVideoMode::IsInFullscreen() const { return m_IsFullscreen; } void CVideoMode::UpdatePosition(int x, int y) { if (!m_IsFullscreen) { m_WindowedX = x; m_WindowedY = y; } } void CVideoMode::UpdateRenderer(int w, int h) { if (w < 2) w = 2; // avoid GL errors caused by invalid sizes if (h < 2) h = 2; g_xres = w; g_yres = h; SViewPort vp = { 0, 0, w, h }; if (CRenderer::IsInitialised()) { g_Renderer.SetViewport(vp); g_Renderer.Resize(w, h); } if (g_GUI) g_GUI->UpdateResolution(); if (g_Console) g_Console->UpdateScreenSize(w, h); if (g_Game) g_Game->GetView()->SetViewport(vp); } int CVideoMode::GetBestBPP() { if (m_ConfigBPP) return m_ConfigBPP; if (m_PreferredBPP) return m_PreferredBPP; return 32; } int CVideoMode::GetXRes() const { ENSURE(m_IsInitialised); return m_CurrentW; } int CVideoMode::GetYRes() const { ENSURE(m_IsInitialised); return m_CurrentH; } int CVideoMode::GetBPP() const { ENSURE(m_IsInitialised); return m_CurrentBPP; } bool CVideoMode::IsVSyncEnabled() const { ENSURE(m_IsInitialised); return m_ConfigVSync; } int CVideoMode::GetDesktopXRes() const { ENSURE(m_IsInitialised); return m_PreferredW; } int CVideoMode::GetDesktopYRes() const { ENSURE(m_IsInitialised); return m_PreferredH; } int CVideoMode::GetDesktopBPP() const { ENSURE(m_IsInitialised); return m_PreferredBPP; } int CVideoMode::GetDesktopFreq() const { ENSURE(m_IsInitialised); return m_PreferredFreq; } SDL_Window* CVideoMode::GetWindow() { ENSURE(m_IsInitialised); return m_Window; } void CVideoMode::SetWindowIcon() { // The window icon should be kept outside of art/textures/, or else it will be converted // to DDS by the archive builder and will become unusable here. Using DDS makes BGRA // conversion needlessly complicated. std::shared_ptr iconFile; size_t iconFileSize; if (g_VFS->LoadFile("art/icons/window.png", iconFile, iconFileSize) != INFO::OK) { LOGWARNING("Window icon not found."); return; } Tex iconTexture; if (iconTexture.decode(iconFile, iconFileSize) != INFO::OK) return; // Convert to required BGRA format. const size_t iconFlags = (iconTexture.m_Flags | TEX_BGR) & ~TEX_DXT; if (iconTexture.transform_to(iconFlags) != INFO::OK) return; void* bgra_img = iconTexture.get_data(); if (!bgra_img) return; SDL_Surface *iconSurface = SDL_CreateRGBSurfaceFrom(bgra_img, iconTexture.m_Width, iconTexture.m_Height, 32, iconTexture.m_Width * 4, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); if (!iconSurface) return; SDL_SetWindowIcon(m_Window, iconSurface); SDL_FreeSurface(iconSurface); } void CVideoMode::SetCursor(const CStrW& name) { if (m_Cursor) m_Cursor->SetCursor(name); } void CVideoMode::ResetCursor() { if (m_Cursor) m_Cursor->ResetCursor(); }