From 13430b4369285593b623bc274050b76b3186a83c Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:15 -0500 Subject: [PATCH 01/24] Add stb library dependency via FetchContent --- CMakeLists.txt | 1 + cmake/stb.cmake | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) 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..957c7d235d0 --- /dev/null +++ b/cmake/stb.cmake @@ -0,0 +1,17 @@ +# TheSuperHackers @bobtista 02/11/2025 +# STB single-file public domain libraries for image encoding +# https://github.com/nothings/stb + +FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master # Could pin to specific commit for stability + GIT_SHALLOW TRUE +) + +FetchContent_MakeAvailable(stb) + +# Create interface library for stb headers +add_library(stb INTERFACE) +target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) + From d53fafd93d0f82b2b2bbe3ec32594475c4011611 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:22 -0500 Subject: [PATCH 02/24] Add MSG_META_TAKE_SCREENSHOT_COMPRESSED message and F11 keybinding Add takeScreenShotCompressed() to Display interface --- .../Code/GameEngine/Include/Common/MessageStream.h | 1 + Generals/Code/GameEngine/Include/GameClient/Display.h | 1 + .../Code/GameEngine/Source/Common/MessageStream.cpp | 1 + .../Source/GameClient/MessageStream/CommandXlat.cpp | 8 ++++++++ .../Source/GameClient/MessageStream/MetaEvent.cpp | 11 +++++++++++ .../Code/GameEngine/Include/Common/MessageStream.h | 1 + .../Code/GameEngine/Include/GameClient/Display.h | 1 + .../Code/GameEngine/Source/Common/MessageStream.cpp | 1 + .../Source/GameClient/MessageStream/CommandXlat.cpp | 8 ++++++++ .../Source/GameClient/MessageStream/MetaEvent.cpp | 11 +++++++++++ 10 files changed, 44 insertions(+) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 25413a73f80..4b4f5463b28 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -258,6 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take screenshot + MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< TheSuperHackers @bobtista take compressed screenshot (JPG/PNG) without stalling MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index 33238a7a450..179a858b319 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -167,6 +167,7 @@ class Display : public SubsystemInterface virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset virtual void takeScreenShot(void) = 0; ///< saves screenshot to a file + virtual void takeScreenShotCompressed(void) = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp index 2fe31017bf3..1b05257d904 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,6 +364,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_COMPRESSED) 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/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 42932540419..5296d5fdb61 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3419,6 +3419,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { if (TheDisplay) TheDisplay->takeScreenShot(); + break; + } + + // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling + case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: + { + if (TheDisplay) + TheDisplay->takeScreenShotCompressed(); disp = DESTROY_MESSAGE; break; } diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index c1dbae122eb..c9102fd1f5a 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -163,6 +163,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, + { "TAKE_SCREENSHOT_COMPRESSED", GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -813,6 +814,16 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) map->m_usableIn = COMMANDUSABLE_GAME; } } + { + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); + if (map->m_key == MK_NONE) + { + map->m_key = MK_F11; + map->m_transition = DOWN; + map->m_modState = NONE; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } #if defined(RTS_DEBUG) { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index fbdb8596731..2882632ccca 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -258,6 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take screenshot + MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< TheSuperHackers @bobtista take compressed screenshot (JPG/PNG) without stalling MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index c6c69b62298..cb2913cf065 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -167,6 +167,7 @@ class Display : public SubsystemInterface virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset virtual void takeScreenShot(void) = 0; ///< saves screenshot to a file + virtual void takeScreenShotCompressed(void) = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index 809f59f5ffa..d0ff30a9b83 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,6 +364,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_COMPRESSED) 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/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index c90804d231f..146b894c335 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3752,6 +3752,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { if (TheDisplay) TheDisplay->takeScreenShot(); + break; + } + + // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling + case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: + { + if (TheDisplay) + TheDisplay->takeScreenShotCompressed(); disp = DESTROY_MESSAGE; break; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 9d9df747c00..92d405b2506 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/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_COMPRESSED", GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -871,6 +872,16 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) map->m_usableIn = COMMANDUSABLE_GAME; } } + { + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); + if (map->m_key == MK_NONE) + { + map->m_key = MK_F11; + map->m_transition = DOWN; + map->m_modState = NONE; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } #if defined(RTS_DEBUG) { From 50ed01e3713d0d675affdc8a43d3a5ab5949efd9 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:34 -0500 Subject: [PATCH 03/24] Implement threaded JPEG screenshot for GeneralsMD Implement threaded JPEG screenshot for Generals Link stb library to GameEngineDevice targets Remove excessive comments from screenshot implementation --- .../GameEngine/Include/Common/MessageStream.h | 2 +- .../GameEngine/Include/GameClient/Display.h | 2 +- .../GameClient/MessageStream/CommandXlat.cpp | 1 - Generals/Code/GameEngineDevice/CMakeLists.txt | 1 + .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 103 ++++++++++++++++++ .../GameEngine/Include/Common/MessageStream.h | 2 +- .../GameEngine/Include/GameClient/Display.h | 2 +- .../GameClient/MessageStream/CommandXlat.cpp | 1 - .../Code/GameEngineDevice/CMakeLists.txt | 1 + .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 103 ++++++++++++++++++ 12 files changed, 214 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 4b4f5463b28..8d69f786df2 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -258,7 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take screenshot - MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< TheSuperHackers @bobtista take compressed screenshot (JPG/PNG) without stalling + MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< take compressed screenshot without stalling MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index 179a858b319..1fad658b881 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -167,7 +167,7 @@ class Display : public SubsystemInterface virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset virtual void takeScreenShot(void) = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed(void) = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(void) = 0; ///< saves compressed screenshot without stalling virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 5296d5fdb61..aa6bf092610 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3422,7 +3422,6 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } - // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: { if (TheDisplay) diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index a5c83215b7e..7674beadb07 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -200,6 +200,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 472d9d31bcd..4afd44ab8f3 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -121,6 +121,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer( void ) ; ///< Create a video buffer that can be used for this display virtual void takeScreenShot(void); //save screenshot to file + virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); /// #include #include +#include +#include + +// TheSuperHackers @bobtista 02/11/2025 STB for image encoding +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include // USER INCLUDES ////////////////////////////////////////////////////////////// #include "Common/FramePacer.h" @@ -3016,6 +3022,103 @@ void W3DDisplay::takeScreenShot(void) TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } +/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game +/// This implementation captures the frame buffer on the main thread, then spawns a background thread +/// to compress and save the image, allowing the game to continue running smoothly. +void W3DDisplay::takeScreenShotCompressed(void) +{ + // TheSuperHackers @bobtista 02/11/2025 Find next available filename + char leafname[256]; + char pathname[1024]; + static int frame_number = 1; + + Bool done = false; + while (!done) { + sprintf(leafname, "sshot%.3d.jpg", frame_number++); + strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + if (_access(pathname, 0) == -1) + done = true; + } + + // TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy + 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(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + + surface->Release_Ref(); + surface = NULL; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == NULL) + { + surfaceCopy->Release_Ref(); + return; + } + + unsigned int x, y, index, index2; + unsigned int width = surfaceDesc.Width; + unsigned int height = surfaceDesc.Height; + + // TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data + // Using shared_ptr for automatic cleanup in the background thread + std::shared_ptr imageData(new unsigned char[3 * width * height], + std::default_delete()); + unsigned char* image = imageData.get(); + + // TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB + 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); // R + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B + } + } + + surfaceCopy->Unlock(); + surfaceCopy->Release_Ref(); + surfaceCopy = NULL; + + // TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread + std::string pathnameCopy(pathname); + std::string leafnameCopy(leafname); + + // TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image + // This allows the game to continue running without freezing + std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() { + // TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100) + // stbi_write_jpg expects image data with Y-axis going down, which matches our data + int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90); + + if (!result) { + // TheSuperHackers @bobtista 02/11/2025 Log error if write failed + // Note: Can't show UI message from background thread + OutputDebugStringA("Failed to write screenshot JPEG\n"); + } + + // TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope + }).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently + + // TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background) + UnicodeString ufileName; + ufileName.translate(leafnameCopy.c_str()); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); +} + /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture(void) { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 2882632ccca..d657f07ac03 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -258,7 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take screenshot - MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< TheSuperHackers @bobtista take compressed screenshot (JPG/PNG) without stalling + MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< take compressed screenshot without stalling MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index cb2913cf065..7de97d03f3e 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -167,7 +167,7 @@ class Display : public SubsystemInterface virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset virtual void takeScreenShot(void) = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed(void) = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(void) = 0; ///< saves compressed screenshot without stalling virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 146b894c335..52f4a57da63 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3755,7 +3755,6 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } - // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: { if (TheDisplay) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 5f6e8cae882..ad2470f24f4 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -213,6 +213,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 dc997134a9f..2a173a4bf8d 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -121,6 +121,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer( void ) ; ///< Create a video buffer that can be used for this display virtual void takeScreenShot(void); //save screenshot to file + virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); /// #include #include +#include +#include + +// TheSuperHackers @bobtista 02/11/2025 STB for image encoding +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include // USER INCLUDES ////////////////////////////////////////////////////////////// #include "Common/FramePacer.h" @@ -3128,6 +3134,103 @@ void W3DDisplay::takeScreenShot(void) TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } +/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game +/// This implementation captures the frame buffer on the main thread, then spawns a background thread +/// to compress and save the image, allowing the game to continue running smoothly. +void W3DDisplay::takeScreenShotCompressed(void) +{ + // TheSuperHackers @bobtista 02/11/2025 Find next available filename + char leafname[256]; + char pathname[1024]; + static int frame_number = 1; + + Bool done = false; + while (!done) { + sprintf(leafname, "sshot%.3d.jpg", frame_number++); + strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + if (_access(pathname, 0) == -1) + done = true; + } + + // TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy + 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(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + + surface->Release_Ref(); + surface = NULL; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == NULL) + { + surfaceCopy->Release_Ref(); + return; + } + + unsigned int x, y, index, index2; + unsigned int width = surfaceDesc.Width; + unsigned int height = surfaceDesc.Height; + + // TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data + // Using shared_ptr for automatic cleanup in the background thread + std::shared_ptr imageData(new unsigned char[3 * width * height], + std::default_delete()); + unsigned char* image = imageData.get(); + + // TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB + 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); // R + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B + } + } + + surfaceCopy->Unlock(); + surfaceCopy->Release_Ref(); + surfaceCopy = NULL; + + // TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread + std::string pathnameCopy(pathname); + std::string leafnameCopy(leafname); + + // TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image + // This allows the game to continue running without freezing + std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() { + // TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100) + // stbi_write_jpg expects image data with Y-axis going down, which matches our data + int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90); + + if (!result) { + // TheSuperHackers @bobtista 02/11/2025 Log error if write failed + // Note: Can't show UI message from background thread + OutputDebugStringA("Failed to write screenshot JPEG\n"); + } + + // TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope + }).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently + + // TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background) + UnicodeString ufileName; + ufileName.translate(leafnameCopy.c_str()); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); +} + /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture(void) { From c8a27bc85f607fa781344a01332e13d042dbdb92 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 23:29:47 -0500 Subject: [PATCH 04/24] Use Win32 CreateThread for VC6 compatibility and add GUIEditDisplay stub --- .../W3DDevice/GameClient/W3DDisplay.cpp | 82 ++++++++++--------- .../Tools/GUIEdit/Include/GUIEditDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 82 ++++++++++--------- .../Tools/GUIEdit/Include/GUIEditDisplay.h | 1 + 4 files changed, 88 insertions(+), 78 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 495660f3dbf..9bf32f20fd9 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -39,10 +39,7 @@ static void drawFramerateBar(void); #include #include #include -#include -#include -// TheSuperHackers @bobtista 02/11/2025 STB for image encoding #define STB_IMAGE_WRITE_IMPLEMENTATION #include @@ -3022,12 +3019,33 @@ void W3DDisplay::takeScreenShot(void) TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } -/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game -/// This implementation captures the frame buffer on the main thread, then spawns a background thread -/// to compress and save the image, allowing the game to continue running smoothly. +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[1024]; + char leafname[256]; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, 90); + + if (!result) { + OutputDebugStringA("Failed to write screenshot JPEG\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + void W3DDisplay::takeScreenShotCompressed(void) { - // TheSuperHackers @bobtista 02/11/2025 Find next available filename char leafname[256]; char pathname[1024]; static int frame_number = 1; @@ -3041,7 +3059,6 @@ void W3DDisplay::takeScreenShotCompressed(void) done = true; } - // TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); @@ -3069,13 +3086,8 @@ void W3DDisplay::takeScreenShotCompressed(void) unsigned int width = surfaceDesc.Width; unsigned int height = surfaceDesc.Height; - // TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data - // Using shared_ptr for automatic cleanup in the background thread - std::shared_ptr imageData(new unsigned char[3 * width * height], - std::default_delete()); - unsigned char* image = imageData.get(); + unsigned char* image = new unsigned char[3 * width * height]; - // TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB for (y = 0; y < height; y++) { for (x = 0; x < width; x++) @@ -3083,9 +3095,9 @@ void W3DDisplay::takeScreenShotCompressed(void) index = 3 * (x + y * width); index2 = y * lrect.Pitch + 4 * x; - image[index] = *((unsigned char*)lrect.pBits + index2 + 2); // R - image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G - image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B + 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); } } @@ -3093,29 +3105,21 @@ void W3DDisplay::takeScreenShotCompressed(void) surfaceCopy->Release_Ref(); surfaceCopy = NULL; - // TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread - std::string pathnameCopy(pathname); - std::string leafnameCopy(leafname); - - // TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image - // This allows the game to continue running without freezing - std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() { - // TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100) - // stbi_write_jpg expects image data with Y-axis going down, which matches our data - int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90); - - if (!result) { - // TheSuperHackers @bobtista 02/11/2025 Log error if write failed - // Note: Can't show UI message from background thread - OutputDebugStringA("Failed to write screenshot JPEG\n"); - } - - // TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope - }).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently - - // TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background) + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + strcpy(threadData->pathname, pathname); + strcpy(threadData->leafname, leafname); + + DWORD threadId; + HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + } + UnicodeString ufileName; - ufileName.translate(leafnameCopy.c_str()); + ufileName.translate(leafname); TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index 24086a3e220..1b261394f8a 100644 --- a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -102,6 +102,7 @@ class GUIEditDisplay : public Display virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) { } virtual void takeScreenShot(void){ } + virtual void takeScreenShotCompressed(void){ } virtual void toggleMovieCapture(void) {} // methods that we need to stub diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 05e5821fcb0..7ce92178612 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -39,10 +39,7 @@ static void drawFramerateBar(void); #include #include #include -#include -#include -// TheSuperHackers @bobtista 02/11/2025 STB for image encoding #define STB_IMAGE_WRITE_IMPLEMENTATION #include @@ -3134,12 +3131,33 @@ void W3DDisplay::takeScreenShot(void) TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } -/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game -/// This implementation captures the frame buffer on the main thread, then spawns a background thread -/// to compress and save the image, allowing the game to continue running smoothly. +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[1024]; + char leafname[256]; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, 90); + + if (!result) { + OutputDebugStringA("Failed to write screenshot JPEG\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + void W3DDisplay::takeScreenShotCompressed(void) { - // TheSuperHackers @bobtista 02/11/2025 Find next available filename char leafname[256]; char pathname[1024]; static int frame_number = 1; @@ -3153,7 +3171,6 @@ void W3DDisplay::takeScreenShotCompressed(void) done = true; } - // TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); @@ -3181,13 +3198,8 @@ void W3DDisplay::takeScreenShotCompressed(void) unsigned int width = surfaceDesc.Width; unsigned int height = surfaceDesc.Height; - // TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data - // Using shared_ptr for automatic cleanup in the background thread - std::shared_ptr imageData(new unsigned char[3 * width * height], - std::default_delete()); - unsigned char* image = imageData.get(); + unsigned char* image = new unsigned char[3 * width * height]; - // TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB for (y = 0; y < height; y++) { for (x = 0; x < width; x++) @@ -3195,9 +3207,9 @@ void W3DDisplay::takeScreenShotCompressed(void) index = 3 * (x + y * width); index2 = y * lrect.Pitch + 4 * x; - image[index] = *((unsigned char*)lrect.pBits + index2 + 2); // R - image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G - image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B + 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); } } @@ -3205,29 +3217,21 @@ void W3DDisplay::takeScreenShotCompressed(void) surfaceCopy->Release_Ref(); surfaceCopy = NULL; - // TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread - std::string pathnameCopy(pathname); - std::string leafnameCopy(leafname); - - // TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image - // This allows the game to continue running without freezing - std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() { - // TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100) - // stbi_write_jpg expects image data with Y-axis going down, which matches our data - int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90); - - if (!result) { - // TheSuperHackers @bobtista 02/11/2025 Log error if write failed - // Note: Can't show UI message from background thread - OutputDebugStringA("Failed to write screenshot JPEG\n"); - } - - // TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope - }).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently - - // TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background) + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + strcpy(threadData->pathname, pathname); + strcpy(threadData->leafname, leafname); + + DWORD threadId; + HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + } + UnicodeString ufileName; - ufileName.translate(leafnameCopy.c_str()); + ufileName.translate(leafname); TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index 19f2f4803eb..b3e3afb0227 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -102,6 +102,7 @@ class GUIEditDisplay : public Display virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) { } virtual void takeScreenShot(void){ } + virtual void takeScreenShotCompressed(void){ } virtual void toggleMovieCapture(void) {} // methods that we need to stub From c346f1b6da86b71bfb6e66bf4ec786f2b6756ae5 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:04:47 -0500 Subject: [PATCH 05/24] Move screenshot logic to Core to eliminate code duplication Add shared screenshot implementation in Core --- Core/GameEngineDevice/CMakeLists.txt | 3 + .../W3DDevice/GameClient/W3DScreenshot.h | 28 ++++ .../W3DDevice/GameClient/W3DScreenshot.cpp | 152 ++++++++++++++++++ .../W3DDevice/GameClient/W3DDisplay.cpp | 105 +----------- .../W3DDevice/GameClient/W3DDisplay.cpp | 105 +----------- 5 files changed, 187 insertions(+), 206 deletions(-) create mode 100644 Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h create mode 100644 Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 44b25e6cd4b..ecd1211b3cd 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -72,6 +72,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 @@ -173,6 +174,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/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp Source/W3DDevice/GameClient/Water/W3DWater.cpp @@ -218,6 +220,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..234560af0fe --- /dev/null +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -0,0 +1,28 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** 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 + +enum ScreenshotFormat +{ + SCREENSHOT_JPEG, + SCREENSHOT_PNG +}; + +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 80); + diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp new file mode 100644 index 00000000000..08a37c9b9d6 --- /dev/null +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -0,0 +1,152 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** 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 +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +#include "W3DDevice/GameClient/W3DScreenshot.h" +#include "Common/GlobalData.h" +#include "GameClient/InGameUI.h" +#include "GameClient/GameText.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/surface.h" + +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[1024]; + char leafname[256]; + int quality; + ScreenshotFormat format; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = 0; + if (data->format == SCREENSHOT_JPEG) + { + result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); + } + else if (data->format == SCREENSHOT_PNG) + { + result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + } + + if (!result) { + OutputDebugStringA("Failed to write screenshot\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) +{ + char leafname[256]; + char pathname[1024]; + 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); + strcpy(pathname, TheGlobalData->getPath_UserData().str()); + 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(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + + surface->Release_Ref(); + surface = NULL; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == NULL) + { + 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 = NULL; + + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + threadData->quality = quality; + threadData->format = format; + strcpy(threadData->pathname, pathname); + strcpy(threadData->leafname, leafname); + + DWORD threadId; + HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + } + + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); +} + diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 9bf32f20fd9..6a2779425db 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -40,10 +40,8 @@ static void drawFramerateBar(void); #include #include -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include - // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" @@ -3019,108 +3017,9 @@ void W3DDisplay::takeScreenShot(void) TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } -struct ScreenshotThreadData -{ - unsigned char* imageData; - unsigned int width; - unsigned int height; - char pathname[1024]; - char leafname[256]; -}; - -static DWORD WINAPI screenshotThreadFunc(LPVOID param) -{ - ScreenshotThreadData* data = (ScreenshotThreadData*)param; - - int result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, 90); - - if (!result) { - OutputDebugStringA("Failed to write screenshot JPEG\n"); - } - - delete [] data->imageData; - delete data; - - return 0; -} - void W3DDisplay::takeScreenShotCompressed(void) { - char leafname[256]; - char pathname[1024]; - static int frame_number = 1; - - Bool done = false; - while (!done) { - sprintf(leafname, "sshot%.3d.jpg", frame_number++); - strcpy(pathname, TheGlobalData->getPath_UserData().str()); - 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(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); - - surface->Release_Ref(); - surface = NULL; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == NULL) - { - 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 = NULL; - - ScreenshotThreadData* threadData = new ScreenshotThreadData(); - threadData->imageData = image; - threadData->width = width; - threadData->height = height; - strcpy(threadData->pathname, pathname); - strcpy(threadData->leafname, leafname); - - DWORD threadId; - HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); - if (hThread) { - CloseHandle(hThread); - } - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); + W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); } /** Start/Stop capturing an AVI movie*/ diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 7ce92178612..fe2c1a3cc21 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -40,10 +40,8 @@ static void drawFramerateBar(void); #include #include -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include - // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" @@ -3131,108 +3129,9 @@ void W3DDisplay::takeScreenShot(void) TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } -struct ScreenshotThreadData -{ - unsigned char* imageData; - unsigned int width; - unsigned int height; - char pathname[1024]; - char leafname[256]; -}; - -static DWORD WINAPI screenshotThreadFunc(LPVOID param) -{ - ScreenshotThreadData* data = (ScreenshotThreadData*)param; - - int result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, 90); - - if (!result) { - OutputDebugStringA("Failed to write screenshot JPEG\n"); - } - - delete [] data->imageData; - delete data; - - return 0; -} - void W3DDisplay::takeScreenShotCompressed(void) { - char leafname[256]; - char pathname[1024]; - static int frame_number = 1; - - Bool done = false; - while (!done) { - sprintf(leafname, "sshot%.3d.jpg", frame_number++); - strcpy(pathname, TheGlobalData->getPath_UserData().str()); - 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(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); - - surface->Release_Ref(); - surface = NULL; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == NULL) - { - 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 = NULL; - - ScreenshotThreadData* threadData = new ScreenshotThreadData(); - threadData->imageData = image; - threadData->width = width; - threadData->height = height; - strcpy(threadData->pathname, pathname); - strcpy(threadData->leafname, leafname); - - DWORD threadId; - HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); - if (hThread) { - CloseHandle(hThread); - } - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); + W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); } /** Start/Stop capturing an AVI movie*/ From aa9dd09bda0064b4376c5706a9fed00eb2888983 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:12:09 -0500 Subject: [PATCH 06/24] Change F12 to JPEG and add CTRL+F12 for PNG screenshots --- Generals/Code/GameEngine/Include/Common/MessageStream.h | 4 ++-- Generals/Code/GameEngine/Include/GameClient/Display.h | 4 ++-- Generals/Code/GameEngine/Source/Common/MessageStream.cpp | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 6 +++--- .../Source/GameClient/MessageStream/MetaEvent.cpp | 8 ++++---- .../Include/W3DDevice/GameClient/W3DDisplay.h | 4 ++-- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 5 +++++ Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h | 2 +- GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h | 4 ++-- GeneralsMD/Code/GameEngine/Include/GameClient/Display.h | 4 ++-- .../Code/GameEngine/Source/Common/MessageStream.cpp | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 6 +++--- .../Source/GameClient/MessageStream/MetaEvent.cpp | 8 ++++---- .../Include/W3DDevice/GameClient/W3DDisplay.h | 4 ++-- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 5 +++++ GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h | 2 +- 16 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 8d69f786df2..f9e91183a96 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -257,8 +257,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_COMPRESSED, ///< take compressed screenshot without stalling + MSG_META_TAKE_SCREENSHOT, ///< take screenshot (JPEG) + MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index 1fad658b881..7357bf0b0e1 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -166,8 +166,8 @@ 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(void) = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed(void) = 0; ///< saves compressed screenshot without stalling + virtual void takeScreenShotCompressed(void) = 0; ///< saves JPEG screenshot + virtual void takeScreenShotPNG(void) = 0; ///< saves PNG screenshot virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp index 1b05257d904..d8a31509aaa 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,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_COMPRESSED) + 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/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index aa6bf092610..9ce1e385560 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3418,14 +3418,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage case GameMessage::MSG_META_TAKE_SCREENSHOT: { if (TheDisplay) - TheDisplay->takeScreenShot(); + TheDisplay->takeScreenShotCompressed(); break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: { if (TheDisplay) - TheDisplay->takeScreenShotCompressed(); + TheDisplay->takeScreenShotPNG(); disp = DESTROY_MESSAGE; break; } diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index c9102fd1f5a..e700febc5df 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -163,7 +163,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_COMPRESSED", GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED }, + { "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 }, @@ -815,12 +815,12 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); if (map->m_key == MK_NONE) { - map->m_key = MK_F11; + map->m_key = MK_F12; map->m_transition = DOWN; - map->m_modState = NONE; + map->m_modState = CTRL; map->m_usableIn = COMMANDUSABLE_EVERYWHERE; } } diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 4afd44ab8f3..e1c4636ff0a 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -120,8 +120,8 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer( void ) ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot(void); //save screenshot to file - virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(void); //save JPEG screenshot + virtual void takeScreenShotPNG(void); //save PNG screenshot virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); ///takeScreenShot(); + TheDisplay->takeScreenShotCompressed(); break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: { if (TheDisplay) - TheDisplay->takeScreenShotCompressed(); + TheDisplay->takeScreenShotPNG(); disp = DESTROY_MESSAGE; break; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 92d405b2506..2a62398be19 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -171,7 +171,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_COMPRESSED", GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED }, + { "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 }, @@ -873,12 +873,12 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); if (map->m_key == MK_NONE) { - map->m_key = MK_F11; + map->m_key = MK_F12; map->m_transition = DOWN; - map->m_modState = NONE; + map->m_modState = CTRL; map->m_usableIn = COMMANDUSABLE_EVERYWHERE; } } diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 2a173a4bf8d..7f8f682b649 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -120,8 +120,8 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer( void ) ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot(void); //save screenshot to file - virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(void); //save JPEG screenshot + virtual void takeScreenShotPNG(void); //save PNG screenshot virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); /// Date: Mon, 3 Nov 2025 11:13:05 -0500 Subject: [PATCH 07/24] Remove old BMP screenshot code --- .../W3DDevice/GameClient/W3DDisplay.cpp | 138 ------------------ .../W3DDevice/GameClient/W3DDisplay.cpp | 138 ------------------ 2 files changed, 276 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index b6be31a245b..c69f662833f 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2879,144 +2879,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) LocalFree( (HLOCAL) pbmi); } -///Save Screen Capture to a file -void W3DDisplay::takeScreenShot(void) -{ - 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()); -} - void W3DDisplay::takeScreenShotCompressed(void) { W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 5a8e35ccc4e..b474c1a2c74 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2991,144 +2991,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) LocalFree( (HLOCAL) pbmi); } -///Save Screen Capture to a file -void W3DDisplay::takeScreenShot(void) -{ - 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()); -} - void W3DDisplay::takeScreenShotCompressed(void) { W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); From 0eb7821a30d9210a1f867e51fa66e6af9ebb71e7 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:15:41 -0500 Subject: [PATCH 08/24] Add JPEGQuality option to Options.ini (default 80) Move W3DScreenshot implementation to game-specific directories Fix include order for VC6 precompiled headers Remove default parameter from function definition Move STB implementation to separate file to avoid PCH issues Include screenshot implementation directly in W3DDisplay.cpp to avoid PCH issues Use Windows constants and switch statement in screenshot code Use vcpkg for stb dependency with FetchContent fallback --- .../Include/Common/UserPreferences.h | 1 + .../W3DDevice/GameClient/W3DScreenshot.h | 6 +- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/MessageStream.h | 4 +- .../GameEngine/Include/GameClient/Display.h | 9 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/Common/MessageStream.cpp | 2 +- .../GUI/GUICallbacks/Menus/OptionsMenu.cpp | 12 ++ .../GameClient/MessageStream/CommandXlat.cpp | 6 +- .../GameClient/MessageStream/MetaEvent.cpp | 14 +- Generals/Code/GameEngineDevice/CMakeLists.txt | 10 ++ .../Include/W3DDevice/GameClient/W3DDisplay.h | 3 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 11 +- .../W3DDevice/GameClient/W3DScreenshot.cpp | 58 +++----- .../GameClient/stb_image_write_impl.cpp | 21 +++ .../Tools/GUIEdit/Include/GUIEditDisplay.h | 3 +- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/MessageStream.h | 4 +- .../GameEngine/Include/GameClient/Display.h | 9 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/Common/MessageStream.cpp | 2 +- .../GUI/GUICallbacks/Menus/OptionsMenu.cpp | 12 ++ .../GameClient/MessageStream/CommandXlat.cpp | 6 +- .../GameClient/MessageStream/MetaEvent.cpp | 14 +- .../Code/GameEngineDevice/CMakeLists.txt | 10 ++ .../Include/W3DDevice/GameClient/W3DDisplay.h | 3 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 11 +- .../W3DDevice/GameClient/W3DScreenshot.cpp | 130 ++++++++++++++++++ .../GameClient/stb_image_write_impl.cpp | 21 +++ .../Tools/GUIEdit/Include/GUIEditDisplay.h | 3 +- cmake/stb.cmake | 22 +-- vcpkg.json | 3 +- 32 files changed, 312 insertions(+), 102 deletions(-) rename {Core => Generals/Code}/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp (66%) create mode 100644 Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp create mode 100644 GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp create mode 100644 GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp diff --git a/Core/GameEngine/Include/Common/UserPreferences.h b/Core/GameEngine/Include/Common/UserPreferences.h index e3ec9b450ec..f1f64a7d2a2 100644 --- a/Core/GameEngine/Include/Common/UserPreferences.h +++ b/Core/GameEngine/Include/Common/UserPreferences.h @@ -93,6 +93,7 @@ class OptionPreferences : public UserPreferences Bool getAlternateMouseModeEnabled(void); // convenience function Bool getRetaliationModeEnabled(); // convenience function Bool getDoubleClickAttackMoveEnabled(void); // convenience function + Int getJPEGQuality(void); // convenience function Real getScrollFactor(void); // convenience function Bool getDrawScrollAnchor(void); Bool getMoveScrollAnchor(void); diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index 234560af0fe..fe43aa8032c 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -18,11 +18,7 @@ #pragma once -enum ScreenshotFormat -{ - SCREENSHOT_JPEG, - SCREENSHOT_PNG -}; +#include "GameClient/Display.h" void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 80); diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 401c19cd1f1..8972cce3189 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -142,6 +142,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 f9e91183a96..2796efad2e4 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -257,8 +257,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 (JPEG) - MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot + MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) + MSG_META_TAKE_SCREENSHOT_JPEG, ///< 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/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index 7357bf0b0e1..78d9ea2caa9 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/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. @@ -166,8 +172,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 takeScreenShotCompressed(void) = 0; ///< saves JPEG screenshot - virtual void takeScreenShotPNG(void) = 0; ///< saves PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index 3c8894ac5d9..04f903dc3ef 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1189,6 +1189,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useAlternateMouse = optionPref.getAlternateMouseModeEnabled(); 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 d8a31509aaa..0b08bc592b6 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,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_TAKE_SCREENSHOT_JPEG) 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/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp index 6222ac9232b..548cb0fd5c1 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -354,6 +354,18 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled(void) return FALSE; } +Int OptionPreferences::getJPEGQuality(void) +{ + OptionPreferences::const_iterator it = find("JPEGQuality"); + if (it == end()) + return 80; + + Int quality = atoi(it->second.str()); + if (quality < 1) quality = 1; + if (quality > 100) quality = 100; + return quality; +} + Real OptionPreferences::getScrollFactor(void) { OptionPreferences::const_iterator it = find("ScrollFactor"); diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 9ce1e385560..38f443e4f85 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3418,14 +3418,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage case GameMessage::MSG_META_TAKE_SCREENSHOT: { if (TheDisplay) - TheDisplay->takeScreenShotCompressed(); + TheDisplay->takeScreenShot(SCREENSHOT_JPEG); break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: + case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG: { if (TheDisplay) - TheDisplay->takeScreenShotPNG(); + TheDisplay->takeScreenShot(SCREENSHOT_PNG); disp = DESTROY_MESSAGE; break; } diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index e700febc5df..4fdf92e728a 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -163,7 +163,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 }, + { "TAKE_SCREENSHOT_JPEG", GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -815,7 +815,17 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); + 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_JPEG); if (map->m_key == MK_NONE) { map->m_key = MK_F12; diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 7674beadb07..736ebba0ced 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -203,6 +203,16 @@ target_link_libraries(g_gameenginedevice PRIVATE stb ) +target_sources(g_gameenginedevice PRIVATE + Source/W3DDevice/GameClient/stb_image_write_impl.cpp +) + +set_source_files_properties( + Source/W3DDevice/GameClient/stb_image_write_impl.cpp + PROPERTIES + SKIP_PRECOMPILE_HEADERS ON +) + target_link_libraries(g_gameenginedevice PUBLIC corei_gameenginedevice_public g_gameengine diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index e1c4636ff0a..01c341ab19c 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -120,8 +120,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer( void ) ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShotCompressed(void); //save JPEG screenshot - virtual void takeScreenShotPNG(void); //save PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format); //save screenshot in specified format virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); ///. -*/ - -#include -#include -#include - -#define STB_IMAGE_WRITE_IMPLEMENTATION #include -#include "W3DDevice/GameClient/W3DScreenshot.h" -#include "Common/GlobalData.h" -#include "GameClient/InGameUI.h" -#include "GameClient/GameText.h" -#include "WW3D2/dx8wrapper.h" -#include "WW3D2/surface.h" - struct ScreenshotThreadData { unsigned char* imageData; unsigned int width; unsigned int height; - char pathname[1024]; - char leafname[256]; + char pathname[_MAX_PATH]; + char leafname[_MAX_FNAME]; int quality; ScreenshotFormat format; }; @@ -46,13 +16,14 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) ScreenshotThreadData* data = (ScreenshotThreadData*)param; int result = 0; - if (data->format == SCREENSHOT_JPEG) - { - result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); - } - else if (data->format == SCREENSHOT_PNG) + switch (data->format) { - result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + 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) { @@ -67,8 +38,8 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) { - char leafname[256]; - char pathname[1024]; + char leafname[_MAX_FNAME]; + char pathname[_MAX_PATH]; static int jpegFrameNumber = 1; static int pngFrameNumber = 1; @@ -130,6 +101,9 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy->Release_Ref(); surfaceCopy = NULL; + if (quality <= 0 && format == SCREENSHOT_JPEG) + quality = TheGlobalData->m_jpegQuality; + ScreenshotThreadData* threadData = new ScreenshotThreadData(); threadData->imageData = image; threadData->width = width; @@ -150,3 +124,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } +void W3DDisplay::takeScreenShot(ScreenshotFormat format) +{ + W3D_TakeCompressedScreenshot(format); +} diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp new file mode 100644 index 00000000000..364368901a5 --- /dev/null +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -0,0 +1,21 @@ +/* +** Command & Conquer Generals(tm) +** Copyright 2025 Electronic Arts Inc. +** +** 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/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index 962305e190d..a581174d563 100644 --- a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -101,8 +101,7 @@ class GUIEditDisplay : public Display virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) { } virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) { } - virtual void takeScreenShotCompressed(void){ } - virtual void takeScreenShotPNG(void){ } + virtual void takeScreenShot(ScreenshotFormat){ } virtual void toggleMovieCapture(void) {} // 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 fb1f737e5de..103b38f4cf9 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/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/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 27100eac3cf..794f82713d6 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -257,8 +257,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 (JPEG) - MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot + MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) + MSG_META_TAKE_SCREENSHOT_JPEG, ///< 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/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index 7b6eecb6e0f..de6a0c3bd7e 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/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. @@ -166,8 +172,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 takeScreenShotCompressed(void) = 0; ///< saves JPEG screenshot - virtual void takeScreenShotPNG(void) = 0; ///< saves PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 033768b2f41..cce31a5a041 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1209,6 +1209,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useAlternateMouse = optionPref.getAlternateMouseModeEnabled(); 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 f83c751281d..ed1d1271154 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,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_TAKE_SCREENSHOT_JPEG) 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/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp index ea926bd987d..d1ebd72c75f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -363,6 +363,18 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled(void) return FALSE; } +Int OptionPreferences::getJPEGQuality(void) +{ + OptionPreferences::const_iterator it = find("JPEGQuality"); + if (it == end()) + return 80; + + Int quality = atoi(it->second.str()); + if (quality < 1) quality = 1; + if (quality > 100) quality = 100; + return quality; +} + Real OptionPreferences::getScrollFactor(void) { OptionPreferences::const_iterator it = find("ScrollFactor"); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index a14f00c5bb6..72a89798606 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3751,14 +3751,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage case GameMessage::MSG_META_TAKE_SCREENSHOT: { if (TheDisplay) - TheDisplay->takeScreenShotCompressed(); + TheDisplay->takeScreenShot(SCREENSHOT_JPEG); break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: + case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG: { if (TheDisplay) - TheDisplay->takeScreenShotPNG(); + TheDisplay->takeScreenShot(SCREENSHOT_PNG); disp = DESTROY_MESSAGE; break; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 2a62398be19..b8482ea78a0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -171,7 +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 }, + { "TAKE_SCREENSHOT_JPEG", GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -873,7 +873,17 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); + 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_JPEG); if (map->m_key == MK_NONE) { map->m_key = MK_F12; diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index ad2470f24f4..6498e0f5572 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -216,6 +216,16 @@ target_link_libraries(z_gameenginedevice PRIVATE stb ) +target_sources(z_gameenginedevice PRIVATE + Source/W3DDevice/GameClient/stb_image_write_impl.cpp +) + +set_source_files_properties( + Source/W3DDevice/GameClient/stb_image_write_impl.cpp + PROPERTIES + SKIP_PRECOMPILE_HEADERS ON +) + target_link_libraries(z_gameenginedevice PUBLIC corei_gameenginedevice_public z_gameengine diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 7f8f682b649..2b09aa06bcc 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -120,8 +120,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer( void ) ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShotCompressed(void); //save JPEG screenshot - virtual void takeScreenShotPNG(void); //save PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format); //save screenshot in specified format virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); /// + +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); + strcpy(pathname, TheGlobalData->getPath_UserData().str()); + 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(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + + surface->Release_Ref(); + surface = NULL; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == NULL) + { + 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 = NULL; + + 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; + strcpy(threadData->pathname, pathname); + strcpy(threadData->leafname, leafname); + + DWORD threadId; + HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + } + + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); +} + +void W3DDisplay::takeScreenShot(ScreenshotFormat format) +{ + W3D_TakeCompressedScreenshot(format); +} diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp new file mode 100644 index 00000000000..364368901a5 --- /dev/null +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -0,0 +1,21 @@ +/* +** Command & Conquer Generals(tm) +** Copyright 2025 Electronic Arts Inc. +** +** 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/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index a90791bb233..94cdca5c8a8 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -101,8 +101,7 @@ class GUIEditDisplay : public Display virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) { } virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) { } - virtual void takeScreenShotCompressed(void){ } - virtual void takeScreenShotPNG(void){ } + virtual void takeScreenShot(ScreenshotFormat){ } virtual void toggleMovieCapture(void) {} // methods that we need to stub diff --git a/cmake/stb.cmake b/cmake/stb.cmake index 957c7d235d0..8f2078a8106 100644 --- a/cmake/stb.cmake +++ b/cmake/stb.cmake @@ -2,16 +2,18 @@ # STB single-file public domain libraries for image encoding # https://github.com/nothings/stb -FetchContent_Declare( - stb - GIT_REPOSITORY https://github.com/nothings/stb.git - GIT_TAG master # Could pin to specific commit for stability - GIT_SHALLOW TRUE -) +find_package(Stb CONFIG QUIET) -FetchContent_MakeAvailable(stb) +if(NOT Stb_FOUND) + include(FetchContent) + FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG 5c205738c191bcb0abc65c4febfa9bd25ff35234 + ) -# Create interface library for stb headers -add_library(stb INTERFACE) -target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) + FetchContent_MakeAvailable(stb) + add_library(stb INTERFACE) + target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) +endif() 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 1c45436a2d2803c1398e9c9801248538c9a4d365 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 13:10:09 -0500 Subject: [PATCH 09/24] Remove trailing whitespace from empty lines --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 10 +++++----- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 9a601d12327..a091022dd43 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -14,7 +14,7 @@ struct ScreenshotThreadData static DWORD WINAPI screenshotThreadFunc(LPVOID param) { ScreenshotThreadData* data = (ScreenshotThreadData*)param; - + int result = 0; switch (data->format) { @@ -25,14 +25,14 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) 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; } @@ -42,7 +42,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) 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"; diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 9a601d12327..a091022dd43 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -14,7 +14,7 @@ struct ScreenshotThreadData static DWORD WINAPI screenshotThreadFunc(LPVOID param) { ScreenshotThreadData* data = (ScreenshotThreadData*)param; - + int result = 0; switch (data->format) { @@ -25,14 +25,14 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) 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; } @@ -42,7 +42,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) 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"; From 12858a99f25b4fd30e1cb49ff1231a9292150f42 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:19 -0500 Subject: [PATCH 10/24] Fix copyright headers: Change Electronic Arts Inc. to TheSuperHackers --- .../Include/W3DDevice/GameClient/W3DScreenshot.h | 2 +- .../Source/W3DDevice/GameClient/stb_image_write_impl.cpp | 2 +- .../Source/W3DDevice/GameClient/stb_image_write_impl.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index fe43aa8032c..892952645d7 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 Electronic Arts Inc. +** 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 diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp index 364368901a5..2264ba2c403 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. +** 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 diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp index 364368901a5..2264ba2c403 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. +** 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 From 7ac1a95beea22061b784712c4b731696406ca537 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:22 -0500 Subject: [PATCH 11/24] Add file headers to W3DScreenshot.cpp files --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 18 ++++++++++++++++++ .../W3DDevice/GameClient/W3DScreenshot.cpp | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index a091022dd43..e5554cb26e2 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -1,3 +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 . +*/ + #include struct ScreenshotThreadData diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index a091022dd43..e6a5ab2fe4e 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -1,3 +1,21 @@ +/* +** 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 struct ScreenshotThreadData From 98ae29f7c31a32454f838d30f6f349fdab812dc1 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:27 -0500 Subject: [PATCH 12/24] Rename MSG_META_TAKE_SCREENSHOT_JPEG to MSG_META_TAKE_SCREENSHOT_PNG --- Generals/Code/GameEngine/Include/Common/MessageStream.h | 2 +- Generals/Code/GameEngine/Source/Common/MessageStream.cpp | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 2 +- .../GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp | 4 ++-- GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h | 2 +- GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 2 +- .../GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 2796efad2e4..e017e7b5950 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -258,7 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) - MSG_META_TAKE_SCREENSHOT_JPEG, ///< take PNG screenshot (CTRL+F12, lossless) + 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/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp index 0b08bc592b6..d8a31509aaa 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,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_JPEG) + 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/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 38f443e4f85..08dbab13a8d 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3422,7 +3422,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG: + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: { if (TheDisplay) TheDisplay->takeScreenShot(SCREENSHOT_PNG); diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 4fdf92e728a..051983e6513 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -163,7 +163,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_JPEG", GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG }, + { "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 }, @@ -825,7 +825,7 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); if (map->m_key == MK_NONE) { map->m_key = MK_F12; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 794f82713d6..49df459c8d8 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -258,7 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) - MSG_META_TAKE_SCREENSHOT_JPEG, ///< take PNG screenshot (CTRL+F12, lossless) + 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/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index ed1d1271154..f83c751281d 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,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_JPEG) + 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/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 72a89798606..68bb74df84b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3755,7 +3755,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG: + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: { if (TheDisplay) TheDisplay->takeScreenShot(SCREENSHOT_PNG); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index b8482ea78a0..c84e93f177f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -171,7 +171,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_JPEG", GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG }, + { "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 }, @@ -883,7 +883,7 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); if (map->m_key == MK_NONE) { map->m_key = MK_F12; From 13108c2ae71cd27bedcbd50b2a8bb73f67728876 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 11:51:30 -0500 Subject: [PATCH 13/24] fix(screenshot): Fix W3DScreenshot.cpp references in CMakeLists after rebase --- Core/GameEngineDevice/CMakeLists.txt | 2 +- Generals/Code/GameEngineDevice/CMakeLists.txt | 1 + GeneralsMD/Code/GameEngineDevice/CMakeLists.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index ecd1211b3cd..f6b1e923192 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -174,7 +174,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/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp Source/W3DDevice/GameClient/Water/W3DWater.cpp diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 736ebba0ced..9d81f2f69f6 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,6 +204,7 @@ target_link_libraries(g_gameenginedevice PRIVATE ) target_sources(g_gameenginedevice PRIVATE + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 6498e0f5572..bffcf6b450e 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,6 +217,7 @@ target_link_libraries(z_gameenginedevice PRIVATE ) target_sources(z_gameenginedevice PRIVATE + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) From 1f46d55f173fe3cec262455cedeaea8bc326796c Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 11:57:18 -0500 Subject: [PATCH 14/24] fix(screenshot): Add missing W3DScreenshot.h include for ScreenshotFormat enum --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 1 + .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index e5554cb26e2..dffda88bae7 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,6 +16,7 @@ ** along with this program. If not, see . */ +#include "W3DScreenshot.h" #include struct ScreenshotThreadData diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index e6a5ab2fe4e..4ad41ebbba8 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,6 +16,7 @@ ** along with this program. If not, see . */ +#include "W3DScreenshot.h" #include struct ScreenshotThreadData From 8eb00366a20be55f52a4e47ec146e87771a1df61 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:01:13 -0500 Subject: [PATCH 15/24] fix(screenshot): Use full include path for W3DScreenshot.h from Core --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 +- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index dffda88bae7..c3786386497 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,7 +16,7 @@ ** along with this program. If not, see . */ -#include "W3DScreenshot.h" +#include "W3DDevice/GameClient/W3DScreenshot.h" #include struct ScreenshotThreadData diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 4ad41ebbba8..b3fe609c467 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,7 +16,7 @@ ** along with this program. If not, see . */ -#include "W3DScreenshot.h" +#include "W3DDevice/GameClient/W3DScreenshot.h" #include struct ScreenshotThreadData From f0e1a0361a2b193ece787199914bf99acf43ab79 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:06:23 -0500 Subject: [PATCH 16/24] fix(screenshot): Remove W3DScreenshot.cpp from separate compilation since it's included in W3DDisplay.cpp --- Generals/Code/GameEngineDevice/CMakeLists.txt | 1 - GeneralsMD/Code/GameEngineDevice/CMakeLists.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 9d81f2f69f6..736ebba0ced 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,7 +204,6 @@ target_link_libraries(g_gameenginedevice PRIVATE ) target_sources(g_gameenginedevice PRIVATE - Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index bffcf6b450e..6498e0f5572 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,7 +217,6 @@ target_link_libraries(z_gameenginedevice PRIVATE ) target_sources(z_gameenginedevice PRIVATE - Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) From 14ca7a075718690b2dbddbd0bf1a683998bb2336 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:40:54 -0500 Subject: [PATCH 17/24] refactor(screenshot): Consolidate stb_image_write_impl.cpp to Core Move stb_image_write_impl.cpp from game-specific directories to Core since it's identical between Generals and GeneralsMD and contains only STB library implementation code with no game-specific logic. --- Core/GameEngineDevice/CMakeLists.txt | 1 + .../GameClient/stb_image_write_impl.cpp | 0 Generals/Code/GameEngineDevice/CMakeLists.txt | 4 ++-- .../Code/GameEngineDevice/CMakeLists.txt | 4 ++-- .../GameClient/stb_image_write_impl.cpp | 21 ------------------- 5 files changed, 5 insertions(+), 25 deletions(-) rename {Generals/Code => Core}/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp (100%) delete mode 100644 GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index f6b1e923192..f82409d2e2b 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -175,6 +175,7 @@ set(GAMEENGINEDEVICE_SRC 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 diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp similarity index 100% rename from Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp rename to Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 736ebba0ced..344f3d7cb65 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,11 +204,11 @@ target_link_libraries(g_gameenginedevice PRIVATE ) target_sources(g_gameenginedevice PRIVATE - Source/W3DDevice/GameClient/stb_image_write_impl.cpp +# Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) set_source_files_properties( - Source/W3DDevice/GameClient/stb_image_write_impl.cpp +# Source/W3DDevice/GameClient/stb_image_write_impl.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS ON ) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 6498e0f5572..105dde56407 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,11 +217,11 @@ target_link_libraries(z_gameenginedevice PRIVATE ) target_sources(z_gameenginedevice PRIVATE - Source/W3DDevice/GameClient/stb_image_write_impl.cpp +# Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) set_source_files_properties( - Source/W3DDevice/GameClient/stb_image_write_impl.cpp +# Source/W3DDevice/GameClient/stb_image_write_impl.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS ON ) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp deleted file mode 100644 index 2264ba2c403..00000000000 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp +++ /dev/null @@ -1,21 +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 . -*/ - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include - From df506b5017033ef323b11107964a3c3ebf0037b3 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:41:36 -0500 Subject: [PATCH 18/24] docs(unify): Document stb_image_write_impl.cpp unification in script --- scripts/cpp/unify_move_files.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py index 4d7f61f7d00..2b1cce8a4d2 100644 --- a/scripts/cpp/unify_move_files.py +++ b/scripts/cpp/unify_move_files.py @@ -256,6 +256,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") From dd65a74cc82f6a263e59558b8adb84526862cd5b Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 14:46:26 -0500 Subject: [PATCH 19/24] fix(screenshot): Restore DESTROY_MESSAGE, fix quality default, NULL to nullptr, strcpy to strlcpy, fix thread leak --- .../W3DDevice/GameClient/W3DScreenshot.h | 2 +- .../GameClient/MessageStream/CommandXlat.cpp | 1 + .../W3DDevice/GameClient/W3DScreenshot.cpp | 19 +++++++++++-------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index 892952645d7..01b04030838 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 = 80); +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 0); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 68bb74df84b..6134b58548b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3752,6 +3752,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { if (TheDisplay) TheDisplay->takeScreenShot(SCREENSHOT_JPEG); + disp = DESTROY_MESSAGE; break; } diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index b3fe609c467..9aaac56d3e1 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -68,7 +68,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) Bool done = false; while (!done) { sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); - strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); strlcat(pathname, leafname, ARRAY_SIZE(pathname)); if (_access(pathname, 0) == -1) done = true; @@ -79,10 +79,10 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) 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(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); - surface = NULL; + surface = nullptr; struct Rect { @@ -91,7 +91,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) } lrect; lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == NULL) + if (lrect.pBits == nullptr) { surfaceCopy->Release_Ref(); return; @@ -118,7 +118,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy->Unlock(); surfaceCopy->Release_Ref(); - surfaceCopy = NULL; + surfaceCopy = nullptr; if (quality <= 0 && format == SCREENSHOT_JPEG) quality = TheGlobalData->m_jpegQuality; @@ -129,13 +129,16 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) threadData->height = height; threadData->quality = quality; threadData->format = format; - strcpy(threadData->pathname, pathname); - strcpy(threadData->leafname, leafname); + strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); + strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); DWORD threadId; - HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); if (hThread) { CloseHandle(hThread); + } else { + delete [] threadData->imageData; + delete threadData; } UnicodeString ufileName; From 516acb281dbf50aa62023d2084c122de10e2a19b Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 14:46:35 -0500 Subject: [PATCH 20/24] fix(screenshot): Replicate screenshot fixes to Generals --- .../GameClient/MessageStream/CommandXlat.cpp | 1 + .../W3DDevice/GameClient/W3DScreenshot.cpp | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 08dbab13a8d..09c4bf62f04 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3419,6 +3419,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { if (TheDisplay) TheDisplay->takeScreenShot(SCREENSHOT_JPEG); + disp = DESTROY_MESSAGE; break; } diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index c3786386497..715834345cb 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -68,7 +68,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) Bool done = false; while (!done) { sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); - strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); strlcat(pathname, leafname, ARRAY_SIZE(pathname)); if (_access(pathname, 0) == -1) done = true; @@ -79,10 +79,10 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) 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(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); - surface = NULL; + surface = nullptr; struct Rect { @@ -91,7 +91,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) } lrect; lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == NULL) + if (lrect.pBits == nullptr) { surfaceCopy->Release_Ref(); return; @@ -118,7 +118,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy->Unlock(); surfaceCopy->Release_Ref(); - surfaceCopy = NULL; + surfaceCopy = nullptr; if (quality <= 0 && format == SCREENSHOT_JPEG) quality = TheGlobalData->m_jpegQuality; @@ -129,13 +129,16 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) threadData->height = height; threadData->quality = quality; threadData->format = format; - strcpy(threadData->pathname, pathname); - strcpy(threadData->leafname, leafname); + strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); + strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); DWORD threadId; - HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); if (hThread) { CloseHandle(hThread); + } else { + delete [] threadData->imageData; + delete threadData; } UnicodeString ufileName; From 7e3c76557c9a7d437868479c8863ddd23a0d9bdf Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 15:04:19 -0500 Subject: [PATCH 21/24] refactor(screenshot): Compile W3DScreenshot.cpp as separate translation unit instead of #including it --- GeneralsMD/Code/GameEngineDevice/CMakeLists.txt | 1 + .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 1 - .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 6 ++++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 105dde56407..fb13ca9662a 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 diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index f6390fee432..a8a41e10ba8 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2992,7 +2992,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) } ///Save Screen Capture to a file -#include "W3DScreenshot.cpp" /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture(void) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 9aaac56d3e1..0c20405f843 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,7 +16,13 @@ ** 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 struct ScreenshotThreadData From 45c0339cb40c68a244ec901c6333b1f5474933a0 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 15:04:34 -0500 Subject: [PATCH 22/24] refactor(screenshot): Replicate separate compilation fix to Generals --- Generals/Code/GameEngineDevice/CMakeLists.txt | 1 + .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 1 - .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 8 ++++++++ .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 ++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 344f3d7cb65..32fc56c3b1d 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 diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index e21b8737741..d155f6f46c2 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2880,7 +2880,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) } ///Save Screen Capture to a file -#include "W3DScreenshot.cpp" /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture(void) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 715834345cb..0961fd6163d 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,7 +16,15 @@ ** 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 diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 0c20405f843..05d30bc67f1 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -23,6 +23,8 @@ #include "Common/GlobalData.h" #include "GameClient/GameText.h" #include "GameClient/InGameUI.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/surfaceclass.h" #include struct ScreenshotThreadData From a6f02b98710720194a7ff05aab137a1800136913 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 16:50:31 -0500 Subject: [PATCH 23/24] fix(screenshot): Initialize m_jpegQuality in constructor and remove empty CMake blocks --- .../Code/GameEngine/Source/Common/GlobalData.cpp | 1 + GeneralsMD/Code/GameEngineDevice/CMakeLists.txt | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index cce31a5a041..b5ea1287ee6 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -649,6 +649,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! diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index fb13ca9662a..9737b59f0c4 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,16 +217,6 @@ target_link_libraries(z_gameenginedevice PRIVATE stb ) -target_sources(z_gameenginedevice PRIVATE -# Source/W3DDevice/GameClient/stb_image_write_impl.cpp -) - -set_source_files_properties( -# Source/W3DDevice/GameClient/stb_image_write_impl.cpp - PROPERTIES - SKIP_PRECOMPILE_HEADERS ON -) - target_link_libraries(z_gameenginedevice PUBLIC corei_gameenginedevice_public z_gameengine From 65867f4d27b936518bd649374a018f963c241f2f Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 16:50:43 -0500 Subject: [PATCH 24/24] fix(screenshot): Replicate m_jpegQuality init and CMake cleanup to Generals --- Generals/Code/GameEngine/Source/Common/GlobalData.cpp | 1 + Generals/Code/GameEngineDevice/CMakeLists.txt | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index 04f903dc3ef..ad73a2a00ba 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -645,6 +645,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! diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 32fc56c3b1d..7262a69e7fc 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,16 +204,6 @@ target_link_libraries(g_gameenginedevice PRIVATE stb ) -target_sources(g_gameenginedevice PRIVATE -# Source/W3DDevice/GameClient/stb_image_write_impl.cpp -) - -set_source_files_properties( -# Source/W3DDevice/GameClient/stb_image_write_impl.cpp - PROPERTIES - SKIP_PRECOMPILE_HEADERS ON -) - target_link_libraries(g_gameenginedevice PUBLIC corei_gameenginedevice_public g_gameengine