#include "game_window.h" #include #include #include #include #include #include #include #if _WIN32 #include #pragma comment(lib, "dwmapi.lib") #endif #include #include #include #include #include #include #include bool m_isFullscreenKeyReleased = true; bool m_isResizing = false; int Window_OnSDLEvent(void*, SDL_Event* event) { if (ImGui::GetIO().BackendPlatformUserData != nullptr) ImGui_ImplSDL2_ProcessEvent(event); for (auto listener : GetEventListeners()) listener->OnSDLEvent(event); switch (event->type) { case SDL_QUIT: App::Exit(); break; case SDL_KEYDOWN: { switch (event->key.keysym.sym) { // Toggle fullscreen on ALT+ENTER. case SDLK_RETURN: { if (!(event->key.keysym.mod & KMOD_ALT) || !m_isFullscreenKeyReleased) break; Config::Fullscreen = GameWindow::SetFullscreen(!GameWindow::IsFullscreen()); if (Config::Fullscreen) { Config::Monitor = GameWindow::GetDisplay(); } else { Config::WindowState = GameWindow::SetMaximised(Config::WindowState == EWindowState::Maximised); } // Block holding ALT+ENTER spamming window changes. m_isFullscreenKeyReleased = false; break; } // Restore original window dimensions on F2. case SDLK_F2: Config::Fullscreen = GameWindow::SetFullscreen(false); GameWindow::ResetDimensions(); break; // Recentre window on F3. case SDLK_F3: { if (GameWindow::IsFullscreen()) break; GameWindow::SetDimensions(GameWindow::s_width, GameWindow::s_height); break; } } break; } case SDL_KEYUP: { switch (event->key.keysym.sym) { // Allow user to input ALT+ENTER again. case SDLK_RETURN: m_isFullscreenKeyReleased = true; break; } } case SDL_WINDOWEVENT: { switch (event->window.event) { case SDL_WINDOWEVENT_FOCUS_LOST: GameWindow::s_isFocused = false; SDL_ShowCursor(SDL_ENABLE); break; case SDL_WINDOWEVENT_FOCUS_GAINED: { GameWindow::s_isFocused = true; if (GameWindow::IsFullscreen()) SDL_ShowCursor(GameWindow::s_isCursorVisible ? SDL_ENABLE : SDL_DISABLE); break; } case SDL_WINDOWEVENT_RESTORED: Config::WindowState = EWindowState::Normal; break; case SDL_WINDOWEVENT_MAXIMIZED: Config::WindowState = EWindowState::Maximised; break; case SDL_WINDOWEVENT_RESIZED: m_isResizing = true; Config::WindowSize = -1; GameWindow::s_width = event->window.data1; GameWindow::s_height = event->window.data2; GameWindow::SetTitle(fmt::format("{} - [{}x{}]", GameWindow::GetTitle(), GameWindow::s_width, GameWindow::s_height).c_str()); break; case SDL_WINDOWEVENT_MOVED: GameWindow::s_x = event->window.data1; GameWindow::s_y = event->window.data2; break; } break; } case SDL_USER_EVILSONIC: GameWindow::s_isIconNight = event->user.code; GameWindow::SetIcon(GameWindow::s_isIconNight); break; } if (!GameWindow::IsFullscreen()) { if (event->type == SDL_CONTROLLERBUTTONDOWN || event->type == SDL_CONTROLLERBUTTONUP || event->type == SDL_CONTROLLERAXISMOTION) { // Hide mouse cursor when controller input is detected. SDL_ShowCursor(GameWindow::s_isCursorVisible ? SDL_ENABLE : SDL_DISABLE); } else if (event->type == SDL_MOUSEMOTION) { // Restore mouse cursor when mouse input is detected. SDL_ShowCursor(SDL_ENABLE); } } return 0; } void GameWindow::Init(const char* sdlVideoDriver) { #ifdef __linux__ SDL_SetHint("SDL_APP_ID", "io.github.hedge_dev.unleashedrecomp"); #endif if (SDL_VideoInit(sdlVideoDriver) != 0 && sdlVideoDriver) { LOGFN_ERROR("Failed to initialise the SDL video driver: \"{}\". Falling back to default.", sdlVideoDriver); SDL_VideoInit(nullptr); } auto videoDriverName = SDL_GetCurrentVideoDriver(); if (videoDriverName) LOGFN("SDL video driver: \"{}\"", videoDriverName); SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE); SDL_AddEventWatch(Window_OnSDLEvent, s_pWindow); #ifdef _WIN32 SetProcessDPIAware(); #endif Config::WindowSize.LockCallback = [](ConfigDef* def) { // Try matching the current window size with a known configuration. if (def->Value < 0) def->Value = FindNearestDisplayMode(); }; Config::WindowSize.ApplyCallback = [](ConfigDef* def) { auto displayModes = GetDisplayModes(); // Use largest supported resolution if overflowed. if (def->Value >= displayModes.size()) def->Value = displayModes.size() - 1; auto& mode = displayModes[def->Value]; auto centre = SDL_WINDOWPOS_CENTERED_DISPLAY(GetDisplay()); SetDimensions(mode.w, mode.h, centre, centre); }; s_x = Config::WindowX; s_y = Config::WindowY; s_width = Config::WindowWidth; s_height = Config::WindowHeight; if (s_x == -1 && s_y == -1) s_x = s_y = SDL_WINDOWPOS_CENTERED; if (!IsPositionValid()) GameWindow::ResetDimensions(); s_pWindow = SDL_CreateWindow("SWA", s_x, s_y, s_width, s_height, GetWindowFlags()); if (IsFullscreen()) SDL_ShowCursor(SDL_DISABLE); SetDisplay(Config::Monitor); SetIcon(); SetTitle(); SDL_SetWindowMinimumSize(s_pWindow, MIN_WIDTH, MIN_HEIGHT); SDL_SysWMinfo info; SDL_VERSION(&info.version); SDL_GetWindowWMInfo(s_pWindow, &info); #if defined(_WIN32) s_renderWindow = info.info.win.window; #elif defined(SDL_VULKAN_ENABLED) s_renderWindow = s_pWindow; #elif defined(__linux__) s_renderWindow = { info.info.x11.display, info.info.x11.window }; #else static_assert(false, "Unknown platform."); #endif SetTitleBarColour(); SDL_ShowWindow(s_pWindow); } void GameWindow::Update() { if (!GameWindow::IsFullscreen() && !GameWindow::IsMaximised() && !s_isChangingDisplay) { Config::WindowX = GameWindow::s_x; Config::WindowY = GameWindow::s_y; Config::WindowWidth = GameWindow::s_width; Config::WindowHeight = GameWindow::s_height; } if (m_isResizing) { SetTitle(); m_isResizing = false; } if (g_needsResize) s_isChangingDisplay = false; } SDL_Surface* GameWindow::CreateSurface(void* pBitmapData, size_t bitmapSize) { auto rw = SDL_RWFromMem(pBitmapData, bitmapSize); auto surface = SDL_LoadBMP_RW(rw, 1); if (!surface) LOGF_ERROR("Failed to create surface: {}", SDL_GetError()); return surface; } void GameWindow::SetIcon(void* pIconBmp, size_t iconSize) { if (auto surface = CreateSurface(pIconBmp, iconSize)) { SDL_SetWindowIcon(s_pWindow, surface); SDL_FreeSurface(surface); } } void GameWindow::SetIcon(bool isNight) { if (isNight) { SetIcon(g_game_icon_night, sizeof(g_game_icon_night)); } else { SetIcon(g_game_icon, sizeof(g_game_icon)); } } void GameWindow::SetCursor(ECursorType cursorType) { if (s_currentCursor == cursorType) return; s_currentCursor = cursorType; uint8_t* pBitmapData; size_t bitmapSize; int x, y; switch (cursorType) { case ECursorType::DebugDefault: pBitmapData = g_mouse_cursor; bitmapSize = sizeof(g_mouse_cursor); x = 0; y = 0; break; case ECursorType::DebugHorizontal: pBitmapData = g_mouse_cursor_h; bitmapSize = sizeof(g_mouse_cursor_h); x = 15; y = 15; break; case ECursorType::DebugVertical: pBitmapData = g_mouse_cursor_v; bitmapSize = sizeof(g_mouse_cursor_v); x = 15; y = 15; break; case ECursorType::DebugLeftDiagonal: pBitmapData = g_mouse_cursor_slant_l; bitmapSize = sizeof(g_mouse_cursor_slant_l); x = 16; y = 15; break; case ECursorType::DebugRightDiagonal: pBitmapData = g_mouse_cursor_slant_r; bitmapSize = sizeof(g_mouse_cursor_slant_r); x = 16; y = 15; break; default: SDL_SetCursor(SDL_GetDefaultCursor()); return; } if (auto surface = CreateSurface(pBitmapData, bitmapSize)) { if (auto cursor = SDL_CreateColorCursor(surface, x, y)) { SDL_SetCursor(cursor); SDL_FreeSurface(surface); } } } const char* GameWindow::GetTitle() { return Config::Language == ELanguage::Japanese ? "SONIC WORLD ADVENTURE" : "SONIC UNLEASHED"; } void GameWindow::SetTitle(const char* title) { SDL_SetWindowTitle(s_pWindow, title ? title : GetTitle()); } void GameWindow::SetTitleBarColour() { #if _WIN32 if (os::user::IsDarkTheme()) { auto version = os::version::GetOSVersion(); if (version.Major < 10 || version.Build <= 17763) return; auto flag = version.Build >= 18985 ? DWMWA_USE_IMMERSIVE_DARK_MODE : 19; // DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 const DWORD useImmersiveDarkMode = 1; DwmSetWindowAttribute(s_renderWindow, flag, &useImmersiveDarkMode, sizeof(useImmersiveDarkMode)); } #endif } bool GameWindow::IsFullscreen() { return SDL_GetWindowFlags(s_pWindow) & SDL_WINDOW_FULLSCREEN_DESKTOP; } bool GameWindow::SetFullscreen(bool isEnabled) { if (isEnabled) { SDL_SetWindowFullscreen(s_pWindow, SDL_WINDOW_FULLSCREEN_DESKTOP); SDL_ShowCursor(s_isCursorVisible ? SDL_ENABLE : SDL_DISABLE); } else { SDL_SetWindowFullscreen(s_pWindow, 0); SDL_ShowCursor(SDL_ENABLE); SetIcon(GameWindow::s_isIconNight); SetDimensions(Config::WindowWidth, Config::WindowHeight, Config::WindowX, Config::WindowY); } return isEnabled; } void GameWindow::SetCursorVisibility(bool isVisible) { s_isCursorVisible = isVisible; if (IsFullscreen()) { SDL_ShowCursor(s_isCursorVisible ? SDL_ENABLE : SDL_DISABLE); } else { SDL_ShowCursor(SDL_ENABLE); } } bool GameWindow::IsMaximised() { return SDL_GetWindowFlags(s_pWindow) & SDL_WINDOW_MAXIMIZED; } EWindowState GameWindow::SetMaximised(bool isEnabled) { if (isEnabled) { SDL_MaximizeWindow(s_pWindow); } else { SDL_RestoreWindow(s_pWindow); } return isEnabled ? EWindowState::Maximised : EWindowState::Normal; } SDL_Rect GameWindow::GetDimensions() { SDL_Rect rect{}; SDL_GetWindowPosition(s_pWindow, &rect.x, &rect.y); SDL_GetWindowSize(s_pWindow, &rect.w, &rect.h); return rect; } void GameWindow::SetDimensions(int w, int h, int x, int y) { s_width = w; s_height = h; s_x = x; s_y = y; SDL_SetWindowSize(s_pWindow, w, h); SDL_ResizeEvent(s_pWindow, w, h); SDL_SetWindowPosition(s_pWindow, x, y); SDL_MoveEvent(s_pWindow, x, y); } void GameWindow::ResetDimensions() { s_x = SDL_WINDOWPOS_CENTERED; s_y = SDL_WINDOWPOS_CENTERED; s_width = DEFAULT_WIDTH; s_height = DEFAULT_HEIGHT; Config::WindowX = s_x; Config::WindowY = s_y; Config::WindowWidth = s_width; Config::WindowHeight = s_height; } uint32_t GameWindow::GetWindowFlags() { uint32_t flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; if (Config::WindowState == EWindowState::Maximised) flags |= SDL_WINDOW_MAXIMIZED; if (Config::Fullscreen) flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; #ifdef SDL_VULKAN_ENABLED flags |= SDL_WINDOW_VULKAN; #endif return flags; } int GameWindow::GetDisplayCount() { auto result = SDL_GetNumVideoDisplays(); if (result < 0) { LOGF_ERROR("Failed to get display count: {}", SDL_GetError()); return 1; } return result; } int GameWindow::GetDisplay() { return SDL_GetWindowDisplayIndex(s_pWindow); } void GameWindow::SetDisplay(int displayIndex) { if (!IsFullscreen()) return; s_isChangingDisplay = true; SDL_Rect bounds; if (SDL_GetDisplayBounds(displayIndex, &bounds) == 0) { SetFullscreen(false); SetDimensions(bounds.w, bounds.h, bounds.x, bounds.y); SetFullscreen(true); } else { ResetDimensions(); } } std::vector GameWindow::GetDisplayModes(bool ignoreInvalidModes, bool ignoreRefreshRates) { auto result = std::vector(); auto uniqueResolutions = std::set>(); auto displayIndex = GetDisplay(); auto modeCount = SDL_GetNumDisplayModes(displayIndex); if (modeCount <= 0) return result; for (int i = modeCount - 1; i >= 0; i--) { SDL_DisplayMode mode; if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) { if (ignoreInvalidModes) { if (mode.w < MIN_WIDTH || mode.h < MIN_HEIGHT) continue; SDL_DisplayMode desktopMode; if (SDL_GetDesktopDisplayMode(displayIndex, &desktopMode) == 0) { if (mode.w >= desktopMode.w || mode.h >= desktopMode.h) continue; } } if (ignoreRefreshRates) { auto res = std::make_pair(mode.w, mode.h); if (uniqueResolutions.find(res) == uniqueResolutions.end()) { uniqueResolutions.insert(res); result.push_back(mode); } } else { result.push_back(mode); } } } return result; } int GameWindow::FindNearestDisplayMode() { auto result = -1; auto displayModes = GetDisplayModes(); auto currentDiff = std::numeric_limits::max(); for (int i = 0; i < displayModes.size(); i++) { auto& mode = displayModes[i]; auto widthDiff = abs(mode.w - s_width); auto heightDiff = abs(mode.h - s_height); auto totalDiff = widthDiff + heightDiff; if (totalDiff < currentDiff) { currentDiff = totalDiff; result = i; } } return result; } bool GameWindow::IsPositionValid() { auto displayCount = GetDisplayCount(); for (int i = 0; i < displayCount; i++) { SDL_Rect bounds; if (SDL_GetDisplayBounds(i, &bounds) == 0) { auto x = s_x; auto y = s_y; // Window spans across the entire display in windowed mode, which is invalid. if (!Config::Fullscreen && s_width == bounds.w && s_height == bounds.h) return false; if (x == SDL_WINDOWPOS_CENTERED_DISPLAY(i)) x = bounds.w / 2 - s_width / 2; if (y == SDL_WINDOWPOS_CENTERED_DISPLAY(i)) y = bounds.h / 2 - s_height / 2; if (x >= bounds.x && x < bounds.x + bounds.w && y >= bounds.y && y < bounds.y + bounds.h) { return true; } } } return false; }