From 7322f0accd8d6598a8ed2d8a67953b594786b6c2 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Fri, 25 Sep 2026 08:18:04 +0200 Subject: [PATCH] fix(main): Show the startup splash in its own frameless window until the game window is ready --- .../W3DDevice/GameClient/W3DDisplay.cpp | 47 ++++++++ GeneralsMD/Code/Main/WinMain.cpp | 106 ++++++++++++++++-- GeneralsMD/Code/Main/WinMain.h | 1 + 3 files changed, 147 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 3fd458ff3b0..5eda9635d85 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -379,6 +379,44 @@ inline Int64 getPerformanceCounterFrequency() return tmp; } +// Startup splash window created by WinMain; the game window stays hidden until handed over here +HWND ApplicationSplashHWnd = nullptr; + +// Show the game window and close the splash +static void finishStartupSplash() +{ + extern HWND ApplicationHWnd; + if (ApplicationSplashHWnd == nullptr) + return; + + if (ApplicationHWnd != nullptr) + { + ::ShowWindow(ApplicationHWnd, SW_SHOW); + ::SetForegroundWindow(ApplicationHWnd); + ::SetFocus(ApplicationHWnd); + } + ::DestroyWindow(ApplicationSplashHWnd); + ApplicationSplashHWnd = nullptr; +} + +// Windowed hand-over, once per frame until done: the first frame shows the game window behind the +// splash without activating it, the next one (after a presented frame) closes the splash +static void updateStartupSplash() +{ + extern HWND ApplicationHWnd; + if (ApplicationSplashHWnd == nullptr || ApplicationHWnd == nullptr) + return; + + if (!::IsWindowVisible(ApplicationHWnd)) + { + ::SetWindowPos(ApplicationHWnd, ApplicationSplashHWnd, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + return; + } + + finishStartupSplash(); +} + // W3DDisplay::W3DDisplay ===================================================== /** */ //============================================================================= @@ -889,6 +927,13 @@ void W3DDisplay::init() WW3D::Set_MSAA_Mode(WW3D::MultiSampleModeEnum::MULTISAMPLE_MODE_NONE); #endif + // exclusive fullscreen needs the game window visible and in the foreground when the device is + // created, and the mode switch blanks the screen anyway, so hand over from the splash now + if (!getWindowed()) + { + finishStartupSplash(); + } + renderDeviceError = WW3D::Set_Render_Device( 0, getWidth(), @@ -1811,6 +1856,8 @@ void W3DDisplay::draw() if (TheGlobalData->m_headless) return; + updateStartupSplash(); + // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread. W3D_UpdateScreenshotMessages(); diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 90e852b502a..ad572f56258 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -691,6 +691,82 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, } +// SplashWndProc ============================================================== +/** Paints the load screen bitmap; the bitmap lives exactly as long as this window. */ +//============================================================================= +static LRESULT CALLBACK SplashWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) + { + case WM_PAINT: + { + PAINTSTRUCT paint; + HDC dc = ::BeginPaint(hWnd, &paint); + if (gLoadScreenBitmap != nullptr) + { + RECT client; + ::GetClientRect(hWnd, &client); + HDC tmpDC = ::CreateCompatibleDC(dc); + HBITMAP savBitmap = (HBITMAP)::SelectObject(tmpDC, gLoadScreenBitmap); + ::BitBlt(dc, 0, 0, client.right, client.bottom, tmpDC, 0, 0, SRCCOPY); + ::SelectObject(tmpDC, savBitmap); + ::DeleteDC(tmpDC); + } + ::EndPaint(hWnd, &paint); + return 0; + } + + case WM_ERASEBKGND: + return TRUE; // the bitmap covers the whole window + + case WM_NCDESTROY: + if (gLoadScreenBitmap != nullptr) + { + ::DeleteObject(gLoadScreenBitmap); + gLoadScreenBitmap = nullptr; + } + break; + } + return DefWindowProc(hWnd, message, wParam, lParam); +} + +// createSplashWindow ========================================================= +/** Frameless window with the load screen bitmap, centred on the primary monitor. */ +//============================================================================= +static HWND createSplashWindow(HINSTANCE hInstance, HICON icon) +{ + BITMAP bitmap; + if (gLoadScreenBitmap == nullptr || ::GetObject(gLoadScreenBitmap, sizeof(bitmap), &bitmap) == 0 + || bitmap.bmWidth <= 0 || bitmap.bmHeight <= 0) + { + return nullptr; + } + + WNDCLASS splashClass = { 0, SplashWndProc, 0, 0, hInstance, icon, + LoadCursor(nullptr, IDC_APPSTARTING), + (HBRUSH)GetStockObject(BLACK_BRUSH), nullptr, + TEXT("Game Splash") }; + if (RegisterClass(&splashClass) == 0) + { + return nullptr; + } + + const POINT origin = { 0, 0 }; + MONITORINFO monitorInfo = { sizeof(MONITORINFO) }; + GetMonitorInfo(MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY), &monitorInfo); + const Int x = (monitorInfo.rcWork.left + monitorInfo.rcWork.right - bitmap.bmWidth) / 2; + const Int y = (monitorInfo.rcWork.top + monitorInfo.rcWork.bottom - bitmap.bmHeight) / 2; + + HWND hWnd = CreateWindow(TEXT("Game Splash"), TEXT("Command and Conquer Generals"), WS_POPUP | WS_VISIBLE, + x, y, bitmap.bmWidth, bitmap.bmHeight, nullptr, nullptr, hInstance, nullptr); + if (hWnd != nullptr) + { + SetForegroundWindow(hWnd); + UpdateWindow(hWnd); + } + return hWnd; +} + // initializeAppWindows ======================================================= /** Register windows class and create application windows. */ //============================================================================= @@ -709,8 +785,14 @@ static Bool initializeAppWindows(HINSTANCE hInstance, Int nCmdShow, Bool runWind TEXT("Game Window") }; RegisterClass(&wndClass); + // The splash gets its own frameless window; the game window stays hidden until W3DDisplay hands over, + // so it never shows stale splash pixels while its frame, size and render device are set up + ApplicationSplashHWnd = createSplashWindow(hInstance, wndClass.hIcon); + // Create our main window - windowStyle = WS_POPUP | WS_VISIBLE; + windowStyle = WS_POPUP; + if (ApplicationSplashHWnd == nullptr) + windowStyle |= WS_VISIBLE; if (runWindowed) windowStyle |= WS_MINIMIZEBOX | WS_SYSMENU | WS_DLGFRAME | WS_CAPTION; else @@ -759,16 +841,19 @@ static Bool initializeAppWindows(HINSTANCE hInstance, Int nCmdShow, Bool runWind else SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); - SetFocus(hWnd); + if (ApplicationSplashHWnd == nullptr) + { + SetFocus(hWnd); - SetForegroundWindow(hWnd); - ShowWindow(hWnd, nCmdShow); - UpdateWindow(hWnd); + SetForegroundWindow(hWnd); + ShowWindow(hWnd, nCmdShow); + UpdateWindow(hWnd); + } // save our application window handle for future use ApplicationHWnd = hWnd; gInitializing = false; - if (!runWindowed) { + if (!runWindowed || ApplicationSplashHWnd != nullptr) { gDoPaint = false; } @@ -895,7 +980,8 @@ Int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, // save our application instance for future use ApplicationHInstance = hInstance; - if (gLoadScreenBitmap != nullptr) { + // the splash window owns the bitmap while it exists + if (gLoadScreenBitmap != nullptr && ApplicationSplashHWnd == nullptr) { ::DeleteObject(gLoadScreenBitmap); gLoadScreenBitmap = nullptr; } @@ -946,6 +1032,12 @@ Int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, // run the game main loop exitcode = GameMain(); + // startup ended before the first frame was drawn + if (ApplicationSplashHWnd != nullptr) { + ::DestroyWindow(ApplicationSplashHWnd); + ApplicationSplashHWnd = nullptr; + } + delete TheVersion; TheVersion = nullptr; diff --git a/GeneralsMD/Code/Main/WinMain.h b/GeneralsMD/Code/Main/WinMain.h index 594964a79b8..722f314f683 100644 --- a/GeneralsMD/Code/Main/WinMain.h +++ b/GeneralsMD/Code/Main/WinMain.h @@ -41,4 +41,5 @@ // EXTERNAL /////////////////////////////////////////////////////////////////// extern HINSTANCE ApplicationHInstance; ///< our application instance extern HWND ApplicationHWnd; ///< our application window handle +extern HWND ApplicationSplashHWnd; ///< startup splash window, until W3DDisplay hands over to the game window extern Win32Mouse *TheWin32Mouse; ///< global for win32 mouse only!