From 86a2f84e747df5b52325ebb70bfbeabdd44f08d0 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:15 -0500 Subject: [PATCH 1/8] 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 4160c918c7..7256433de2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,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 0000000000..957c7d235d --- /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 014e5f732eb1b98fbb0e2dc88cf32021cba8c0a5 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:22 -0500 Subject: [PATCH 2/8] Add MSG_META_TAKE_SCREENSHOT_COMPRESSED message and F11 keybinding Add takeScreenShotCompressed() to Display interface --- .../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 | 13 +++++++++++++ .../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 | 13 +++++++++++++ 10 files changed, 48 insertions(+) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index a36aeed7ad..306129e895 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 4d12e246e3..9faee5e616 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -169,6 +169,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 0af149735e..10ad23125f 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 f71498c242..fa51bc9bbe 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3411,6 +3411,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 2b8b0c18a0..e25e8bcba1 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 }, @@ -793,6 +794,18 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) map->m_displayName = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SelectNextIdleWorker", L"Next Idle Worker"); } } + { + // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling + // Bind F11 to the new compressed screenshot function + 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 324f28cfdc..d400534904 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 3f3a783946..805b659bd9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -169,6 +169,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 1acee2d16f..6f7afc9cc7 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 633474ef7f..af999e7d8b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3744,6 +3744,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 2a7f4097fc..33b8a5af87 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 }, @@ -851,6 +852,18 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) map->m_displayName = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SelectNextIdleWorker", L"Next Idle Worker"); } } + { + // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling + // Bind F11 to the new compressed screenshot function + 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 829512fc5d8405328e336d48d334af12d98743fd Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:34 -0500 Subject: [PATCH 3/8] 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 - .../GameClient/MessageStream/MetaEvent.cpp | 2 - Generals/Code/GameEngineDevice/CMakeLists.txt | 3 +- .../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 - .../GameClient/MessageStream/MetaEvent.cpp | 2 - .../Code/GameEngineDevice/CMakeLists.txt | 1 + .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 103 ++++++++++++++++++ 14 files changed, 215 insertions(+), 11 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 306129e895..a2a96b15d2 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 9faee5e616..cd40b8be3a 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -169,7 +169,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 fa51bc9bbe..ce754bfc87 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3414,7 +3414,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/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index e25e8bcba1..2947042d97 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -795,8 +795,6 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling - // Bind F11 to the new compressed screenshot function MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); if (map->m_key == MK_NONE) { diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 63a78b15a4..13d3a4d934 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -200,7 +200,8 @@ target_precompile_headers(g_gameenginedevice PRIVATE target_link_libraries(g_gameenginedevice PRIVATE corei_gameenginedevice_private gi_always - gi_main + 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 018e27ef81..f1124b9698 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 d400534904..ddd07266ef 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 805b659bd9..306789a98c 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -169,7 +169,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 af999e7d8b..b1e7595d09 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3747,7 +3747,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/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 33b8a5af87..3257ee0e53 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -853,8 +853,6 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling - // Bind F11 to the new compressed screenshot function MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); if (map->m_key == MK_NONE) { diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 82c1bb4259..22b48e089f 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -214,6 +214,7 @@ target_link_libraries(z_gameenginedevice PRIVATE corei_gameenginedevice_private zi_always zi_main + stb ) target_link_libraries(z_gameenginedevice PUBLIC diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 7f5ad9f174..f5916e202a 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" @@ -3135,6 +3141,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 9206af3ef4246ab3bc52ba602ca56e3cac558770 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 23:29:47 -0500 Subject: [PATCH 4/8] 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 007d9824a1..80034af978 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 dda18bf3ae..051430ece2 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 7c236db1ce..b626349c7c 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 @@ -3141,12 +3138,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; @@ -3160,7 +3178,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); @@ -3188,13 +3205,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++) @@ -3202,9 +3214,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); } } @@ -3212,29 +3224,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 e7741c4016..7488abf446 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 953d6d445f46b0f11e48be6c4b690b2d85ba49c9 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:04:47 -0500 Subject: [PATCH 5/8] 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 404c1eb60e..3f7f985676 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -71,6 +71,7 @@ set(GAMEENGINEDEVICE_SRC # Include/W3DDevice/GameClient/W3DTerrainVisual.h # Include/W3DDevice/GameClient/W3DTreeBuffer.h Include/W3DDevice/GameClient/W3DVideoBuffer.h + Include/W3DDevice/GameClient/W3DScreenshot.h # Include/W3DDevice/GameClient/W3DView.h # Include/W3DDevice/GameClient/W3DVolumetricShadow.h # Include/W3DDevice/GameClient/W3DWater.h @@ -173,6 +174,7 @@ set(GAMEENGINEDEVICE_SRC # Source/W3DDevice/GameClient/W3DTerrainVisual.cpp # Source/W3DDevice/GameClient/W3DTreeBuffer.cpp Source/W3DDevice/GameClient/W3DVideoBuffer.cpp + Source/W3DDevice/GameClient/W3DScreenshot.cpp # Source/W3DDevice/GameClient/W3DView.cpp # Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp @@ -220,6 +222,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 0000000000..234560af0f --- /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 0000000000..08a37c9b9d --- /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 80034af978..de83e8078e 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 b626349c7c..f1aa031bd0 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" @@ -3138,108 +3136,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 4a9b6ff4a301da2eefb43ed3dd5d5bf4fa1bf09e Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:12:09 -0500 Subject: [PATCH 6/8] 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 a2a96b15d2..984329e6dd 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 cd40b8be3a..d4d30a7d14 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -168,8 +168,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 10ad23125f..5ea5d1f991 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 ce754bfc87..f4aed59cd9 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3410,14 +3410,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 2947042d97..217cbd979d 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 }, @@ -795,12 +795,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 f1124b9698..cdf800837d 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 3257ee0e53..a071e4068e 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 }, @@ -853,12 +853,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 f5916e202a..a37dcf9486 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 7/8] 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 6a868a7d17..380490e0e0 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(), 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,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 = NULL; - - 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 = NULL; - - //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 1013d10ca3..db43e7354d 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2998,144 +2998,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(), 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,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 = NULL; - - 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 = NULL; - - //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 96699668aab161b19ffdae5c257211eb185c9780 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:15:41 -0500 Subject: [PATCH 8/8] 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 --- Core/GameEngineDevice/CMakeLists.txt | 1 - .../W3DDevice/GameClient/W3DScreenshot.h | 6 +- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/MessageStream.h | 4 +- .../Include/Common/UserPreferences.h | 1 + .../GameEngine/Include/GameClient/Display.h | 9 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/Common/MessageStream.cpp | 2 +- .../GUI/GUICallbacks/Menus/OptionsMenu.cpp | 11 ++ .../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 +- .../Include/Common/UserPreferences.h | 1 + .../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 +- 34 files changed, 312 insertions(+), 103 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/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 3f7f985676..43d57498f5 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -174,7 +174,6 @@ set(GAMEENGINEDEVICE_SRC # Source/W3DDevice/GameClient/W3DTerrainVisual.cpp # Source/W3DDevice/GameClient/W3DTreeBuffer.cpp Source/W3DDevice/GameClient/W3DVideoBuffer.cpp - Source/W3DDevice/GameClient/W3DScreenshot.cpp # Source/W3DDevice/GameClient/W3DView.cpp # Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index 234560af0f..fe43aa8032 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 e10943f2e4..c55bbadbaf 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -139,6 +139,7 @@ class GlobalData : public SubsystemInterface Int m_terrainLODTargetTimeMS; Bool m_useAlternateMouse; 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 984329e6dd..1a24b12e6e 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/Common/UserPreferences.h b/Generals/Code/GameEngine/Include/Common/UserPreferences.h index aef49361d3..175ce1eddd 100644 --- a/Generals/Code/GameEngine/Include/Common/UserPreferences.h +++ b/Generals/Code/GameEngine/Include/Common/UserPreferences.h @@ -91,6 +91,7 @@ class OptionPreferences : public UserPreferences void setOnlineIPAddress(UnsignedInt IP); // convenience function Bool getArchiveReplaysEnabled() const; // convenience function Bool getAlternateMouseModeEnabled(void); // convenience function + Int getJPEGQuality(void); // convenience function Real getScrollFactor(void); // convenience function Bool getDrawScrollAnchor(void); Bool getMoveScrollAnchor(void); diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index d4d30a7d14..00b235121a 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -35,6 +35,12 @@ class View; +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. @@ -168,8 +174,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 4b72223877..c1dae082af 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1180,6 +1180,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) // override INI values with user preferences OptionPreferences optionPref; TheWritableGlobalData->m_useAlternateMouse = optionPref.getAlternateMouseModeEnabled(); + 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 5ea5d1f991..5aeefea00d 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 60ff6cbcee..2d759dbc65 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -335,6 +335,17 @@ Bool OptionPreferences::getAlternateMouseModeEnabled(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) { diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index f4aed59cd9..27647194fc 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3410,14 +3410,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 217cbd979d..7ae1ea3fde 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 }, @@ -795,7 +795,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 13d3a4d934..47bf0b5229 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,6 +204,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 cdf800837d..8fa09a4e36 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 0000000000..364368901a --- /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 1e094f20fd..8d36e555b9 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 5ff1ed4039..609c9ea955 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 ec14d1b1f4..9a653c5d24 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/Common/UserPreferences.h b/GeneralsMD/Code/GameEngine/Include/Common/UserPreferences.h index 7936cfd8ee..324918566c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/UserPreferences.h +++ b/GeneralsMD/Code/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/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index 1ffeeb0a9f..cebeefce4c 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -35,6 +35,12 @@ class View; +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. @@ -168,8 +174,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 071c5debeb..917e851e65 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1208,6 +1208,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 0a5b13da21..bde560b6e3 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 180dad5cfb..c8dd6c49fb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -368,6 +368,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 37e10d7da3..7e50853dad 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3743,14 +3743,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 a071e4068e..9f6a19e6c8 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 }, @@ -853,7 +853,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 22b48e089f..114cfacc31 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,6 +217,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 a37dcf9486..df14b0480a 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 0000000000..364368901a --- /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 6e7e9828dd..44f625229b 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 957c7d235d..8f2078a810 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 011b913c8a..9ce3c6667c 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