From 7a3c3f7ec33c76e40ff8df88e7216f45a8d92d2e Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 24 Apr 2026 10:38:42 +1000 Subject: [PATCH 01/31] build(screenshot): Add stb_image_write library via FetchContent --- CMakeLists.txt | 1 + cmake/stb.cmake | 19 +++++++++++++++++++ scripts/cpp/unify_move_files.py | 1 + vcpkg.json | 3 ++- 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 cmake/stb.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 28ce09560e9..39062e3fbe6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ endif() include(cmake/config.cmake) include(cmake/gamespy.cmake) include(cmake/lzhl.cmake) +include(cmake/stb.cmake) if (IS_VS6_BUILD) # The original max sdk does not compile against a modern compiler. diff --git a/cmake/stb.cmake b/cmake/stb.cmake new file mode 100644 index 00000000000..8f2078a8106 --- /dev/null +++ b/cmake/stb.cmake @@ -0,0 +1,19 @@ +# TheSuperHackers @bobtista 02/11/2025 +# STB single-file public domain libraries for image encoding +# https://github.com/nothings/stb + +find_package(Stb CONFIG QUIET) + +if(NOT Stb_FOUND) + include(FetchContent) + FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG 5c205738c191bcb0abc65c4febfa9bd25ff35234 + ) + + FetchContent_MakeAvailable(stb) + + add_library(stb INTERFACE) + target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) +endif() diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py index b95939719ec..8bb57341344 100644 --- a/scripts/cpp/unify_move_files.py +++ b/scripts/cpp/unify_move_files.py @@ -311,6 +311,7 @@ def main(): #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp") #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp") #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp") + #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp") #unify_file(Game.ZEROHOUR, "GameEngine/Include/Common/UserPreferences.h", Game.CORE, "GameEngine/Include/Common/UserPreferences.h") #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/UserPreferences.cpp", Game.CORE, "GameEngine/Source/Common/UserPreferences.cpp") diff --git a/vcpkg.json b/vcpkg.json index 011b913c8aa..9ce3c6667c3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -3,6 +3,7 @@ "builtin-baseline": "b02e341c927f16d991edbd915d8ea43eac52096c", "dependencies": [ "zlib", - "ffmpeg" + "ffmpeg", + "stb" ] } \ No newline at end of file From 50c3e271ce12bdbb6dc80c2d2d4e92b1e1283bdc Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 24 Apr 2026 10:38:55 +1000 Subject: [PATCH 02/31] feat(screenshot): Add threaded JPEG/PNG screenshots without game stalls --- .../Include/Common/OptionPreferences.h | 1 + Core/GameEngine/Include/GameClient/Display.h | 8 +- .../Source/Common/OptionPreferences.cpp | 10 + .../GameClient/MessageStream/CommandXlat.cpp | 10 +- .../GameClient/MessageStream/MetaEvent.cpp | 21 ++ Core/GameEngineDevice/CMakeLists.txt | 4 + .../W3DDevice/GameClient/W3DScreenshot.h | 24 ++ .../GameClient/stb_image_write_impl.cpp | 21 ++ .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/MessageStream.h | 3 +- .../GameEngine/Source/Common/GlobalData.cpp | 2 + .../Source/Common/MessageStream.cpp | 1 + Generals/Code/GameEngineDevice/CMakeLists.txt | 2 + .../Include/W3DDevice/GameClient/W3DDisplay.h | 2 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 216 +----------------- .../W3DDevice/GameClient/W3DScreenshot.cpp | 160 +++++++++++++ .../Tools/GUIEdit/Include/GUIEditDisplay.h | 2 +- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/MessageStream.h | 3 +- .../GameEngine/Source/Common/GlobalData.cpp | 2 + .../Source/Common/MessageStream.cpp | 1 + .../Code/GameEngineDevice/CMakeLists.txt | 2 + .../Include/W3DDevice/GameClient/W3DDisplay.h | 2 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 216 +----------------- .../W3DDevice/GameClient/W3DScreenshot.cpp | 160 +++++++++++++ .../Tools/GUIEdit/Include/GUIEditDisplay.h | 2 +- 26 files changed, 439 insertions(+), 438 deletions(-) create mode 100644 Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h create mode 100644 Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp create mode 100644 Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp create mode 100644 GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 8448ccd1a14..3abcbb9a0aa 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -72,6 +72,7 @@ class OptionPreferences : public UserPreferences Bool getRightMouseScrollWithAlternateMouseEnabled() const; Bool getRetaliationModeEnabled(); Bool getDoubleClickAttackMoveEnabled(); + Int getJPEGQuality(); Real getScrollFactor(); Bool getDrawScrollAnchor(); Bool getMoveScrollAnchor(); diff --git a/Core/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h index 8c022244206..e7bf9e465e7 100644 --- a/Core/GameEngine/Include/GameClient/Display.h +++ b/Core/GameEngine/Include/GameClient/Display.h @@ -33,6 +33,12 @@ #include "GameClient/GameFont.h" #include "GameClient/View.h" +enum ScreenshotFormat +{ + SCREENSHOT_JPEG, + SCREENSHOT_PNG +}; + struct ShroudLevel { Short m_currentShroud; ///< A Value of 1 means shrouded. 0 is not. Negative is the count of people looking. @@ -172,7 +178,7 @@ class Display : public SubsystemInterface virtual void preloadModelAssets( AsciiString model ) = 0; ///< preload model asset virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset - virtual void takeScreenShot() = 0; ///< saves screenshot to a file + virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index bab11cb7c76..755b4b0c439 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -239,6 +239,16 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled() return FALSE; } +Int OptionPreferences::getJPEGQuality() +{ + OptionPreferences::const_iterator it = find("JPEGQuality"); + if (it == end()) + return 80; + + Int quality = atoi(it->second.str()); + return clamp(1, quality, 100); +} + Real OptionPreferences::getScrollFactor() { OptionPreferences::const_iterator it = find("ScrollFactor"); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index ac77d3c6017..6410806cb9c 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3738,7 +3738,15 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage case GameMessage::MSG_META_TAKE_SCREENSHOT: { if (TheDisplay) - TheDisplay->takeScreenShot(); + TheDisplay->takeScreenShot(SCREENSHOT_JPEG); + disp = DESTROY_MESSAGE; + break; + } + + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: + { + if (TheDisplay) + TheDisplay->takeScreenShot(SCREENSHOT_PNG); disp = DESTROY_MESSAGE; break; } diff --git a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index e5032c93b75..31a954e67b5 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -171,6 +171,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, + { "TAKE_SCREENSHOT_PNG", GameMessage::MSG_META_TAKE_SCREENSHOT_PNG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -945,6 +946,26 @@ void MetaMap::generateMetaMap() map->m_usableIn = COMMANDUSABLE_GAME; } } + { + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT); + if (map->m_key == MK_NONE) + { + map->m_key = MK_F12; + map->m_transition = DOWN; + map->m_modState = NONE; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } + { + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); + if (map->m_key == MK_NONE) + { + map->m_key = MK_F12; + map->m_transition = DOWN; + map->m_modState = CTRL; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } #if defined(RTS_DEBUG) { diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index a7016b52e73..82bcc335b07 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -73,6 +73,7 @@ set(GAMEENGINEDEVICE_SRC Include/W3DDevice/GameClient/W3DTreeBuffer.h Include/W3DDevice/GameClient/W3DVideoBuffer.h Include/W3DDevice/GameClient/W3DView.h + Include/W3DDevice/GameClient/W3DScreenshot.h # Include/W3DDevice/GameClient/W3DVolumetricShadow.h Include/W3DDevice/GameClient/W3DWater.h Include/W3DDevice/GameClient/W3DWaterTracks.h @@ -175,6 +176,8 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DTreeBuffer.cpp Source/W3DDevice/GameClient/W3DVideoBuffer.cpp Source/W3DDevice/GameClient/W3DView.cpp +# Source/W3DDevice/GameClient/W3DScreenshot.cpp + Source/W3DDevice/GameClient/stb_image_write_impl.cpp # Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp Source/W3DDevice/GameClient/Water/W3DWater.cpp @@ -220,6 +223,7 @@ target_include_directories(corei_gameenginedevice_public INTERFACE target_link_libraries(corei_gameenginedevice_private INTERFACE corei_always corei_main + stb ) target_link_libraries(corei_gameenginedevice_public INTERFACE diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h new file mode 100644 index 00000000000..01b04030838 --- /dev/null +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -0,0 +1,24 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#pragma once + +#include "GameClient/Display.h" + +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 0); + diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp new file mode 100644 index 00000000000..2264ba2c403 --- /dev/null +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -0,0 +1,21 @@ +/* +** Command & Conquer Generals(tm) +** Copyright 2025 TheSuperHackers +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 67264cbea62..6f79e4c1fbd 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -143,6 +143,7 @@ class GlobalData : public SubsystemInterface Bool m_clientRetaliationModeEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; + Int m_jpegQuality; Bool m_useWaterPlane; Bool m_useCloudPlane; Bool m_useShadowVolumes; diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 36f09af52bb..15a9b5e2344 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -256,7 +256,8 @@ class GameMessage : public MemoryPoolObject MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. - MSG_META_TAKE_SCREENSHOT, ///< take screenshot + MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) + MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot (CTRL+F12, lossless) MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index 3aabf4ca449..858ff1ecee9 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -650,6 +650,7 @@ GlobalData::GlobalData() m_enableDynamicLOD = TRUE; m_enableStaticLOD = TRUE; m_rightMouseAlwaysScrolls = FALSE; + m_jpegQuality = 80; m_useWaterPlane = FALSE; m_useCloudPlane = FALSE; m_downwindAngle = ( -0.785f );//Northeast! @@ -1198,6 +1199,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_jpegQuality = optionPref.getJPEGQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); TheWritableGlobalData->m_moveScrollAnchor = optionPref.getMoveScrollAnchor(); diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp index e89dbd6f969..6081067f857 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -337,6 +337,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 704352959cf..500aa9eb304 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -139,6 +139,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DDebugDisplay.cpp Source/W3DDevice/GameClient/W3DDebugIcons.cpp Source/W3DDevice/GameClient/W3DDisplay.cpp + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/W3DDisplayString.cpp Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp Source/W3DDevice/GameClient/W3DDynamicLight.cpp @@ -200,6 +201,7 @@ target_link_libraries(g_gameenginedevice PRIVATE corei_gameenginedevice_private gi_always gi_main + stb ) target_link_libraries(g_gameenginedevice PUBLIC diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 11e480e210c..2c90f1471c6 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -122,7 +122,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() override; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot() override; //save screenshot to file + virtual void takeScreenShot(ScreenshotFormat format) override; //save screenshot in specified format virtual void toggleMovieCapture() override; //enable AVI or frame capture mode. virtual void toggleLetterBox() override; /// // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" @@ -2941,221 +2942,6 @@ void W3DDisplay::setShroudLevel( Int x, Int y, CellShroudStatus setting ) } } -//============================================================================= -///Utility function to dump data into a .BMP file -static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) -{ - HANDLE hf; // file handle - BITMAPFILEHEADER hdr; // bitmap file-header - PBITMAPINFOHEADER pbih; // bitmap info-header - LPBYTE lpBits; // memory pointer - DWORD dwTotal; // total count of bytes - DWORD cb; // incremental count of bytes - BYTE *hp; // byte pointer - DWORD dwTmp; - - PBITMAPINFO pbmi; - - pbmi = (PBITMAPINFO) LocalAlloc(LPTR,sizeof(BITMAPINFOHEADER)); - if (pbmi == nullptr) - return; - - pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - pbmi->bmiHeader.biWidth = width; - pbmi->bmiHeader.biHeight = height; - pbmi->bmiHeader.biPlanes = 1; - pbmi->bmiHeader.biBitCount = 24; - pbmi->bmiHeader.biCompression = BI_RGB; - pbmi->bmiHeader.biSizeImage = (pbmi->bmiHeader.biWidth + 7) /8 * pbmi->bmiHeader.biHeight * 24; - pbmi->bmiHeader.biClrImportant = 0; - - pbih = (PBITMAPINFOHEADER) pbmi; - lpBits = (LPBYTE) image; - - // Create the .BMP file. - hf = CreateFile(pszFile, - GENERIC_READ | GENERIC_WRITE, - (DWORD) 0, - nullptr, - CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - (HANDLE) nullptr); - - if (hf != INVALID_HANDLE_VALUE) - { - hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M" - // Compute the size of the entire file. - hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) + - pbih->biSize + pbih->biClrUsed - * sizeof(RGBQUAD) + pbih->biSizeImage); - hdr.bfReserved1 = 0; - hdr.bfReserved2 = 0; - - // Compute the offset to the array of color indices. - hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) + - pbih->biSize + pbih->biClrUsed - * sizeof (RGBQUAD); - - // Copy the BITMAPFILEHEADER into the .BMP file. - if (WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER), - (LPDWORD) &dwTmp, nullptr)) - { - // Copy the BITMAPINFOHEADER and RGBQUAD array into the file. - if (WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER) + pbih->biClrUsed * sizeof (RGBQUAD),(LPDWORD) &dwTmp, nullptr)) - { - // Copy the array of color indices into the .BMP file. - dwTotal = cb = pbih->biSizeImage; - hp = lpBits; - WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp, nullptr); - } - } - - // Close the .BMP file. - CloseHandle(hf); - } - - // Free memory. - LocalFree( (HLOCAL) pbmi); -} - -///Save Screen Capture to a file -void W3DDisplay::takeScreenShot() -{ - char leafname[256]; - char pathname[1024]; - - static int frame_number = 1; - - Bool done = false; - while (!done) { -#ifdef CAPTURE_TO_TARGA - sprintf( leafname, "%s%.3d.tga", "sshot", frame_number++); -#else - sprintf( leafname, "%s%.3d.bmp", "sshot", frame_number++); -#endif - strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - if (_access( pathname, 0 ) == -1) - done = true; - } - - // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. - // Originally this code took the front buffer and tried to lock it. This does not work when the - // render view clips outside the desktop boundaries. It crashed the game. - SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); - - SurfaceClass::SurfaceDescription surfaceDesc; - surface->Get_Description(surfaceDesc); - - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); - - surface->Release_Ref(); - surface = nullptr; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == nullptr) - { - surfaceCopy->Release_Ref(); - return; - } - - unsigned int x,y,index,index2,width,height; - - width = surfaceDesc.Width; - height = surfaceDesc.Height; - - char *image=NEW char[3*width*height]; -#ifdef CAPTURE_TO_TARGA - //bytes are mixed in targa files, not rgb order. - for (y=0; yUnlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - Targa targ; - memset(&targ.Header,0,sizeof(targ.Header)); - targ.Header.Width=width; - targ.Header.Height=height; - targ.Header.PixelDepth=24; - targ.Header.ImageType=TGA_TRUECOLOR; - targ.SetImage(image); - targ.YFlip(); - - targ.Save(pathname,TGAF_IMAGE,false); -#else //capturing to bmp file - //bmp is same byte order - for (y=0; yUnlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - //Flip the image - char *ptr,*ptr1; - char v,v1; - - for (y = 0; y < (height >> 1); y++) - { - /* Compute address of lines to exchange. */ - ptr = (image + ((width * y) * 3)); - ptr1 = (image + ((width * (height - 1)) * 3)); - ptr1 -= ((width * y) * 3); - - /* Exchange all the pixels on this scan line. */ - for (x = 0; x < (width * 3); x++) - { - v = *ptr; - v1 = *ptr1; - *ptr = v1; - *ptr1 = v; - ptr++; - ptr1++; - } - } - CreateBMPFile(pathname, image, width, height); -#endif - - delete [] image; - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); -} - /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() { diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp new file mode 100644 index 00000000000..b1fc685f1da --- /dev/null +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -0,0 +1,160 @@ +/* +** Command & Conquer Generals(tm) +** Copyright 2025 TheSuperHackers +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include + +#include "W3DDevice/GameClient/W3DScreenshot.h" +#include "W3DDevice/GameClient/W3DDisplay.h" +#include "Common/GlobalData.h" +#include "GameClient/GameText.h" +#include "GameClient/InGameUI.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/surfaceclass.h" +#include + +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[_MAX_PATH]; + char leafname[_MAX_FNAME]; + int quality; + ScreenshotFormat format; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = 0; + switch (data->format) + { + case SCREENSHOT_JPEG: + result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); + break; + case SCREENSHOT_PNG: + result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + break; + } + + if (!result) { + OutputDebugStringA("Failed to write screenshot\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) +{ + char leafname[_MAX_FNAME]; + char pathname[_MAX_PATH]; + static int jpegFrameNumber = 1; + static int pngFrameNumber = 1; + + int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber; + const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; + + Bool done = false; + while (!done) { + sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); + strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + if (_access(pathname, 0) == -1) + done = true; + } + + SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); + SurfaceClass::SurfaceDescription surfaceDesc; + surface->Get_Description(surfaceDesc); + + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); + + surface->Release_Ref(); + surface = nullptr; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == nullptr) + { + surfaceCopy->Release_Ref(); + return; + } + + unsigned int x, y, index, index2; + unsigned int width = surfaceDesc.Width; + unsigned int height = surfaceDesc.Height; + + unsigned char* image = new unsigned char[3 * width * height]; + + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + index = 3 * (x + y * width); + index2 = y * lrect.Pitch + 4 * x; + + image[index] = *((unsigned char*)lrect.pBits + index2 + 2); + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); + } + } + + surfaceCopy->Unlock(); + surfaceCopy->Release_Ref(); + surfaceCopy = nullptr; + + if (quality <= 0 && format == SCREENSHOT_JPEG) + quality = TheGlobalData->m_jpegQuality; + + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + threadData->quality = quality; + threadData->format = format; + strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); + strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); + + DWORD threadId; + HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); + } else { + delete [] threadData->imageData; + delete threadData; + } +} + +void W3DDisplay::takeScreenShot(ScreenshotFormat format) +{ + W3D_TakeCompressedScreenshot(format); +} diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index ee27b4cc7a0..0d8fb26369d 100644 --- a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -101,7 +101,7 @@ class GUIEditDisplay : public Display virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) override { } virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) override { } - virtual void takeScreenShot() override { } + virtual void takeScreenShot(ScreenshotFormat format) override { } virtual void toggleMovieCapture() override {} // methods that we need to stub diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 4c5130e1c05..844e3bf986c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -144,6 +144,7 @@ class GlobalData : public SubsystemInterface Bool m_clientRetaliationModeEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; + Int m_jpegQuality; Bool m_useWaterPlane; Bool m_useCloudPlane; Bool m_useShadowVolumes; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 594ae255b5d..ebf97de5fc4 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -256,7 +256,8 @@ class GameMessage : public MemoryPoolObject MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. - MSG_META_TAKE_SCREENSHOT, ///< take screenshot + MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) + MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot (CTRL+F12, lossless) MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 9900029d48d..1286c06cdb2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -654,6 +654,7 @@ GlobalData::GlobalData() m_enableDynamicLOD = TRUE; m_enableStaticLOD = TRUE; m_rightMouseAlwaysScrolls = FALSE; + m_jpegQuality = 80; m_useWaterPlane = FALSE; m_useCloudPlane = FALSE; m_downwindAngle = ( -0.785f );//Northeast! @@ -1205,6 +1206,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_jpegQuality = optionPref.getJPEGQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); TheWritableGlobalData->m_moveScrollAnchor = optionPref.getMoveScrollAnchor(); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index f7c58e978d2..116b9016399 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -337,6 +337,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 5cae369888c..65248211714 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -150,6 +150,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DDebugDisplay.cpp Source/W3DDevice/GameClient/W3DDebugIcons.cpp Source/W3DDevice/GameClient/W3DDisplay.cpp + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/W3DDisplayString.cpp Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp Source/W3DDevice/GameClient/W3DDynamicLight.cpp @@ -213,6 +214,7 @@ target_link_libraries(z_gameenginedevice PRIVATE corei_gameenginedevice_private zi_always zi_main + stb ) target_link_libraries(z_gameenginedevice PUBLIC diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index c43ec2691a7..19bd6c0d27f 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -122,7 +122,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() override; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot() override; //save screenshot to file + virtual void takeScreenShot(ScreenshotFormat format) override; //save screenshot in specified format virtual void toggleMovieCapture() override; //enable AVI or frame capture mode. virtual void toggleLetterBox() override; /// // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" @@ -3053,221 +3054,6 @@ void W3DDisplay::setShroudLevel( Int x, Int y, CellShroudStatus setting ) } } -//============================================================================= -///Utility function to dump data into a .BMP file -static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) -{ - HANDLE hf; // file handle - BITMAPFILEHEADER hdr; // bitmap file-header - PBITMAPINFOHEADER pbih; // bitmap info-header - LPBYTE lpBits; // memory pointer - DWORD dwTotal; // total count of bytes - DWORD cb; // incremental count of bytes - BYTE *hp; // byte pointer - DWORD dwTmp; - - PBITMAPINFO pbmi; - - pbmi = (PBITMAPINFO) LocalAlloc(LPTR,sizeof(BITMAPINFOHEADER)); - if (pbmi == nullptr) - return; - - pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - pbmi->bmiHeader.biWidth = width; - pbmi->bmiHeader.biHeight = height; - pbmi->bmiHeader.biPlanes = 1; - pbmi->bmiHeader.biBitCount = 24; - pbmi->bmiHeader.biCompression = BI_RGB; - pbmi->bmiHeader.biSizeImage = (pbmi->bmiHeader.biWidth + 7) /8 * pbmi->bmiHeader.biHeight * 24; - pbmi->bmiHeader.biClrImportant = 0; - - pbih = (PBITMAPINFOHEADER) pbmi; - lpBits = (LPBYTE) image; - - // Create the .BMP file. - hf = CreateFile(pszFile, - GENERIC_READ | GENERIC_WRITE, - (DWORD) 0, - nullptr, - CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - (HANDLE) nullptr); - - if (hf != INVALID_HANDLE_VALUE) - { - hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M" - // Compute the size of the entire file. - hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) + - pbih->biSize + pbih->biClrUsed - * sizeof(RGBQUAD) + pbih->biSizeImage); - hdr.bfReserved1 = 0; - hdr.bfReserved2 = 0; - - // Compute the offset to the array of color indices. - hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) + - pbih->biSize + pbih->biClrUsed - * sizeof (RGBQUAD); - - // Copy the BITMAPFILEHEADER into the .BMP file. - if (WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER), - (LPDWORD) &dwTmp, nullptr)) - { - // Copy the BITMAPINFOHEADER and RGBQUAD array into the file. - if (WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER) + pbih->biClrUsed * sizeof (RGBQUAD),(LPDWORD) &dwTmp, nullptr)) - { - // Copy the array of color indices into the .BMP file. - dwTotal = cb = pbih->biSizeImage; - hp = lpBits; - WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp, nullptr); - } - } - - // Close the .BMP file. - CloseHandle(hf); - } - - // Free memory. - LocalFree( (HLOCAL) pbmi); -} - -///Save Screen Capture to a file -void W3DDisplay::takeScreenShot() -{ - char leafname[256]; - char pathname[1024]; - - static int frame_number = 1; - - Bool done = false; - while (!done) { -#ifdef CAPTURE_TO_TARGA - sprintf( leafname, "%s%.3d.tga", "sshot", frame_number++); -#else - sprintf( leafname, "%s%.3d.bmp", "sshot", frame_number++); -#endif - strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - if (_access( pathname, 0 ) == -1) - done = true; - } - - // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. - // Originally this code took the front buffer and tried to lock it. This does not work when the - // render view clips outside the desktop boundaries. It crashed the game. - SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); - - SurfaceClass::SurfaceDescription surfaceDesc; - surface->Get_Description(surfaceDesc); - - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); - - surface->Release_Ref(); - surface = nullptr; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == nullptr) - { - surfaceCopy->Release_Ref(); - return; - } - - unsigned int x,y,index,index2,width,height; - - width = surfaceDesc.Width; - height = surfaceDesc.Height; - - char *image=NEW char[3*width*height]; -#ifdef CAPTURE_TO_TARGA - //bytes are mixed in targa files, not rgb order. - for (y=0; yUnlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - Targa targ; - memset(&targ.Header,0,sizeof(targ.Header)); - targ.Header.Width=width; - targ.Header.Height=height; - targ.Header.PixelDepth=24; - targ.Header.ImageType=TGA_TRUECOLOR; - targ.SetImage(image); - targ.YFlip(); - - targ.Save(pathname,TGAF_IMAGE,false); -#else //capturing to bmp file - //bmp is same byte order - for (y=0; yUnlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - //Flip the image - char *ptr,*ptr1; - char v,v1; - - for (y = 0; y < (height >> 1); y++) - { - /* Compute address of lines to exchange. */ - ptr = (image + ((width * y) * 3)); - ptr1 = (image + ((width * (height - 1)) * 3)); - ptr1 -= ((width * y) * 3); - - /* Exchange all the pixels on this scan line. */ - for (x = 0; x < (width * 3); x++) - { - v = *ptr; - v1 = *ptr1; - *ptr = v1; - *ptr1 = v; - ptr++; - ptr1++; - } - } - CreateBMPFile(pathname, image, width, height); -#endif - - delete [] image; - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); -} - /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() { diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp new file mode 100644 index 00000000000..1526ec921a3 --- /dev/null +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -0,0 +1,160 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program 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 3 of the License, or +** (at your option) any later version. +** +** This program 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 this program. If not, see . +*/ + +#include + +#include "W3DDevice/GameClient/W3DScreenshot.h" +#include "W3DDevice/GameClient/W3DDisplay.h" +#include "Common/GlobalData.h" +#include "GameClient/GameText.h" +#include "GameClient/InGameUI.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/surfaceclass.h" +#include + +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[_MAX_PATH]; + char leafname[_MAX_FNAME]; + int quality; + ScreenshotFormat format; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = 0; + switch (data->format) + { + case SCREENSHOT_JPEG: + result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); + break; + case SCREENSHOT_PNG: + result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + break; + } + + if (!result) { + OutputDebugStringA("Failed to write screenshot\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) +{ + char leafname[_MAX_FNAME]; + char pathname[_MAX_PATH]; + static int jpegFrameNumber = 1; + static int pngFrameNumber = 1; + + int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber; + const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; + + Bool done = false; + while (!done) { + sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); + strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + if (_access(pathname, 0) == -1) + done = true; + } + + SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); + SurfaceClass::SurfaceDescription surfaceDesc; + surface->Get_Description(surfaceDesc); + + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); + + surface->Release_Ref(); + surface = nullptr; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == nullptr) + { + surfaceCopy->Release_Ref(); + return; + } + + unsigned int x, y, index, index2; + unsigned int width = surfaceDesc.Width; + unsigned int height = surfaceDesc.Height; + + unsigned char* image = new unsigned char[3 * width * height]; + + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + index = 3 * (x + y * width); + index2 = y * lrect.Pitch + 4 * x; + + image[index] = *((unsigned char*)lrect.pBits + index2 + 2); + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); + } + } + + surfaceCopy->Unlock(); + surfaceCopy->Release_Ref(); + surfaceCopy = nullptr; + + if (quality <= 0 && format == SCREENSHOT_JPEG) + quality = TheGlobalData->m_jpegQuality; + + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + threadData->quality = quality; + threadData->format = format; + strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); + strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); + + DWORD threadId; + HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); + } else { + delete [] threadData->imageData; + delete threadData; + } +} + +void W3DDisplay::takeScreenShot(ScreenshotFormat format) +{ + W3D_TakeCompressedScreenshot(format); +} diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index 9e432ecb827..d9a659679a1 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -101,7 +101,7 @@ class GUIEditDisplay : public Display virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) override { } virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) override { } - virtual void takeScreenShot() override { } + virtual void takeScreenShot(ScreenshotFormat format) override { } virtual void toggleMovieCapture() override {} // methods that we need to stub From fc332dcaef4d6283ff53c70b7d802320de223652 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 21 May 2026 15:02:43 +1000 Subject: [PATCH 03/31] tweak(screenshot): Name files by timestamp so JPG and PNG sort together --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 1526ec921a3..976b145e813 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -67,20 +67,14 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) { char leafname[_MAX_FNAME]; char pathname[_MAX_PATH]; - static int jpegFrameNumber = 1; - static int pngFrameNumber = 1; - - int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber; const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; - Bool done = false; - while (!done) { - sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); - strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - if (_access(pathname, 0) == -1) - done = true; - } + SYSTEMTIME st; + GetLocalTime(&st); + sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension); + strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); SurfaceClass::SurfaceDescription surfaceDesc; From 7a736c47b4d8a522c1482be3d453b49f69d3c742 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 21 May 2026 15:02:47 +1000 Subject: [PATCH 04/31] tweak(screenshot): Replicate to Generals --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index b1fc685f1da..ebc12f57fa4 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -67,20 +67,14 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) { char leafname[_MAX_FNAME]; char pathname[_MAX_PATH]; - static int jpegFrameNumber = 1; - static int pngFrameNumber = 1; - - int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber; const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; - Bool done = false; - while (!done) { - sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); - strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - if (_access(pathname, 0) == -1) - done = true; - } + SYSTEMTIME st; + GetLocalTime(&st); + sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension); + strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); SurfaceClass::SurfaceDescription surfaceDesc; From 31dbce92c3d49ef72550faf9583c36d4631f0ecd Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 21 May 2026 15:14:22 +1000 Subject: [PATCH 05/31] fix(stb): Create stb target on both vcpkg and FetchContent paths --- cmake/stb.cmake | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmake/stb.cmake b/cmake/stb.cmake index 8f2078a8106..f7ecfccfee4 100644 --- a/cmake/stb.cmake +++ b/cmake/stb.cmake @@ -14,6 +14,8 @@ if(NOT Stb_FOUND) FetchContent_MakeAvailable(stb) - add_library(stb INTERFACE) - target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) + set(Stb_INCLUDE_DIR ${stb_SOURCE_DIR}) endif() + +add_library(stb INTERFACE) +target_include_directories(stb INTERFACE ${Stb_INCLUDE_DIR}) From 1ae734e769c31cfdeb584a981df6586917d76ac6 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 18 Jun 2026 15:54:44 +0700 Subject: [PATCH 06/31] fix(screenshot): Use engine logging and match codebase style --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 976b145e813..a6b59ca8e86 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,8 +16,6 @@ ** along with this program. If not, see . */ -#include - #include "W3DDevice/GameClient/W3DScreenshot.h" #include "W3DDevice/GameClient/W3DDisplay.h" #include "Common/GlobalData.h" @@ -53,8 +51,9 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) break; } - if (!result) { - OutputDebugStringA("Failed to write screenshot\n"); + if (!result) + { + DEBUG_LOG(("Failed to write screenshot %s", data->pathname)); } delete [] data->imageData; @@ -76,6 +75,9 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. + // Originally this code took the front buffer and tried to lock it. This does not work when the + // render view clips outside the desktop boundaries. It crashed the game. SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); @@ -123,7 +125,9 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy = nullptr; if (quality <= 0 && format == SCREENSHOT_JPEG) + { quality = TheGlobalData->m_jpegQuality; + } ScreenshotThreadData* threadData = new ScreenshotThreadData(); threadData->imageData = image; @@ -136,13 +140,16 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) DWORD threadId; HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); - if (hThread) { + if (hThread) + { CloseHandle(hThread); UnicodeString ufileName; ufileName.translate(leafname); TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); - } else { + } + else + { delete [] threadData->imageData; delete threadData; } From c9f43be4f745d7e7df1ab53f1c9b78639d0ed342 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 18 Jun 2026 15:54:44 +0700 Subject: [PATCH 07/31] fix(screenshot): Replicate to Generals --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index ebc12f57fa4..eb045972a50 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,8 +16,6 @@ ** along with this program. If not, see . */ -#include - #include "W3DDevice/GameClient/W3DScreenshot.h" #include "W3DDevice/GameClient/W3DDisplay.h" #include "Common/GlobalData.h" @@ -53,8 +51,9 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) break; } - if (!result) { - OutputDebugStringA("Failed to write screenshot\n"); + if (!result) + { + DEBUG_LOG(("Failed to write screenshot %s", data->pathname)); } delete [] data->imageData; @@ -76,6 +75,9 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. + // Originally this code took the front buffer and tried to lock it. This does not work when the + // render view clips outside the desktop boundaries. It crashed the game. SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); @@ -123,7 +125,9 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy = nullptr; if (quality <= 0 && format == SCREENSHOT_JPEG) + { quality = TheGlobalData->m_jpegQuality; + } ScreenshotThreadData* threadData = new ScreenshotThreadData(); threadData->imageData = image; @@ -136,13 +140,16 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) DWORD threadId; HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); - if (hThread) { + if (hThread) + { CloseHandle(hThread); UnicodeString ufileName; ufileName.translate(leafname); TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); - } else { + } + else + { delete [] threadData->imageData; delete threadData; } From 14cc69db7a19fce738fb504747150977694475c2 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 29 Jun 2026 15:27:20 -0400 Subject: [PATCH 08/31] build(stb): use TheSuperHackers comment keyword format --- cmake/stb.cmake | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmake/stb.cmake b/cmake/stb.cmake index f7ecfccfee4..42a6bc5ad90 100644 --- a/cmake/stb.cmake +++ b/cmake/stb.cmake @@ -1,5 +1,4 @@ -# TheSuperHackers @bobtista 02/11/2025 -# STB single-file public domain libraries for image encoding +# TheSuperHackers @build bobtista 02/11/2025 STB single-file public domain libraries for image encoding # https://github.com/nothings/stb find_package(Stb CONFIG QUIET) From 0144f86b274e52c67dcb2eaf05d1bdfd8d899a2c Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 29 Jun 2026 15:27:20 -0400 Subject: [PATCH 09/31] style(screenshot): drop unused leafname from thread data --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index a6b59ca8e86..eb4e11cf106 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -31,7 +31,6 @@ struct ScreenshotThreadData unsigned int width; unsigned int height; char pathname[_MAX_PATH]; - char leafname[_MAX_FNAME]; int quality; ScreenshotFormat format; }; @@ -136,7 +135,6 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) threadData->quality = quality; threadData->format = format; strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); - strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); DWORD threadId; HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); From a6837a0e1a4a379586585e3a3a51939c45c5efb5 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 29 Jun 2026 15:27:20 -0400 Subject: [PATCH 10/31] style(screenshot): Replicate to Generals --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index eb045972a50..c0b7b98b857 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -31,7 +31,6 @@ struct ScreenshotThreadData unsigned int width; unsigned int height; char pathname[_MAX_PATH]; - char leafname[_MAX_FNAME]; int quality; ScreenshotFormat format; }; @@ -136,7 +135,6 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) threadData->quality = quality; threadData->format = format; strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); - strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); DWORD threadId; HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); From a1d44bbd6f17c80960b74e26962f06053d01502e Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 3 Jul 2026 15:37:25 -0400 Subject: [PATCH 11/31] chore(screenshot): Remove unused W3DScreenshot.h include from W3DDisplay --- .../GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp | 1 - .../GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index d6bf2f961b2..bfb5b12f556 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -41,7 +41,6 @@ static void drawFramerateBar(); #include // USER INCLUDES ////////////////////////////////////////////////////////////// -#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index c3d0cf8e76d..b2b5ab90e7c 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -41,7 +41,6 @@ static void drawFramerateBar(); #include // USER INCLUDES ////////////////////////////////////////////////////////////// -#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" From 1b1d9305ab88ea47cc337ee21e2f655f6c0ef359 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 00:37:02 +0400 Subject: [PATCH 12/31] unify(screenshot): Move W3DScreenshot to Core --- Core/GameEngineDevice/CMakeLists.txt | 2 +- .../W3DDevice/GameClient/W3DScreenshot.cpp | 6 - Generals/Code/GameEngineDevice/CMakeLists.txt | 1 - .../W3DDevice/GameClient/W3DDisplay.cpp | 6 + .../W3DDevice/GameClient/W3DScreenshot.cpp | 159 ------------------ .../Code/GameEngineDevice/CMakeLists.txt | 1 - .../W3DDevice/GameClient/W3DDisplay.cpp | 6 + 7 files changed, 13 insertions(+), 168 deletions(-) rename {GeneralsMD/Code => Core}/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp (96%) delete mode 100644 Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 82bcc335b07..1b0791a2104 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -176,7 +176,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DTreeBuffer.cpp Source/W3DDevice/GameClient/W3DVideoBuffer.cpp Source/W3DDevice/GameClient/W3DView.cpp -# Source/W3DDevice/GameClient/W3DScreenshot.cpp + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp # Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp similarity index 96% rename from GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp rename to Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index eb4e11cf106..7d8acefe1f3 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -17,7 +17,6 @@ */ #include "W3DDevice/GameClient/W3DScreenshot.h" -#include "W3DDevice/GameClient/W3DDisplay.h" #include "Common/GlobalData.h" #include "GameClient/GameText.h" #include "GameClient/InGameUI.h" @@ -152,8 +151,3 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) delete threadData; } } - -void W3DDisplay::takeScreenShot(ScreenshotFormat format) -{ - W3D_TakeCompressedScreenshot(format); -} diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 500aa9eb304..adc64c5d3c3 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -139,7 +139,6 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DDebugDisplay.cpp Source/W3DDevice/GameClient/W3DDebugIcons.cpp Source/W3DDevice/GameClient/W3DDisplay.cpp - Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/W3DDisplayString.cpp Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp Source/W3DDevice/GameClient/W3DDynamicLight.cpp diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index bfb5b12f556..2169934b957 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -41,6 +41,7 @@ static void drawFramerateBar(); #include // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" @@ -2947,6 +2948,11 @@ void W3DDisplay::toggleMovieCapture() WW3D::Toggle_Movie_Capture("Movie",30); } +void W3DDisplay::takeScreenShot(ScreenshotFormat format) +{ + W3D_TakeCompressedScreenshot(format); +} + #if defined(RTS_DEBUG) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp deleted file mode 100644 index c0b7b98b857..00000000000 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ /dev/null @@ -1,159 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 TheSuperHackers -** -** This program 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 3 of the License, or -** (at your option) any later version. -** -** This program 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 this program. If not, see . -*/ - -#include "W3DDevice/GameClient/W3DScreenshot.h" -#include "W3DDevice/GameClient/W3DDisplay.h" -#include "Common/GlobalData.h" -#include "GameClient/GameText.h" -#include "GameClient/InGameUI.h" -#include "WW3D2/dx8wrapper.h" -#include "WW3D2/surfaceclass.h" -#include - -struct ScreenshotThreadData -{ - unsigned char* imageData; - unsigned int width; - unsigned int height; - char pathname[_MAX_PATH]; - int quality; - ScreenshotFormat format; -}; - -static DWORD WINAPI screenshotThreadFunc(LPVOID param) -{ - ScreenshotThreadData* data = (ScreenshotThreadData*)param; - - int result = 0; - switch (data->format) - { - case SCREENSHOT_JPEG: - result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); - break; - case SCREENSHOT_PNG: - result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); - break; - } - - if (!result) - { - DEBUG_LOG(("Failed to write screenshot %s", data->pathname)); - } - - delete [] data->imageData; - delete data; - - return 0; -} - -void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) -{ - char leafname[_MAX_FNAME]; - char pathname[_MAX_PATH]; - const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; - - SYSTEMTIME st; - GetLocalTime(&st); - sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s", - st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension); - strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - - // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. - // Originally this code took the front buffer and tried to lock it. This does not work when the - // render view clips outside the desktop boundaries. It crashed the game. - SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); - SurfaceClass::SurfaceDescription surfaceDesc; - surface->Get_Description(surfaceDesc); - - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); - - surface->Release_Ref(); - surface = nullptr; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == nullptr) - { - surfaceCopy->Release_Ref(); - return; - } - - unsigned int x, y, index, index2; - unsigned int width = surfaceDesc.Width; - unsigned int height = surfaceDesc.Height; - - unsigned char* image = new unsigned char[3 * width * height]; - - for (y = 0; y < height; y++) - { - for (x = 0; x < width; x++) - { - index = 3 * (x + y * width); - index2 = y * lrect.Pitch + 4 * x; - - image[index] = *((unsigned char*)lrect.pBits + index2 + 2); - image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); - image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); - } - } - - surfaceCopy->Unlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - if (quality <= 0 && format == SCREENSHOT_JPEG) - { - quality = TheGlobalData->m_jpegQuality; - } - - ScreenshotThreadData* threadData = new ScreenshotThreadData(); - threadData->imageData = image; - threadData->width = width; - threadData->height = height; - threadData->quality = quality; - threadData->format = format; - strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); - - DWORD threadId; - HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); - if (hThread) - { - CloseHandle(hThread); - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); - } - else - { - delete [] threadData->imageData; - delete threadData; - } -} - -void W3DDisplay::takeScreenShot(ScreenshotFormat format) -{ - W3D_TakeCompressedScreenshot(format); -} diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 65248211714..27352e4f00d 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -150,7 +150,6 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DDebugDisplay.cpp Source/W3DDevice/GameClient/W3DDebugIcons.cpp Source/W3DDevice/GameClient/W3DDisplay.cpp - Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/W3DDisplayString.cpp Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp Source/W3DDevice/GameClient/W3DDynamicLight.cpp diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index b2b5ab90e7c..6e16d2c5cf9 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -41,6 +41,7 @@ static void drawFramerateBar(); #include // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" @@ -3059,6 +3060,11 @@ void W3DDisplay::toggleMovieCapture() WW3D::Toggle_Movie_Capture("Movie",30); } +void W3DDisplay::takeScreenShot(ScreenshotFormat format) +{ + W3D_TakeCompressedScreenshot(format); +} + #if defined(RTS_DEBUG) From 9ce4a5fc1d4869462cb5ea0836401cb1f2302f0d Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 00:37:54 +0400 Subject: [PATCH 13/31] fix(screenshot): Convert 16 bit back buffers to 24 bit color --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 7d8acefe1f3..e80ff084a86 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -80,6 +80,18 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); + // TheSuperHackers @bugfix bobtista 08/07/2026 Support the 16 bit back buffer format that the + // game uses when running in 16 bit color mode. Reading it with the 32 bit stride read garbage. + const bool is32Bit = surfaceDesc.Format == WW3D_FORMAT_A8R8G8B8 || surfaceDesc.Format == WW3D_FORMAT_X8R8G8B8; + const bool is16Bit = surfaceDesc.Format == WW3D_FORMAT_R5G6B5; + + if (!is32Bit && !is16Bit) + { + DEBUG_LOG(("Screenshot does not support back buffer format %d", (int)surfaceDesc.Format)); + surface->Release_Ref(); + return; + } + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); @@ -99,22 +111,41 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) return; } - unsigned int x, y, index, index2; + unsigned int x, y, index; unsigned int width = surfaceDesc.Width; unsigned int height = surfaceDesc.Height; unsigned char* image = new unsigned char[3 * width * height]; - for (y = 0; y < height; y++) + if (is32Bit) { - for (x = 0; x < width; x++) + // Convert A8R8G8B8/X8R8G8B8 to R8G8B8 + for (y = 0; y < height; y++) { - index = 3 * (x + y * width); - index2 = y * lrect.Pitch + 4 * x; - - image[index] = *((unsigned char*)lrect.pBits + index2 + 2); - image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); - image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); + const unsigned char* srcLine = (const unsigned char*)lrect.pBits + y * lrect.Pitch; + for (x = 0; x < width; x++) + { + index = 3 * (x + y * width); + image[index] = srcLine[4 * x + 2]; + image[index + 1] = srcLine[4 * x + 1]; + image[index + 2] = srcLine[4 * x + 0]; + } + } + } + else + { + // Convert R5G6B5 to R8G8B8 + for (y = 0; y < height; y++) + { + const unsigned short* srcLine = (const unsigned short*)((const unsigned char*)lrect.pBits + y * lrect.Pitch); + for (x = 0; x < width; x++) + { + const unsigned short rgb = srcLine[x]; + index = 3 * (x + y * width); + image[index] = (unsigned char)((rgb & 0xF800) >> 8); + image[index + 1] = (unsigned char)((rgb & 0x07E0) >> 3); + image[index + 2] = (unsigned char)((rgb & 0x001F) << 3); + } } } From 24bf262978a89a9b5909d2806b6108e2aeeba750 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 00:38:48 +0400 Subject: [PATCH 14/31] refactor(screenshot): Pass JPEG quality from the message handler --- Core/GameEngine/Include/GameClient/Display.h | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 2 +- .../Include/W3DDevice/GameClient/W3DScreenshot.h | 2 +- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 9 ++------- .../Include/W3DDevice/GameClient/W3DDisplay.h | 2 +- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 4 ++-- Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h | 2 +- .../Include/W3DDevice/GameClient/W3DDisplay.h | 2 +- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 4 ++-- GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h | 2 +- 10 files changed, 13 insertions(+), 18 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h index e7bf9e465e7..d7241cf7b72 100644 --- a/Core/GameEngine/Include/GameClient/Display.h +++ b/Core/GameEngine/Include/GameClient/Display.h @@ -178,7 +178,7 @@ class Display : public SubsystemInterface virtual void preloadModelAssets( AsciiString model ) = 0; ///< preload model asset virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset - virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format + virtual void takeScreenShot(ScreenshotFormat format, Int jpegQuality = 80) = 0; ///< saves screenshot in specified format virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 6410806cb9c..42a32679803 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3738,7 +3738,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage case GameMessage::MSG_META_TAKE_SCREENSHOT: { if (TheDisplay) - TheDisplay->takeScreenShot(SCREENSHOT_JPEG); + TheDisplay->takeScreenShot(SCREENSHOT_JPEG, TheGlobalData->m_jpegQuality); disp = DESTROY_MESSAGE; break; } diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index 01b04030838..6e2e577d475 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -20,5 +20,5 @@ #include "GameClient/Display.h" -void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 0); +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index e80ff084a86..b597d28df9a 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -60,7 +60,7 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) return 0; } -void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) { char leafname[_MAX_FNAME]; char pathname[_MAX_PATH]; @@ -153,16 +153,11 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy->Release_Ref(); surfaceCopy = nullptr; - if (quality <= 0 && format == SCREENSHOT_JPEG) - { - quality = TheGlobalData->m_jpegQuality; - } - ScreenshotThreadData* threadData = new ScreenshotThreadData(); threadData->imageData = image; threadData->width = width; threadData->height = height; - threadData->quality = quality; + threadData->quality = jpegQuality; threadData->format = format; strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 2c90f1471c6..aa3f0b79d2c 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -122,7 +122,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() override; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot(ScreenshotFormat format) override; //save screenshot in specified format + virtual void takeScreenShot(ScreenshotFormat format, Int jpegQuality) override; //save screenshot in specified format virtual void toggleMovieCapture() override; //enable AVI or frame capture mode. virtual void toggleLetterBox() override; /// Date: Thu, 9 Jul 2026 00:39:17 +0400 Subject: [PATCH 15/31] feat(screenshot): Save screenshots into a Screenshots subfolder --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index b597d28df9a..9994844dba1 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -70,7 +70,11 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) GetLocalTime(&st); sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension); + // TheSuperHackers @feature bobtista 08/07/2026 Save screenshots into a Screenshots subfolder + // to keep them out of the User Data root. strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); + strlcat(pathname, "Screenshots\\", ARRAY_SIZE(pathname)); + CreateDirectory(pathname, nullptr); strlcat(pathname, leafname, ARRAY_SIZE(pathname)); // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. From 120921db54960dfe9a1148e1f70adf9212fb44b0 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 00:40:06 +0400 Subject: [PATCH 16/31] refactor(screenshot): Release image buffer in thread data destructor and return write result --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 9994844dba1..872f688234b 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -26,6 +26,11 @@ struct ScreenshotThreadData { + ~ScreenshotThreadData() + { + delete [] imageData; + } + unsigned char* imageData; unsigned int width; unsigned int height; @@ -38,26 +43,25 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) { ScreenshotThreadData* data = (ScreenshotThreadData*)param; - int result = 0; + int success = 0; switch (data->format) { case SCREENSHOT_JPEG: - result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); + success = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); break; case SCREENSHOT_PNG: - result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + success = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); break; } - if (!result) + if (!success) { DEBUG_LOG(("Failed to write screenshot %s", data->pathname)); } - delete [] data->imageData; delete data; - return 0; + return success; } void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) @@ -177,7 +181,6 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) } else { - delete [] threadData->imageData; delete threadData; } } From 0e3dabe485fe0b1966fea28d35d8d775e1c07d12 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 00:40:28 +0400 Subject: [PATCH 17/31] tweak(screenshot): Look up file extension from format array with completeness check --- Core/GameEngine/Include/GameClient/Display.h | 4 +++- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h index d7241cf7b72..50e14a28453 100644 --- a/Core/GameEngine/Include/GameClient/Display.h +++ b/Core/GameEngine/Include/GameClient/Display.h @@ -36,7 +36,9 @@ enum ScreenshotFormat { SCREENSHOT_JPEG, - SCREENSHOT_PNG + SCREENSHOT_PNG, + + SCREENSHOT_FORMAT_COUNT }; struct ShroudLevel diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 872f688234b..cc767660b00 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -66,9 +66,12 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) { + static const char* const ScreenshotFormatExtensions[] = { "jpg", "png" }; + static_assert(ARRAY_SIZE(ScreenshotFormatExtensions) == SCREENSHOT_FORMAT_COUNT, "Incorrect array size"); + char leafname[_MAX_FNAME]; char pathname[_MAX_PATH]; - const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; + const char* extension = ScreenshotFormatExtensions[format]; SYSTEMTIME st; GetLocalTime(&st); From bbb154e77eba3065809f07aaf11262be6f14180e Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 00:40:42 +0400 Subject: [PATCH 18/31] tweak(screenshot): Cap JPEG quality at 95 and make getter const --- Core/GameEngine/Include/Common/OptionPreferences.h | 2 +- Core/GameEngine/Source/Common/OptionPreferences.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 3abcbb9a0aa..30fae8e7acd 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -72,7 +72,7 @@ class OptionPreferences : public UserPreferences Bool getRightMouseScrollWithAlternateMouseEnabled() const; Bool getRetaliationModeEnabled(); Bool getDoubleClickAttackMoveEnabled(); - Int getJPEGQuality(); + Int getJPEGQuality() const; Real getScrollFactor(); Bool getDrawScrollAnchor(); Bool getMoveScrollAnchor(); diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index 755b4b0c439..d359834ff02 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -239,14 +239,16 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled() return FALSE; } -Int OptionPreferences::getJPEGQuality() +Int OptionPreferences::getJPEGQuality() const { OptionPreferences::const_iterator it = find("JPEGQuality"); if (it == end()) return 80; + // TheSuperHackers @tweak bobtista 08/07/2026 Cap the quality at 95, because JPEG quality + // above that increases the file size significantly with no visible benefit. Int quality = atoi(it->second.str()); - return clamp(1, quality, 100); + return clamp(1, quality, 95); } Real OptionPreferences::getScrollFactor() From 7b46fad2e84e5fc0ae0a8996b3e7a39897325b64 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 00:40:58 +0400 Subject: [PATCH 19/31] style(screenshot): Remove key mappings from message comments and update stb.cmake comment --- Generals/Code/GameEngine/Include/Common/MessageStream.h | 4 ++-- GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h | 4 ++-- cmake/stb.cmake | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 15a9b5e2344..4b182c27578 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -256,8 +256,8 @@ class GameMessage : public MemoryPoolObject MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. - MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) - MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot (CTRL+F12, lossless) + MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot + MSG_META_TAKE_SCREENSHOT_PNG, ///< take lossless PNG screenshot MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index ebf97de5fc4..5476dce8850 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -256,8 +256,8 @@ class GameMessage : public MemoryPoolObject MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. - MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) - MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot (CTRL+F12, lossless) + MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot + MSG_META_TAKE_SCREENSHOT_PNG, ///< take lossless PNG screenshot MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/cmake/stb.cmake b/cmake/stb.cmake index 42a6bc5ad90..0712a7d3f60 100644 --- a/cmake/stb.cmake +++ b/cmake/stb.cmake @@ -1,5 +1,4 @@ -# TheSuperHackers @build bobtista 02/11/2025 STB single-file public domain libraries for image encoding -# https://github.com/nothings/stb +# TheSuperHackers @build bobtista 02/11/2025 Fetch the stb library for writing JPEG and PNG screenshots. find_package(Stb CONFIG QUIET) From 30256c80987e073bf22b2056a8a1e84978f0e04c Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 15:01:57 +0400 Subject: [PATCH 20/31] style(screenshot): Remove TheSuperHackers comment prefix from stb.cmake --- cmake/stb.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/stb.cmake b/cmake/stb.cmake index 0712a7d3f60..26481b8357e 100644 --- a/cmake/stb.cmake +++ b/cmake/stb.cmake @@ -1,4 +1,4 @@ -# TheSuperHackers @build bobtista 02/11/2025 Fetch the stb library for writing JPEG and PNG screenshots. +# Fetch the stb library for writing JPEG and PNG screenshots. find_package(Stb CONFIG QUIET) From dee0bbb55045e386238508a54974da4b81ffcf1a Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 21:46:04 +0400 Subject: [PATCH 21/31] chore(screenshot): Remove unify script entry for the new stb_image_write_impl file --- scripts/cpp/unify_move_files.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py index 8bb57341344..b95939719ec 100644 --- a/scripts/cpp/unify_move_files.py +++ b/scripts/cpp/unify_move_files.py @@ -311,7 +311,6 @@ def main(): #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp") #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp") #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp") - #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp") #unify_file(Game.ZEROHOUR, "GameEngine/Include/Common/UserPreferences.h", Game.CORE, "GameEngine/Include/Common/UserPreferences.h") #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/UserPreferences.cpp", Game.CORE, "GameEngine/Source/Common/UserPreferences.cpp") From dd3aa650916e0cc1eaaf7ae8153627e37f9e6c72 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 21:46:04 +0400 Subject: [PATCH 22/31] refactor(screenshot): Move pixel conversion and file operations to the screenshot thread --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 119 +++++++++++------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index cc767660b00..d731cde1db2 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -26,15 +26,22 @@ struct ScreenshotThreadData { + ScreenshotThreadData() + : pixelData(nullptr) + { + } + ~ScreenshotThreadData() { - delete [] imageData; + delete [] pixelData; } - unsigned char* imageData; + unsigned char* pixelData; unsigned int width; unsigned int height; - char pathname[_MAX_PATH]; + bool is16Bit; + char directory[_MAX_PATH]; + char leafname[_MAX_FNAME]; int quality; ScreenshotFormat format; }; @@ -43,22 +50,68 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) { ScreenshotThreadData* data = (ScreenshotThreadData*)param; + CreateDirectory(data->directory, nullptr); + + char pathname[_MAX_PATH]; + strlcpy(pathname, data->directory, ARRAY_SIZE(pathname)); + strlcat(pathname, data->leafname, ARRAY_SIZE(pathname)); + + const unsigned int width = data->width; + const unsigned int height = data->height; + unsigned int x, y, index; + + // Convert to R8G8B8 for stb_image_write. + unsigned char* image = new unsigned char[3 * width * height]; + + if (!data->is16Bit) + { + // Convert A8R8G8B8/X8R8G8B8 to R8G8B8 + for (y = 0; y < height; y++) + { + const unsigned char* srcLine = data->pixelData + y * width * 4; + for (x = 0; x < width; x++) + { + index = 3 * (x + y * width); + image[index + 0] = srcLine[4 * x + 2]; + image[index + 1] = srcLine[4 * x + 1]; + image[index + 2] = srcLine[4 * x + 0]; + } + } + } + else + { + // Convert R5G6B5 to R8G8B8 + for (y = 0; y < height; y++) + { + const unsigned short* srcLine = (const unsigned short*)(data->pixelData + y * width * 2); + for (x = 0; x < width; x++) + { + const unsigned short rgb = srcLine[x]; + index = 3 * (x + y * width); + image[index + 0] = (unsigned char)((rgb & 0xF800) >> 8); + image[index + 1] = (unsigned char)((rgb & 0x07E0) >> 3); + image[index + 2] = (unsigned char)((rgb & 0x001F) << 3); + } + } + } + int success = 0; switch (data->format) { case SCREENSHOT_JPEG: - success = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); + success = stbi_write_jpg(pathname, width, height, 3, image, data->quality); break; case SCREENSHOT_PNG: - success = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + success = stbi_write_png(pathname, width, height, 3, image, width * 3); break; } if (!success) { - DEBUG_LOG(("Failed to write screenshot %s", data->pathname)); + DEBUG_LOG(("Failed to write screenshot %s", pathname)); } + delete [] image; delete data; return success; @@ -69,20 +122,20 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) static const char* const ScreenshotFormatExtensions[] = { "jpg", "png" }; static_assert(ARRAY_SIZE(ScreenshotFormatExtensions) == SCREENSHOT_FORMAT_COUNT, "Incorrect array size"); + // The filename is created here because the success message below shows it. char leafname[_MAX_FNAME]; - char pathname[_MAX_PATH]; const char* extension = ScreenshotFormatExtensions[format]; SYSTEMTIME st; GetLocalTime(&st); sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension); + // TheSuperHackers @feature bobtista 08/07/2026 Save screenshots into a Screenshots subfolder // to keep them out of the User Data root. - strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); - strlcat(pathname, "Screenshots\\", ARRAY_SIZE(pathname)); - CreateDirectory(pathname, nullptr); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + char directory[_MAX_PATH]; + strlcpy(directory, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(directory)); + strlcat(directory, "Screenshots\\", ARRAY_SIZE(directory)); // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. // Originally this code took the front buffer and tried to lock it. This does not work when the @@ -122,42 +175,18 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) return; } - unsigned int x, y, index; + unsigned int y; unsigned int width = surfaceDesc.Width; unsigned int height = surfaceDesc.Height; - unsigned char* image = new unsigned char[3 * width * height]; + // Copy the surface rows into a tightly packed buffer. The pixel conversion and all file + // operations are done on the screenshot thread to keep the main thread cheap. + const unsigned int bytesPerPixel = is32Bit ? 4 : 2; + unsigned char* pixels = new unsigned char[bytesPerPixel * width * height]; - if (is32Bit) + for (y = 0; y < height; y++) { - // Convert A8R8G8B8/X8R8G8B8 to R8G8B8 - for (y = 0; y < height; y++) - { - const unsigned char* srcLine = (const unsigned char*)lrect.pBits + y * lrect.Pitch; - for (x = 0; x < width; x++) - { - index = 3 * (x + y * width); - image[index] = srcLine[4 * x + 2]; - image[index + 1] = srcLine[4 * x + 1]; - image[index + 2] = srcLine[4 * x + 0]; - } - } - } - else - { - // Convert R5G6B5 to R8G8B8 - for (y = 0; y < height; y++) - { - const unsigned short* srcLine = (const unsigned short*)((const unsigned char*)lrect.pBits + y * lrect.Pitch); - for (x = 0; x < width; x++) - { - const unsigned short rgb = srcLine[x]; - index = 3 * (x + y * width); - image[index] = (unsigned char)((rgb & 0xF800) >> 8); - image[index + 1] = (unsigned char)((rgb & 0x07E0) >> 3); - image[index + 2] = (unsigned char)((rgb & 0x001F) << 3); - } - } + memcpy(pixels + y * width * bytesPerPixel, (const unsigned char*)lrect.pBits + y * lrect.Pitch, width * bytesPerPixel); } surfaceCopy->Unlock(); @@ -165,12 +194,14 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) surfaceCopy = nullptr; ScreenshotThreadData* threadData = new ScreenshotThreadData(); - threadData->imageData = image; + threadData->pixelData = pixels; threadData->width = width; threadData->height = height; + threadData->is16Bit = is16Bit; threadData->quality = jpegQuality; threadData->format = format; - strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); + strlcpy(threadData->directory, directory, ARRAY_SIZE(threadData->directory)); + strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); DWORD threadId; HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); From 47f029b898b937ef92b4e95a8b14f6e7dff7102f Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 21:46:04 +0400 Subject: [PATCH 23/31] style(screenshot): Add TheSuperHackers comments to new GlobalData and MessageStream members --- Generals/Code/GameEngine/Include/Common/GlobalData.h | 2 +- Generals/Code/GameEngine/Include/Common/MessageStream.h | 1 + GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 2 +- GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 6f79e4c1fbd..089d3d9049a 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -143,7 +143,7 @@ class GlobalData : public SubsystemInterface Bool m_clientRetaliationModeEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; - Int m_jpegQuality; + Int m_jpegQuality; // TheSuperHackers @feature bobtista 09/07/2026 Quality for JPEG screenshots. Bool m_useWaterPlane; Bool m_useCloudPlane; Bool m_useShadowVolumes; diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 4b182c27578..728b27661a5 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -257,6 +257,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot + // TheSuperHackers @feature bobtista 09/07/2026 New message type for taking PNG screenshots. MSG_META_TAKE_SCREENSHOT_PNG, ///< take lossless PNG screenshot MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 844e3bf986c..70f8b39334f 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -144,7 +144,7 @@ class GlobalData : public SubsystemInterface Bool m_clientRetaliationModeEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; - Int m_jpegQuality; + Int m_jpegQuality; // TheSuperHackers @feature bobtista 09/07/2026 Quality for JPEG screenshots. Bool m_useWaterPlane; Bool m_useCloudPlane; Bool m_useShadowVolumes; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 5476dce8850..99fac7d513c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -257,6 +257,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot + // TheSuperHackers @feature bobtista 09/07/2026 New message type for taking PNG screenshots. MSG_META_TAKE_SCREENSHOT_PNG, ///< take lossless PNG screenshot MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode From f54185dc074c65aee04ae251bc43458442aec6f9 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 22:06:07 +0400 Subject: [PATCH 24/31] refactor(screenshot): Assemble the screenshot directory path on the screenshot thread --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index d731cde1db2..1261fb7b1d9 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -40,7 +40,7 @@ struct ScreenshotThreadData unsigned int width; unsigned int height; bool is16Bit; - char directory[_MAX_PATH]; + char userDataDirectory[_MAX_PATH]; char leafname[_MAX_FNAME]; int quality; ScreenshotFormat format; @@ -50,10 +50,12 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) { ScreenshotThreadData* data = (ScreenshotThreadData*)param; - CreateDirectory(data->directory, nullptr); - + // TheSuperHackers @feature bobtista 08/07/2026 Save screenshots into a Screenshots subfolder + // to keep them out of the User Data root. char pathname[_MAX_PATH]; - strlcpy(pathname, data->directory, ARRAY_SIZE(pathname)); + strlcpy(pathname, data->userDataDirectory, ARRAY_SIZE(pathname)); + strlcat(pathname, "Screenshots\\", ARRAY_SIZE(pathname)); + CreateDirectory(pathname, nullptr); strlcat(pathname, data->leafname, ARRAY_SIZE(pathname)); const unsigned int width = data->width; @@ -131,12 +133,6 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension); - // TheSuperHackers @feature bobtista 08/07/2026 Save screenshots into a Screenshots subfolder - // to keep them out of the User Data root. - char directory[_MAX_PATH]; - strlcpy(directory, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(directory)); - strlcat(directory, "Screenshots\\", ARRAY_SIZE(directory)); - // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. // Originally this code took the front buffer and tried to lock it. This does not work when the // render view clips outside the desktop boundaries. It crashed the game. @@ -200,7 +196,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) threadData->is16Bit = is16Bit; threadData->quality = jpegQuality; threadData->format = format; - strlcpy(threadData->directory, directory, ARRAY_SIZE(threadData->directory)); + strlcpy(threadData->userDataDirectory, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(threadData->userDataDirectory)); strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); DWORD threadId; From 6ffc6df05794db9bdf63e50ccde60ccfc37c0d28 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 22:06:07 +0400 Subject: [PATCH 25/31] style(screenshot): Simplify TheSuperHackers comments on new GlobalData and MessageStream members --- Generals/Code/GameEngine/Include/Common/GlobalData.h | 2 +- Generals/Code/GameEngine/Include/Common/MessageStream.h | 3 +-- GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h | 2 +- GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 089d3d9049a..94357cde728 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -143,7 +143,7 @@ class GlobalData : public SubsystemInterface Bool m_clientRetaliationModeEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; - Int m_jpegQuality; // TheSuperHackers @feature bobtista 09/07/2026 Quality for JPEG screenshots. + Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. Bool m_useWaterPlane; Bool m_useCloudPlane; Bool m_useShadowVolumes; diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 728b27661a5..17b141778f2 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -257,8 +257,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot - // TheSuperHackers @feature bobtista 09/07/2026 New message type for taking PNG screenshots. - MSG_META_TAKE_SCREENSHOT_PNG, ///< take lossless PNG screenshot + MSG_META_TAKE_SCREENSHOT_PNG, ///< TheSuperHackers @feature Take lossless PNG screenshot MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 70f8b39334f..a3a54b96f8c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -144,7 +144,7 @@ class GlobalData : public SubsystemInterface Bool m_clientRetaliationModeEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; - Int m_jpegQuality; // TheSuperHackers @feature bobtista 09/07/2026 Quality for JPEG screenshots. + Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. Bool m_useWaterPlane; Bool m_useCloudPlane; Bool m_useShadowVolumes; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 99fac7d513c..5e27ea5fb0c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -257,8 +257,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot - // TheSuperHackers @feature bobtista 09/07/2026 New message type for taking PNG screenshots. - MSG_META_TAKE_SCREENSHOT_PNG, ///< take lossless PNG screenshot + MSG_META_TAKE_SCREENSHOT_PNG, ///< TheSuperHackers @feature Take lossless PNG screenshot MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode From d8e76291a23b96dc3703489d3efc3a744c24e2eb Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 9 Jul 2026 22:24:22 +0400 Subject: [PATCH 26/31] style(screenshot): Fix comment keyword and wording per review --- Core/GameEngine/Source/Common/OptionPreferences.cpp | 2 +- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index d359834ff02..4705a08b009 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -245,7 +245,7 @@ Int OptionPreferences::getJPEGQuality() const if (it == end()) return 80; - // TheSuperHackers @tweak bobtista 08/07/2026 Cap the quality at 95, because JPEG quality + // TheSuperHackers @feature bobtista 08/07/2026 Cap the quality at 95, because JPEG quality // above that increases the file size significantly with no visible benefit. Int quality = atoi(it->second.str()); return clamp(1, quality, 95); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 1261fb7b1d9..7d3e84791c8 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -51,7 +51,7 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) ScreenshotThreadData* data = (ScreenshotThreadData*)param; // TheSuperHackers @feature bobtista 08/07/2026 Save screenshots into a Screenshots subfolder - // to keep them out of the User Data root. + // to keep the user data root folder tidy. char pathname[_MAX_PATH]; strlcpy(pathname, data->userDataDirectory, ARRAY_SIZE(pathname)); strlcat(pathname, "Screenshots\\", ARRAY_SIZE(pathname)); From e37d21617d0be24f8280ca61d05f059a1edae954 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 10 Jul 2026 02:04:19 +0400 Subject: [PATCH 27/31] refactor(screenshot): Use endian-safe shifts for 32 bit pixel conversion and C++ casts --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 7d3e84791c8..04fd158b936 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -48,7 +48,7 @@ struct ScreenshotThreadData static DWORD WINAPI screenshotThreadFunc(LPVOID param) { - ScreenshotThreadData* data = (ScreenshotThreadData*)param; + ScreenshotThreadData* data = static_cast(param); // TheSuperHackers @feature bobtista 08/07/2026 Save screenshots into a Screenshots subfolder // to keep the user data root folder tidy. @@ -68,25 +68,26 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) if (!data->is16Bit) { // Convert A8R8G8B8/X8R8G8B8 to R8G8B8 - for (y = 0; y < height; y++) + for (y = 0; y < height; ++y) { - const unsigned char* srcLine = data->pixelData + y * width * 4; - for (x = 0; x < width; x++) + const unsigned int* srcLine = reinterpret_cast(data->pixelData + y * width * 4); + for (x = 0; x < width; ++x) { + const unsigned int argb = srcLine[x]; index = 3 * (x + y * width); - image[index + 0] = srcLine[4 * x + 2]; - image[index + 1] = srcLine[4 * x + 1]; - image[index + 2] = srcLine[4 * x + 0]; + image[index + 0] = (unsigned char)((argb >> 16) & 0xFF); // r + image[index + 1] = (unsigned char)((argb >> 8) & 0xFF); // g + image[index + 2] = (unsigned char)((argb >> 0) & 0xFF); // b } } } else { // Convert R5G6B5 to R8G8B8 - for (y = 0; y < height; y++) + for (y = 0; y < height; ++y) { - const unsigned short* srcLine = (const unsigned short*)(data->pixelData + y * width * 2); - for (x = 0; x < width; x++) + const unsigned short* srcLine = reinterpret_cast(data->pixelData + y * width * 2); + for (x = 0; x < width; ++x) { const unsigned short rgb = srcLine[x]; index = 3 * (x + y * width); @@ -180,9 +181,9 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) const unsigned int bytesPerPixel = is32Bit ? 4 : 2; unsigned char* pixels = new unsigned char[bytesPerPixel * width * height]; - for (y = 0; y < height; y++) + for (y = 0; y < height; ++y) { - memcpy(pixels + y * width * bytesPerPixel, (const unsigned char*)lrect.pBits + y * lrect.Pitch, width * bytesPerPixel); + memcpy(pixels + y * width * bytesPerPixel, reinterpret_cast(lrect.pBits) + y * lrect.Pitch, width * bytesPerPixel); } surfaceCopy->Unlock(); From 7ebe951cb332785fe1bfa34597051a3026856ee4 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 10 Jul 2026 02:04:19 +0400 Subject: [PATCH 28/31] style(screenshot): Change quality cap comment keyword to @info --- Core/GameEngine/Source/Common/OptionPreferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index 4705a08b009..72039cedf76 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -245,7 +245,7 @@ Int OptionPreferences::getJPEGQuality() const if (it == end()) return 80; - // TheSuperHackers @feature bobtista 08/07/2026 Cap the quality at 95, because JPEG quality + // TheSuperHackers @info bobtista 08/07/2026 Cap the quality at 95, because JPEG quality // above that increases the file size significantly with no visible benefit. Int quality = atoi(it->second.str()); return clamp(1, quality, 95); From 453ca4cd9e9a956dd8733572be48f35139877303 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 10 Jul 2026 15:52:18 +0400 Subject: [PATCH 29/31] style(screenshot): Remove redundant masks and add rgb comments to the 16 bit conversion --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 04fd158b936..b305b8ff2b8 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -75,9 +75,9 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) { const unsigned int argb = srcLine[x]; index = 3 * (x + y * width); - image[index + 0] = (unsigned char)((argb >> 16) & 0xFF); // r - image[index + 1] = (unsigned char)((argb >> 8) & 0xFF); // g - image[index + 2] = (unsigned char)((argb >> 0) & 0xFF); // b + image[index + 0] = (unsigned char)(argb >> 16); // r + image[index + 1] = (unsigned char)(argb >> 8); // g + image[index + 2] = (unsigned char)(argb >> 0); // b } } } @@ -91,9 +91,9 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) { const unsigned short rgb = srcLine[x]; index = 3 * (x + y * width); - image[index + 0] = (unsigned char)((rgb & 0xF800) >> 8); - image[index + 1] = (unsigned char)((rgb & 0x07E0) >> 3); - image[index + 2] = (unsigned char)((rgb & 0x001F) << 3); + image[index + 0] = (unsigned char)((rgb & 0xF800) >> 8); // r + image[index + 1] = (unsigned char)((rgb & 0x07E0) >> 3); // g + image[index + 2] = (unsigned char)((rgb & 0x001F) << 3); // b } } } From 0503cc9bc908585c73851cb1d13ead4858a949d7 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 10 Jul 2026 15:52:18 +0400 Subject: [PATCH 30/31] fix(screenshot): Show the screenshot success message after the file is written --- .../W3DDevice/GameClient/W3DScreenshot.h | 4 +++ .../W3DDevice/GameClient/W3DScreenshot.cpp | 32 +++++++++++++++---- .../W3DDevice/GameClient/W3DDisplay.cpp | 3 ++ .../W3DDevice/GameClient/W3DDisplay.cpp | 3 ++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index 6e2e577d475..e6edec10cd8 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -22,3 +22,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality); +// Called once per frame on the main thread to show messages for screenshots +// that the screenshot thread has finished writing. +void W3D_UpdateScreenshotMessages(); + diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index b305b8ff2b8..dcef1cd70ea 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -46,6 +46,11 @@ struct ScreenshotThreadData ScreenshotFormat format; }; +// TheInGameUI is not thread safe, so the screenshot thread cannot show the success message +// itself. It posts the written filename here and the main thread shows the message in +// W3D_UpdateScreenshotMessages. +static void* volatile s_screenshotWrittenLeafname = nullptr; + static DWORD WINAPI screenshotThreadFunc(LPVOID param) { ScreenshotThreadData* data = static_cast(param); @@ -109,7 +114,14 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) break; } - if (!success) + if (success) + { + char* leafname = new char[_MAX_FNAME]; + strlcpy(leafname, data->leafname, _MAX_FNAME); + char* oldLeafname = static_cast(InterlockedExchangePointer(&s_screenshotWrittenLeafname, leafname)); + delete [] oldLeafname; + } + else { DEBUG_LOG(("Failed to write screenshot %s", pathname)); } @@ -120,12 +132,24 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) return success; } +void W3D_UpdateScreenshotMessages() +{ + char* leafname = static_cast(InterlockedExchangePointer(&s_screenshotWrittenLeafname, nullptr)); + if (leafname != nullptr) + { + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); + delete [] leafname; + } +} + void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) { static const char* const ScreenshotFormatExtensions[] = { "jpg", "png" }; static_assert(ARRAY_SIZE(ScreenshotFormatExtensions) == SCREENSHOT_FORMAT_COUNT, "Incorrect array size"); - // The filename is created here because the success message below shows it. + // The filename is created here so the timestamp matches the capture time. char leafname[_MAX_FNAME]; const char* extension = ScreenshotFormatExtensions[format]; @@ -205,10 +229,6 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) if (hThread) { CloseHandle(hThread); - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } else { diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 3f06b040f1b..a6a95c8a4fe 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1735,6 +1735,9 @@ void W3DDisplay::draw() if (TheGlobalData->m_headless) return; + // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread. + W3D_UpdateScreenshotMessages(); + updateAverageFPS(); if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD()) { diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 57c62bef9c3..0e7aa5d5482 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1805,6 +1805,9 @@ void W3DDisplay::draw() if (TheGlobalData->m_headless) return; + // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread. + W3D_UpdateScreenshotMessages(); + updateAverageFPS(); if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD()) { From 9a52419e2338078d58d70afde6cbd747cdaf4083 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 10 Jul 2026 17:37:32 +0400 Subject: [PATCH 31/31] build(screenshot): Add InterlockedExchangePointer adapter for VC6 --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 1 + Dependencies/Utility/Utility/interlocked_adapter.h | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index dcef1cd70ea..fc29477c734 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -22,6 +22,7 @@ #include "GameClient/InGameUI.h" #include "WW3D2/dx8wrapper.h" #include "WW3D2/surfaceclass.h" +#include #include struct ScreenshotThreadData diff --git a/Dependencies/Utility/Utility/interlocked_adapter.h b/Dependencies/Utility/Utility/interlocked_adapter.h index 45da6a2f9cb..5405f6ccb8c 100644 --- a/Dependencies/Utility/Utility/interlocked_adapter.h +++ b/Dependencies/Utility/Utility/interlocked_adapter.h @@ -25,4 +25,9 @@ inline long InterlockedCompareExchange(long volatile *Destination, long Exchange return (long)InterlockedCompareExchange((PVOID*)Destination, (PVOID)Exchange, (PVOID)Comparand); } +inline PVOID InterlockedExchangePointer(PVOID volatile *Target, PVOID Value) +{ + return (PVOID)InterlockedExchange((LPLONG)Target, (LONG)Value); +} + #endif