bb
157c6af18e
Avoid cases of filenames Update years in terms and other legal(ish) documents Don't update years in license headers, since change is not meaningful Will add linter rule in seperate commit Happy recompiling everyone! Original Patch By: Nescio Comment By: Gallaecio Differential Revision: D2620 This was SVN commit r27786.
937 lines
23 KiB
C++
937 lines
23 KiB
C++
/* Copyright (C) 2023 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#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/sysdep/os.h"
|
|
#include "lib/tex/tex.h"
|
|
#include "ps/CConsole.h"
|
|
#include "ps/CLogger.h"
|
|
#include "ps/ConfigDB.h"
|
|
#include "ps/CStr.h"
|
|
#if OS_MACOSX && SDL_VERSION_ATLEAST(2, 0, 6)
|
|
#include "ps/DllLoader.h"
|
|
#endif
|
|
#include "ps/Filesystem.h"
|
|
#include "ps/Game.h"
|
|
#include "ps/GameSetup/Config.h"
|
|
#include "ps/Pyrogenesis.h"
|
|
#include "renderer/backend/dummy/DeviceForward.h"
|
|
#include "renderer/backend/gl/DeviceForward.h"
|
|
#include "renderer/backend/IDevice.h"
|
|
#include "renderer/backend/vulkan/DeviceForward.h"
|
|
#include "renderer/Renderer.h"
|
|
|
|
#include <string_view>
|
|
|
|
#if OS_MACOSX && SDL_VERSION_ATLEAST(2, 0, 6)
|
|
#include <SDL_vulkan.h>
|
|
#include <stdlib.h>
|
|
#endif
|
|
|
|
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";
|
|
|
|
Renderer::Backend::Backend GetFallbackBackend(const Renderer::Backend::Backend backend)
|
|
{
|
|
Renderer::Backend::Backend fallback = Renderer::Backend::Backend::DUMMY;
|
|
// We use a switch instead of a list to have compile-time checks for missed
|
|
// values and because a linear priority list doesn't work for general case.
|
|
switch (backend)
|
|
{
|
|
case Renderer::Backend::Backend::GL:
|
|
fallback = Renderer::Backend::Backend::GL_ARB;
|
|
break;
|
|
case Renderer::Backend::Backend::GL_ARB:
|
|
fallback = Renderer::Backend::Backend::DUMMY;
|
|
break;
|
|
case Renderer::Backend::Backend::DUMMY:
|
|
break;
|
|
case Renderer::Backend::Backend::VULKAN:
|
|
fallback = Renderer::Backend::Backend::GL;
|
|
break;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
std::string_view GetBackendName(const Renderer::Backend::Backend backend)
|
|
{
|
|
std::string_view name{"Unknown"};
|
|
switch (backend)
|
|
{
|
|
case Renderer::Backend::Backend::GL:
|
|
name = "GL";
|
|
break;
|
|
case Renderer::Backend::Backend::GL_ARB:
|
|
name = "GL ARB";
|
|
break;
|
|
case Renderer::Backend::Backend::DUMMY:
|
|
name = "Dummy";
|
|
break;
|
|
case Renderer::Backend::Backend::VULKAN:
|
|
name = "Vulkan";
|
|
break;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
} // 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<u8> 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<const wchar_t*>(buffer.get()), size));
|
|
s >> hotspotX >> hotspotY;
|
|
}
|
|
|
|
const VfsPath pathImageName = pathBaseName.ChangeExtension(L".png");
|
|
|
|
std::shared_ptr<u8> 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<int>(t.m_Width), static_cast<int>(t.m_Height), 32,
|
|
static_cast<int>(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);
|
|
|
|
CStr rendererBackend;
|
|
CFG_GET_VAL("rendererbackend", rendererBackend);
|
|
if (rendererBackend == "glarb")
|
|
m_Backend = Renderer::Backend::Backend::GL_ARB;
|
|
else if (rendererBackend == "dummy")
|
|
m_Backend = Renderer::Backend::Backend::DUMMY;
|
|
else if (rendererBackend == "vulkan")
|
|
m_Backend = Renderer::Backend::Backend::VULKAN;
|
|
else
|
|
m_Backend = Renderer::Backend::Backend::GL;
|
|
|
|
#if OS_WIN
|
|
if (m_ConfigEnableHiDPI)
|
|
wutil_EnableHiDPIOnWindows();
|
|
#endif
|
|
}
|
|
|
|
bool CVideoMode::SetVideoMode(int w, int h, int bpp, bool fullscreen)
|
|
{
|
|
Uint32 flags = 0;
|
|
if (fullscreen)
|
|
{
|
|
bool borderlessFullscreen = true;
|
|
CFG_GET_VAL("borderless.fullscreen", borderlessFullscreen);
|
|
flags |= borderlessFullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN;
|
|
}
|
|
else
|
|
{
|
|
bool borderlessWindow = false;
|
|
CFG_GET_VAL("borderless.window", borderlessWindow);
|
|
if (borderlessWindow)
|
|
flags |= SDL_WINDOW_BORDERLESS;
|
|
}
|
|
|
|
if (!m_Window)
|
|
{
|
|
const bool isGLBackend =
|
|
m_Backend == Renderer::Backend::Backend::GL ||
|
|
m_Backend == Renderer::Backend::Backend::GL_ARB;
|
|
if (isGLBackend)
|
|
{
|
|
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 debugContext = false;
|
|
CFG_GET_VAL("renderer.backend.debugcontext", debugContext);
|
|
if (debugContext)
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// Note: these flags only take affect in SDL_CreateWindow
|
|
flags |= SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE;
|
|
if (m_ConfigEnableHiDPI)
|
|
flags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
|
if (isGLBackend)
|
|
flags |= SDL_WINDOW_OPENGL;
|
|
else if (m_Backend == Renderer::Backend::Backend::VULKAN)
|
|
flags |= SDL_WINDOW_VULKAN;
|
|
m_WindowedX = m_WindowedY = SDL_WINDOWPOS_CENTERED_DISPLAY(m_ConfigDisplay);
|
|
|
|
#if OS_MACOSX && SDL_VERSION_ATLEAST(2, 0, 6)
|
|
if (m_Backend == Renderer::Backend::Backend::VULKAN)
|
|
{
|
|
// MoltenVK - enable full component swizzling support.
|
|
setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1);
|
|
CStr fullPathToVulkanLibrary = DllLoader::GenerateFilename("MoltenVK", "", ".dylib");
|
|
// MoltenVK - only print warnings and errors.
|
|
setenv("MVK_CONFIG_LOG_LEVEL", "2", 1);
|
|
if (SDL_Vulkan_LoadLibrary(fullPathToVulkanLibrary.c_str()) != 0)
|
|
{
|
|
LOGWARNING("Failed to load %s.", fullPathToVulkanLibrary.c_str());
|
|
DowngradeBackendSettingAfterCreationFailure();
|
|
return SetVideoMode(w, h, bpp, fullscreen);
|
|
}
|
|
else
|
|
LOGMESSAGE("Loaded %s.", fullPathToVulkanLibrary.c_str());
|
|
}
|
|
#endif
|
|
|
|
m_Window = SDL_CreateWindow(main_window_name, m_WindowedX, m_WindowedY, w, h, flags);
|
|
if (!m_Window)
|
|
{
|
|
// SDL might fail to create a window in case of missing a Vulkan driver.
|
|
if (m_Backend == Renderer::Backend::Backend::VULKAN)
|
|
{
|
|
LOGWARNING("Failed to create a Vulkan window: %s", SDL_GetError());
|
|
DowngradeBackendSettingAfterCreationFailure();
|
|
return SetVideoMode(w, h, bpp, fullscreen);
|
|
}
|
|
|
|
// 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
|
|
{
|
|
if (isGLBackend)
|
|
{
|
|
int depthSize = 24;
|
|
SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &depthSize);
|
|
if (depthSize > 16)
|
|
{
|
|
// 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);
|
|
|
|
return SetVideoMode(w, h, bpp, fullscreen);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#if OS_WIN
|
|
// We need to set the window for an error dialog.
|
|
wutil_SetAppWindow(m_Window);
|
|
#endif
|
|
|
|
if (!TryCreateBackendDevice(m_Window))
|
|
{
|
|
DowngradeBackendSettingAfterCreationFailure();
|
|
SDL_DestroyWindow(m_Window);
|
|
m_Window = nullptr;
|
|
return SetVideoMode(w, h, bpp, fullscreen);
|
|
}
|
|
|
|
if (isGLBackend)
|
|
SDL_GL_SetSwapInterval(m_ConfigVSync ? 1 : 0);
|
|
}
|
|
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;
|
|
|
|
// #545: we need to constrain the window in fullscreen mode to avoid mouse
|
|
// "falling out" of the window in case of multiple displays.
|
|
bool mouseGrabInFullscreen = true;
|
|
CFG_GET_VAL("window.mousegrabinfullscreen", mouseGrabInFullscreen);
|
|
bool mouseGrabInWindowMode = false;
|
|
CFG_GET_VAL("window.mousegrabinwindowmode", mouseGrabInWindowMode);
|
|
|
|
if (fullscreen ? mouseGrabInFullscreen : mouseGrabInWindowMode)
|
|
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
|
else
|
|
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
|
|
|
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);
|
|
}
|
|
|
|
const int bpp = GetBestBPP();
|
|
if (!SetVideoMode(w, h, bpp, m_ConfigFullscreen))
|
|
return false;
|
|
|
|
// 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.
|
|
|
|
m_IsInitialised = true;
|
|
|
|
if (!m_ConfigFullscreen)
|
|
{
|
|
m_WindowedW = w;
|
|
m_WindowedH = h;
|
|
}
|
|
|
|
SetWindowIcon();
|
|
|
|
m_Cursor = std::make_unique<CCursor>();
|
|
|
|
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;
|
|
m_BackendDevice.reset();
|
|
if (m_Window)
|
|
{
|
|
SDL_DestroyWindow(m_Window);
|
|
m_Window = nullptr;
|
|
}
|
|
}
|
|
|
|
bool CVideoMode::CreateBackendDevice(const bool createSDLContext)
|
|
{
|
|
if (!createSDLContext && m_Backend == Renderer::Backend::Backend::VULKAN)
|
|
m_Backend = Renderer::Backend::Backend::GL;
|
|
SDL_Window* window = createSDLContext ? m_Window : nullptr;
|
|
while (m_Backend != Renderer::Backend::Backend::DUMMY)
|
|
{
|
|
if (TryCreateBackendDevice(window))
|
|
return true;
|
|
DowngradeBackendSettingAfterCreationFailure();
|
|
}
|
|
return TryCreateBackendDevice(window);
|
|
}
|
|
|
|
bool CVideoMode::TryCreateBackendDevice(SDL_Window* window)
|
|
{
|
|
switch (m_Backend)
|
|
{
|
|
case Renderer::Backend::Backend::GL:
|
|
m_BackendDevice = Renderer::Backend::GL::CreateDevice(window, false);
|
|
break;
|
|
case Renderer::Backend::Backend::GL_ARB:
|
|
m_BackendDevice = Renderer::Backend::GL::CreateDevice(window, true);
|
|
break;
|
|
case Renderer::Backend::Backend::DUMMY:
|
|
m_BackendDevice = Renderer::Backend::Dummy::CreateDevice(window);
|
|
ENSURE(m_BackendDevice);
|
|
break;
|
|
case Renderer::Backend::Backend::VULKAN:
|
|
m_BackendDevice = Renderer::Backend::Vulkan::CreateDevice(window);
|
|
break;
|
|
}
|
|
return static_cast<bool>(m_BackendDevice);
|
|
}
|
|
|
|
void CVideoMode::DowngradeBackendSettingAfterCreationFailure()
|
|
{
|
|
const Renderer::Backend::Backend fallback = GetFallbackBackend(m_Backend);
|
|
LOGERROR("Unable to create device for %s backend, switching to %s.",
|
|
GetBackendName(m_Backend), GetBackendName(fallback));
|
|
m_Backend = fallback;
|
|
}
|
|
|
|
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 (g_VideoMode.GetBackendDevice())
|
|
g_VideoMode.GetBackendDevice()->OnWindowResize(w, h);
|
|
|
|
if (CRenderer::IsInitialised())
|
|
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<u8> 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();
|
|
}
|