diff --git a/CMakeLists.txt b/CMakeLists.txt
index 28ce09560e9..39062e3fbe6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -65,6 +65,7 @@ endif()
include(cmake/config.cmake)
include(cmake/gamespy.cmake)
include(cmake/lzhl.cmake)
+include(cmake/stb.cmake)
if (IS_VS6_BUILD)
# The original max sdk does not compile against a modern compiler.
diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h
index 9da2f47e9d1..35faf2a2752 100644
--- a/Core/GameEngine/Include/Common/OptionPreferences.h
+++ b/Core/GameEngine/Include/Common/OptionPreferences.h
@@ -72,6 +72,7 @@ class OptionPreferences : public UserPreferences
Bool getRightMouseScrollWithAlternateMouseEnabled() const;
Bool getRetaliationModeEnabled();
Bool getDoubleClickAttackMoveEnabled();
+ Int getJpegQuality() const;
Real getScrollFactor();
Bool getDrawScrollAnchor();
Bool getMoveScrollAnchor();
diff --git a/Core/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h
index 8c022244206..59cc1283232 100644
--- a/Core/GameEngine/Include/GameClient/Display.h
+++ b/Core/GameEngine/Include/GameClient/Display.h
@@ -33,6 +33,16 @@
#include "GameClient/GameFont.h"
#include "GameClient/View.h"
+enum ScreenshotFormat
+{
+ SCREENSHOT_JPEG,
+ SCREENSHOT_PNG,
+
+ SCREENSHOT_FORMAT_COUNT
+};
+
+constexpr const Int DEFAULT_JPEG_QUALITY = 80;
+
struct ShroudLevel
{
Short m_currentShroud; ///< A Value of 1 means shrouded. 0 is not. Negative is the count of people looking.
@@ -172,7 +182,7 @@ class Display : public SubsystemInterface
virtual void preloadModelAssets( AsciiString model ) = 0; ///< preload model asset
virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset
- virtual void takeScreenShot() = 0; ///< saves screenshot to a file
+ virtual void takeScreenShot(ScreenshotFormat format, Int jpegQuality = DEFAULT_JPEG_QUALITY) = 0; ///< saves screenshot in specified format
virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence
virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display
virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off
diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp
index 308447ed5f9..af61c4c84fe 100644
--- a/Core/GameEngine/Source/Common/OptionPreferences.cpp
+++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp
@@ -38,6 +38,7 @@
#include "Common/OptionPreferences.h"
#include "GameClient/ClientInstance.h"
+#include "GameClient/Display.h"
#include "GameClient/LookAtXlat.h"
#include "GameClient/Mouse.h"
@@ -239,6 +240,18 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled()
return FALSE;
}
+Int OptionPreferences::getJpegQuality() const
+{
+ OptionPreferences::const_iterator it = find("JpegQuality");
+ if (it == end())
+ return DEFAULT_JPEG_QUALITY;
+
+ // TheSuperHackers @info bobtista 14/07/2026 Clamp the quality to 50-95: above 95 the file
+ // size increases significantly with no visible benefit, below 50 the image degrades visibly.
+ const Int quality = atoi(it->second.str());
+ return clamp(50, quality, 95);
+}
+
Real OptionPreferences::getScrollFactor()
{
OptionPreferences::const_iterator it = find("ScrollFactor");
diff --git a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
index 8bfb95a7bac..1af3e6c6438 100644
--- a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
+++ b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
@@ -3737,7 +3737,15 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
case GameMessage::MSG_META_TAKE_SCREENSHOT:
{
if (TheDisplay)
- TheDisplay->takeScreenShot();
+ TheDisplay->takeScreenShot(SCREENSHOT_JPEG, TheGlobalData->m_jpegQuality);
+ disp = DESTROY_MESSAGE;
+ break;
+ }
+
+ case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG:
+ {
+ if (TheDisplay)
+ TheDisplay->takeScreenShot(SCREENSHOT_PNG);
disp = DESTROY_MESSAGE;
break;
}
diff --git a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
index e5032c93b75..31a954e67b5 100644
--- a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
+++ b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
@@ -171,6 +171,7 @@ static const LookupListRec GameMessageMetaTypeNames[] =
{ "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION },
{ "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT },
+ { "TAKE_SCREENSHOT_PNG", GameMessage::MSG_META_TAKE_SCREENSHOT_PNG },
{ "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER },
{ "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT },
@@ -945,6 +946,26 @@ void MetaMap::generateMetaMap()
map->m_usableIn = COMMANDUSABLE_GAME;
}
}
+ {
+ MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT);
+ if (map->m_key == MK_NONE)
+ {
+ map->m_key = MK_F12;
+ map->m_transition = DOWN;
+ map->m_modState = NONE;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
+ {
+ MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG);
+ if (map->m_key == MK_NONE)
+ {
+ map->m_key = MK_F12;
+ map->m_transition = DOWN;
+ map->m_modState = CTRL;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
#if defined(RTS_DEBUG)
{
diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt
index 977c2736f94..cf7e2cae76d 100644
--- a/Core/GameEngineDevice/CMakeLists.txt
+++ b/Core/GameEngineDevice/CMakeLists.txt
@@ -73,6 +73,7 @@ set(GAMEENGINEDEVICE_SRC
Include/W3DDevice/GameClient/W3DTreeBuffer.h
Include/W3DDevice/GameClient/W3DVideoBuffer.h
Include/W3DDevice/GameClient/W3DView.h
+ Include/W3DDevice/GameClient/W3DScreenshot.h
# Include/W3DDevice/GameClient/W3DVolumetricShadow.h
Include/W3DDevice/GameClient/W3DWater.h
Include/W3DDevice/GameClient/W3DWaterTracks.h
@@ -175,6 +176,8 @@ set(GAMEENGINEDEVICE_SRC
Source/W3DDevice/GameClient/W3DTreeBuffer.cpp
Source/W3DDevice/GameClient/W3DVideoBuffer.cpp
Source/W3DDevice/GameClient/W3DView.cpp
+ Source/W3DDevice/GameClient/W3DScreenshot.cpp
+ Source/W3DDevice/GameClient/stb_image_write_impl.cpp
# Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp
# Source/W3DDevice/GameClient/W3DWebBrowser.cpp
Source/W3DDevice/GameClient/Water/W3DWater.cpp
@@ -220,6 +223,7 @@ target_include_directories(corei_gameenginedevice_public INTERFACE
target_link_libraries(corei_gameenginedevice_private INTERFACE
corei_always
corei_main
+ stb
)
target_link_libraries(corei_gameenginedevice_public INTERFACE
diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h
new file mode 100644
index 00000000000..e6edec10cd8
--- /dev/null
+++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h
@@ -0,0 +1,28 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#pragma once
+
+#include "GameClient/Display.h"
+
+void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality);
+
+// Called once per frame on the main thread to show messages for screenshots
+// that the screenshot thread has finished writing.
+void W3D_UpdateScreenshotMessages();
+
diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
new file mode 100644
index 00000000000..34a91650756
--- /dev/null
+++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
@@ -0,0 +1,236 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#include "W3DDevice/GameClient/W3DScreenshot.h"
+#include "Common/GlobalData.h"
+#include "GameClient/GameText.h"
+#include "GameClient/InGameUI.h"
+#include "WW3D2/dx8wrapper.h"
+#include "WW3D2/surfaceclass.h"
+#include "mpsc_intrusive_queue.h"
+#include
+
+struct ScreenshotThreadData
+{
+ ScreenshotThreadData()
+ : pixelData(nullptr)
+ {
+ }
+
+ ~ScreenshotThreadData()
+ {
+ delete[] pixelData;
+ }
+
+ unsigned char* pixelData; // is owner
+ unsigned int width;
+ unsigned int height;
+ unsigned int pitch;
+ bool is16Bit;
+ char userDataDirectory[_MAX_PATH];
+ char leafname[_MAX_FNAME];
+ int quality;
+ ScreenshotFormat format;
+};
+
+// TheInGameUI is not thread safe, so the screenshot threads cannot show the success message
+// themselves. Each thread pushes the written filename onto this queue and the main thread
+// shows all pending messages in W3D_UpdateScreenshotMessages, so no message is lost when
+// multiple screenshot threads finish within the same frame.
+struct ScreenshotWrittenMessage
+{
+ ScreenshotWrittenMessage* next;
+ char leafname[_MAX_FNAME];
+};
+static MPSCIntrusiveQueue s_screenshotWrittenQueue;
+
+static DWORD WINAPI screenshotThreadFunc(LPVOID param)
+{
+ ScreenshotThreadData* data = static_cast(param);
+
+ // TheSuperHackers @feature bobtista 08/07/2026 Save screenshots into a Screenshots subfolder
+ // to keep the user data root folder tidy.
+ char pathname[_MAX_PATH];
+ strlcpy(pathname, data->userDataDirectory, ARRAY_SIZE(pathname));
+ strlcat(pathname, "Screenshots\\", ARRAY_SIZE(pathname));
+ CreateDirectory(pathname, nullptr);
+ strlcat(pathname, data->leafname, ARRAY_SIZE(pathname));
+
+ const unsigned int width = data->width;
+ const unsigned int height = data->height;
+
+ // Convert to R8G8B8 for stb_image_write.
+ unsigned char* image = new unsigned char[3 * width * height];
+
+ if (!data->is16Bit)
+ {
+ // Convert A8R8G8B8/X8R8G8B8 to R8G8B8
+ for (unsigned int y = 0; y < height; ++y)
+ {
+ const unsigned int* srcLine = reinterpret_cast(data->pixelData + y * data->pitch);
+ for (unsigned int x = 0; x < width; ++x)
+ {
+ const unsigned int argb = srcLine[x];
+ const unsigned int index = 3 * (x + y * width);
+ image[index + 0] = (unsigned char)(argb >> 16); // r
+ image[index + 1] = (unsigned char)(argb >> 8); // g
+ image[index + 2] = (unsigned char)(argb >> 0); // b
+ }
+ }
+ }
+ else
+ {
+ // Convert R5G6B5 to R8G8B8
+ for (unsigned int y = 0; y < height; ++y)
+ {
+ const unsigned short* srcLine = reinterpret_cast(data->pixelData + y * data->pitch);
+ for (unsigned int x = 0; x < width; ++x)
+ {
+ const unsigned short rgb = srcLine[x];
+ const unsigned int index = 3 * (x + y * width);
+ image[index + 0] = (unsigned char)((rgb & 0xF800) >> 8); // r
+ image[index + 1] = (unsigned char)((rgb & 0x07E0) >> 3); // g
+ image[index + 2] = (unsigned char)((rgb & 0x001F) << 3); // b
+ }
+ }
+ }
+
+ int success = 0;
+ switch (data->format)
+ {
+ case SCREENSHOT_JPEG:
+ success = stbi_write_jpg(pathname, width, height, 3, image, data->quality);
+ break;
+ case SCREENSHOT_PNG:
+ success = stbi_write_png(pathname, width, height, 3, image, width * 3);
+ break;
+ }
+
+ if (success)
+ {
+ ScreenshotWrittenMessage* message = new ScreenshotWrittenMessage;
+ strlcpy(message->leafname, data->leafname, ARRAY_SIZE(message->leafname));
+ s_screenshotWrittenQueue.Push(message);
+ }
+ else
+ {
+ DEBUG_LOG(("Failed to write screenshot %s", pathname));
+ }
+
+ delete[] image;
+ delete data;
+
+ return success;
+}
+
+void W3D_UpdateScreenshotMessages()
+{
+ ScreenshotWrittenMessage* message = s_screenshotWrittenQueue.Flush();
+ while (message != nullptr)
+ {
+ UnicodeString ufileName;
+ ufileName.translate(message->leafname);
+ TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
+ ScreenshotWrittenMessage* next = message->next;
+ delete message;
+ message = next;
+ }
+}
+
+void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality)
+{
+ static constexpr const char* const ScreenshotFormatExtensions[] = { "jpg", "png" };
+ static_assert(ARRAY_SIZE(ScreenshotFormatExtensions) == SCREENSHOT_FORMAT_COUNT, "Incorrect array size");
+
+ // The filename is created here so the timestamp matches the capture time.
+ char leafname[_MAX_FNAME];
+ const char* extension = ScreenshotFormatExtensions[format];
+
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s",
+ st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension);
+
+ // TheSuperHackers @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);
+
+ // TheSuperHackers @bugfix bobtista 08/07/2026 Support the 16 bit back buffer format that the
+ // game uses when running in 16 bit color mode. Reading it with the 32 bit stride read garbage.
+ const bool is32Bit = surfaceDesc.Format == WW3D_FORMAT_A8R8G8B8 || surfaceDesc.Format == WW3D_FORMAT_X8R8G8B8;
+ const bool is16Bit = surfaceDesc.Format == WW3D_FORMAT_R5G6B5;
+
+ if (!is32Bit && !is16Bit)
+ {
+ DEBUG_LOG(("Screenshot does not support back buffer format %d", (int)surfaceDesc.Format));
+ surface->Release_Ref();
+ return;
+ }
+
+ SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)));
+ DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr);
+
+ surface->Release_Ref();
+ surface = nullptr;
+
+ struct Rect
+ {
+ int Pitch;
+ void* pBits;
+ } lrect;
+
+ lrect.pBits = surfaceCopy->Lock(&lrect.Pitch);
+ if (lrect.pBits == nullptr)
+ {
+ surfaceCopy->Release_Ref();
+ return;
+ }
+
+ ScreenshotThreadData* threadData = new ScreenshotThreadData();
+ threadData->width = surfaceDesc.Width;
+ threadData->height = surfaceDesc.Height;
+ threadData->pitch = lrect.Pitch;
+ threadData->is16Bit = is16Bit;
+ threadData->quality = jpegQuality;
+ threadData->format = format;
+ strlcpy(threadData->userDataDirectory, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(threadData->userDataDirectory));
+ strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname));
+
+ // Copy the locked surface with a single memcpy, including any row padding. The pixel
+ // conversion and all file operations are done on the screenshot thread to keep the
+ // main thread cheap.
+ threadData->pixelData = new unsigned char[threadData->pitch * threadData->height];
+ memcpy(threadData->pixelData, lrect.pBits, threadData->pitch * threadData->height);
+
+ surfaceCopy->Unlock();
+ surfaceCopy->Release_Ref();
+ surfaceCopy = nullptr;
+
+ const HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, nullptr);
+ if (hThread)
+ {
+ CloseHandle(hThread);
+ }
+ else
+ {
+ delete threadData;
+ }
+}
diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp
new file mode 100644
index 00000000000..2264ba2c403
--- /dev/null
+++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp
@@ -0,0 +1,21 @@
+/*
+** Command & Conquer Generals(tm)
+** Copyright 2025 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+#include
+
diff --git a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
index 228506fee0e..50c29fcc37b 100644
--- a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
+++ b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
@@ -75,6 +75,7 @@ set(WWLIB_SRC
#md5.h
mempool.h
mmsys.h
+ mpsc_intrusive_queue.h
multilist.cpp
multilist.h
mutex.cpp
diff --git a/Core/Libraries/Source/WWVegas/WWLib/mpsc_intrusive_queue.h b/Core/Libraries/Source/WWVegas/WWLib/mpsc_intrusive_queue.h
new file mode 100644
index 00000000000..11e3da740af
--- /dev/null
+++ b/Core/Libraries/Source/WWVegas/WWLib/mpsc_intrusive_queue.h
@@ -0,0 +1,79 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2026 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#pragma once
+
+// Requires to be included first for the Interlocked intrinsics.
+#include "Utility/interlocked_adapter.h"
+#include "Utility/CppMacros.h"
+
+// Lock-free intrusive queue for multiple producer threads and a single consumer thread,
+// using the same algorithm as the Win32 InterlockedPushEntrySList/InterlockedFlushSList
+// pair. The node type T must have a T* next member, which the queue owns while the node
+// is queued.
+template
+class MPSCIntrusiveQueue
+{
+public:
+ MPSCIntrusiveQueue()
+ : m_head(nullptr)
+ {
+ }
+
+ // Pushes a node. Safe to call from multiple threads concurrently. The node is only
+ // published to the consumer when the compare exchange succeeds against an unchanged
+ // head, so its next pointer is never visible while stale.
+ void Push(T* node)
+ {
+ T* head;
+ do
+ {
+ head = m_head;
+ node->next = head;
+ }
+ while (InterlockedCompareExchangePointer(
+ reinterpret_cast(&m_head), node, head) != head);
+ }
+
+ // Detaches all nodes and returns them linked in push order, or null if the queue is
+ // empty. Must only be called by the single consumer thread. The whole chain is taken
+ // with one exchange, so nodes are never popped individually and the push loop cannot
+ // suffer ABA.
+ T* Flush()
+ {
+ T* list = static_cast(InterlockedExchangePointer(
+ reinterpret_cast(&m_head), nullptr));
+
+ // The detached chain is in last-in-first-out order, so reverse it.
+ T* reversed = nullptr;
+ while (list != nullptr)
+ {
+ T* next = list->next;
+ list->next = reversed;
+ reversed = list;
+ list = next;
+ }
+ return reversed;
+ }
+
+private:
+ MPSCIntrusiveQueue(const MPSCIntrusiveQueue&) CPP_11(= delete);
+ MPSCIntrusiveQueue& operator=(const MPSCIntrusiveQueue&) CPP_11(= delete);
+
+ T* volatile m_head;
+};
diff --git a/Dependencies/Utility/Utility/interlocked_adapter.h b/Dependencies/Utility/Utility/interlocked_adapter.h
index 45da6a2f9cb..126c82f1997 100644
--- a/Dependencies/Utility/Utility/interlocked_adapter.h
+++ b/Dependencies/Utility/Utility/interlocked_adapter.h
@@ -25,4 +25,16 @@ inline long InterlockedCompareExchange(long volatile *Destination, long Exchange
return (long)InterlockedCompareExchange((PVOID*)Destination, (PVOID)Exchange, (PVOID)Comparand);
}
+// The VC6 SDK signatures take non-volatile pointers, so the volatile qualifier
+// must be removed with const_cast before reinterpret_cast can change the type.
+inline PVOID InterlockedExchangePointer(PVOID volatile *Target, PVOID Value)
+{
+ return reinterpret_cast(InterlockedExchange(reinterpret_cast(const_cast(Target)), reinterpret_cast(Value)));
+}
+
+inline PVOID InterlockedCompareExchangePointer(PVOID volatile *Destination, PVOID Exchange, PVOID Comparand)
+{
+ return InterlockedCompareExchange(const_cast(Destination), Exchange, Comparand);
+}
+
#endif
diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h
index a35f7169817..ca03873d636 100644
--- a/Generals/Code/GameEngine/Include/Common/GlobalData.h
+++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h
@@ -143,6 +143,7 @@ class GlobalData : public SubsystemInterface
Bool m_clientRetaliationModeEnabled;
Bool m_doubleClickAttackMove;
Bool m_rightMouseAlwaysScrolls;
+ Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots.
Bool m_useWaterPlane;
Bool m_useCloudPlane;
Bool m_useShadowVolumes;
diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h
index 040de703615..079d9671a16 100644
--- a/Generals/Code/GameEngine/Include/Common/MessageStream.h
+++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h
@@ -256,7 +256,8 @@ class GameMessage : public MemoryPoolObject
MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone
MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released.
- MSG_META_TAKE_SCREENSHOT, ///< take screenshot
+ MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot
+ MSG_META_TAKE_SCREENSHOT_PNG, ///< TheSuperHackers @feature Take lossless PNG screenshot
MSG_META_ALL_CHEER, ///< Yay! :)
MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode
diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
index a171f46aebc..e10aa482f70 100644
--- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
+++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
@@ -57,6 +57,7 @@
#include "GameLogic/Module/BodyModule.h"
#include "GameClient/Color.h"
+#include "GameClient/Display.h"
#include "GameClient/TerrainVisual.h"
#include "GameNetwork/FirewallHelper.h"
@@ -650,6 +651,7 @@ GlobalData::GlobalData()
m_enableDynamicLOD = TRUE;
m_enableStaticLOD = TRUE;
m_rightMouseAlwaysScrolls = FALSE;
+ m_jpegQuality = DEFAULT_JPEG_QUALITY;
m_useWaterPlane = FALSE;
m_useCloudPlane = FALSE;
m_downwindAngle = ( -0.785f );//Northeast!
@@ -1200,6 +1202,7 @@ void GlobalData::parseGameDataDefinition( INI* ini )
TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled();
TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled();
TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled();
+ TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality();
TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor();
TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor();
TheWritableGlobalData->m_moveScrollAnchor = optionPref.getMoveScrollAnchor();
diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp
index 1a1e502f3df..d62fd8ad839 100644
--- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp
+++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp
@@ -337,6 +337,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t)
CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION)
CASE_LABEL(MSG_META_END_PREFER_SELECTION)
CASE_LABEL(MSG_META_TAKE_SCREENSHOT)
+ CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG)
CASE_LABEL(MSG_META_ALL_CHEER)
CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE)
CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT)
diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
index bae15ef4202..5f7a75d8e94 100644
--- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
+++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
@@ -829,6 +829,16 @@ static void saveOptions()
TheWritableGlobalData->m_gameWindowTransitionSpeedMultiplier = speed;
}
+ //-------------------------------------------------------------------------------------------------
+ // Set JPEG screenshot quality
+ {
+ Int quality = pref->getJpegQuality();
+ AsciiString prefString;
+ prefString.format("%d", quality);
+ (*pref)["JpegQuality"] = prefString;
+ TheWritableGlobalData->m_jpegQuality = quality;
+ }
+
//-------------------------------------------------------------------------------------------------
// Resolution
//
diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt
index b7827514490..3212fb6805d 100644
--- a/Generals/Code/GameEngineDevice/CMakeLists.txt
+++ b/Generals/Code/GameEngineDevice/CMakeLists.txt
@@ -200,6 +200,7 @@ target_link_libraries(g_gameenginedevice PRIVATE
corei_gameenginedevice_private
gi_always
gi_main
+ stb
)
target_link_libraries(g_gameenginedevice PUBLIC
diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
index 11e480e210c..aa3f0b79d2c 100644
--- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
+++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
@@ -122,7 +122,7 @@ class W3DDisplay : public Display
virtual VideoBuffer* createVideoBuffer() override; ///< Create a video buffer that can be used for this display
- virtual void takeScreenShot() override; //save screenshot to file
+ virtual void takeScreenShot(ScreenshotFormat format, Int jpegQuality) override; //save screenshot in specified format
virtual void toggleMovieCapture() override; //enable AVI or frame capture mode.
virtual void toggleLetterBox() override; ///m_headless)
return;
+ // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread.
+ W3D_UpdateScreenshotMessages();
+
updateAverageFPS();
if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD())
{
@@ -2941,227 +2945,17 @@ void W3DDisplay::setShroudLevel( Int x, Int y, CellShroudStatus setting )
}
}
-//=============================================================================
-///Utility function to dump data into a .BMP file
-static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height)
-{
- HANDLE hf; // file handle
- BITMAPFILEHEADER hdr; // bitmap file-header
- PBITMAPINFOHEADER pbih; // bitmap info-header
- LPBYTE lpBits; // memory pointer
- DWORD dwTotal; // total count of bytes
- DWORD cb; // incremental count of bytes
- BYTE *hp; // byte pointer
- DWORD dwTmp;
-
- PBITMAPINFO pbmi;
-
- pbmi = (PBITMAPINFO) LocalAlloc(LPTR,sizeof(BITMAPINFOHEADER));
- if (pbmi == nullptr)
- return;
-
- pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
- pbmi->bmiHeader.biWidth = width;
- pbmi->bmiHeader.biHeight = height;
- pbmi->bmiHeader.biPlanes = 1;
- pbmi->bmiHeader.biBitCount = 24;
- pbmi->bmiHeader.biCompression = BI_RGB;
- pbmi->bmiHeader.biSizeImage = (pbmi->bmiHeader.biWidth + 7) /8 * pbmi->bmiHeader.biHeight * 24;
- pbmi->bmiHeader.biClrImportant = 0;
-
- pbih = (PBITMAPINFOHEADER) pbmi;
- lpBits = (LPBYTE) image;
-
- // Create the .BMP file.
- hf = CreateFile(pszFile,
- GENERIC_READ | GENERIC_WRITE,
- (DWORD) 0,
- nullptr,
- CREATE_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- (HANDLE) nullptr);
-
- if (hf != INVALID_HANDLE_VALUE)
- {
- hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M"
- // Compute the size of the entire file.
- hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) +
- pbih->biSize + pbih->biClrUsed
- * sizeof(RGBQUAD) + pbih->biSizeImage);
- hdr.bfReserved1 = 0;
- hdr.bfReserved2 = 0;
-
- // Compute the offset to the array of color indices.
- hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) +
- pbih->biSize + pbih->biClrUsed
- * sizeof (RGBQUAD);
-
- // Copy the BITMAPFILEHEADER into the .BMP file.
- if (WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER),
- (LPDWORD) &dwTmp, nullptr))
- {
- // Copy the BITMAPINFOHEADER and RGBQUAD array into the file.
- if (WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER) + pbih->biClrUsed * sizeof (RGBQUAD),(LPDWORD) &dwTmp, nullptr))
- {
- // Copy the array of color indices into the .BMP file.
- dwTotal = cb = pbih->biSizeImage;
- hp = lpBits;
- WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp, nullptr);
- }
- }
-
- // Close the .BMP file.
- CloseHandle(hf);
- }
-
- // Free memory.
- LocalFree( (HLOCAL) pbmi);
-}
-
-///Save Screen Capture to a file
-void W3DDisplay::takeScreenShot()
-{
- char leafname[256];
- char pathname[1024];
-
- static int frame_number = 1;
-
- Bool done = false;
- while (!done) {
-#ifdef CAPTURE_TO_TARGA
- sprintf( leafname, "%s%.3d.tga", "sshot", frame_number++);
-#else
- sprintf( leafname, "%s%.3d.bmp", "sshot", frame_number++);
-#endif
- strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname));
- strlcat(pathname, leafname, ARRAY_SIZE(pathname));
- if (_access( pathname, 0 ) == -1)
- done = true;
- }
-
- // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface.
- // Originally this code took the front buffer and tried to lock it. This does not work when the
- // render view clips outside the desktop boundaries. It crashed the game.
- SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer();
-
- SurfaceClass::SurfaceDescription surfaceDesc;
- surface->Get_Description(surfaceDesc);
-
- SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)));
- DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr);
-
- surface->Release_Ref();
- surface = nullptr;
-
- struct Rect
- {
- int Pitch;
- void* pBits;
- } lrect;
-
- lrect.pBits = surfaceCopy->Lock(&lrect.Pitch);
- if (lrect.pBits == nullptr)
- {
- surfaceCopy->Release_Ref();
- return;
- }
-
- unsigned int x,y,index,index2,width,height;
-
- width = surfaceDesc.Width;
- height = surfaceDesc.Height;
-
- char *image=NEW char[3*width*height];
-#ifdef CAPTURE_TO_TARGA
- //bytes are mixed in targa files, not rgb order.
- for (y=0; yUnlock();
- surfaceCopy->Release_Ref();
- surfaceCopy = nullptr;
-
- Targa targ;
- memset(&targ.Header,0,sizeof(targ.Header));
- targ.Header.Width=width;
- targ.Header.Height=height;
- targ.Header.PixelDepth=24;
- targ.Header.ImageType=TGA_TRUECOLOR;
- targ.SetImage(image);
- targ.YFlip();
-
- targ.Save(pathname,TGAF_IMAGE,false);
-#else //capturing to bmp file
- //bmp is same byte order
- for (y=0; yUnlock();
- surfaceCopy->Release_Ref();
- surfaceCopy = nullptr;
-
- //Flip the image
- char *ptr,*ptr1;
- char v,v1;
-
- for (y = 0; y < (height >> 1); y++)
- {
- /* Compute address of lines to exchange. */
- ptr = (image + ((width * y) * 3));
- ptr1 = (image + ((width * (height - 1)) * 3));
- ptr1 -= ((width * y) * 3);
-
- /* Exchange all the pixels on this scan line. */
- for (x = 0; x < (width * 3); x++)
- {
- v = *ptr;
- v1 = *ptr1;
- *ptr = v1;
- *ptr1 = v;
- ptr++;
- ptr1++;
- }
- }
- CreateBMPFile(pathname, image, width, height);
-#endif
-
- delete [] image;
-
- UnicodeString ufileName;
- ufileName.translate(leafname);
- TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
-}
-
/** Start/Stop capturing an AVI movie*/
void W3DDisplay::toggleMovieCapture()
{
WW3D::Toggle_Movie_Capture("Movie",30);
}
+void W3DDisplay::takeScreenShot(ScreenshotFormat format, Int jpegQuality)
+{
+ W3D_TakeCompressedScreenshot(format, jpegQuality);
+}
+
#if defined(RTS_DEBUG)
diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
index ee27b4cc7a0..c2219ec7d2e 100644
--- a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
+++ b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
@@ -101,7 +101,7 @@ class GUIEditDisplay : public Display
virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) override { }
virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY,
Int endX, Int endY ) override { }
- virtual void takeScreenShot() override { }
+ virtual void takeScreenShot(ScreenshotFormat format, Int jpegQuality) override { }
virtual void toggleMovieCapture() override {}
// methods that we need to stub
diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
index a69dcdd4444..2b646360086 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
@@ -144,6 +144,7 @@ class GlobalData : public SubsystemInterface
Bool m_clientRetaliationModeEnabled;
Bool m_doubleClickAttackMove;
Bool m_rightMouseAlwaysScrolls;
+ Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots.
Bool m_useWaterPlane;
Bool m_useCloudPlane;
Bool m_useShadowVolumes;
diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
index a34f984f32b..e5dc1fc0896 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
@@ -256,7 +256,8 @@ class GameMessage : public MemoryPoolObject
MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone
MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released.
- MSG_META_TAKE_SCREENSHOT, ///< take screenshot
+ MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot
+ MSG_META_TAKE_SCREENSHOT_PNG, ///< TheSuperHackers @feature Take lossless PNG screenshot
MSG_META_ALL_CHEER, ///< Yay! :)
MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode
diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
index ada4995cfa1..6f5f786ed27 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
@@ -58,6 +58,7 @@
#include "GameLogic/Module/BodyModule.h"
#include "GameClient/Color.h"
+#include "GameClient/Display.h"
#include "GameClient/TerrainVisual.h"
#include "GameNetwork/FirewallHelper.h"
@@ -654,6 +655,7 @@ GlobalData::GlobalData()
m_enableDynamicLOD = TRUE;
m_enableStaticLOD = TRUE;
m_rightMouseAlwaysScrolls = FALSE;
+ m_jpegQuality = DEFAULT_JPEG_QUALITY;
m_useWaterPlane = FALSE;
m_useCloudPlane = FALSE;
m_downwindAngle = ( -0.785f );//Northeast!
@@ -1207,6 +1209,7 @@ void GlobalData::parseGameDataDefinition( INI* ini )
TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled();
TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled();
TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled();
+ TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality();
TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor();
TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor();
TheWritableGlobalData->m_moveScrollAnchor = optionPref.getMoveScrollAnchor();
diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp
index 2f38045f05b..9980021c6ee 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp
@@ -337,6 +337,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t)
CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION)
CASE_LABEL(MSG_META_END_PREFER_SELECTION)
CASE_LABEL(MSG_META_TAKE_SCREENSHOT)
+ CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG)
CASE_LABEL(MSG_META_ALL_CHEER)
CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE)
CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT)
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
index 2ec626d9211..4971df70354 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
@@ -854,6 +854,16 @@ static void saveOptions()
TheWritableGlobalData->m_gameWindowTransitionSpeedMultiplier = speed;
}
+ //-------------------------------------------------------------------------------------------------
+ // Set JPEG screenshot quality
+ {
+ Int quality = pref->getJpegQuality();
+ AsciiString prefString;
+ prefString.format("%d", quality);
+ (*pref)["JpegQuality"] = prefString;
+ TheWritableGlobalData->m_jpegQuality = quality;
+ }
+
//-------------------------------------------------------------------------------------------------
// Resolution
//
diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
index 46e0e9e9df8..198662eb921 100644
--- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
+++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
@@ -213,6 +213,7 @@ target_link_libraries(z_gameenginedevice PRIVATE
corei_gameenginedevice_private
zi_always
zi_main
+ stb
)
target_link_libraries(z_gameenginedevice PUBLIC
diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
index c43ec2691a7..ade19e0ef83 100644
--- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
+++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
@@ -122,7 +122,7 @@ class W3DDisplay : public Display
virtual VideoBuffer* createVideoBuffer() override; ///< Create a video buffer that can be used for this display
- virtual void takeScreenShot() override; //save screenshot to file
+ virtual void takeScreenShot(ScreenshotFormat format, Int jpegQuality) override; //save screenshot in specified format
virtual void toggleMovieCapture() override; //enable AVI or frame capture mode.
virtual void toggleLetterBox() override; ///m_headless)
return;
+ // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread.
+ W3D_UpdateScreenshotMessages();
+
updateAverageFPS();
if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD())
{
@@ -3053,227 +3057,17 @@ void W3DDisplay::setShroudLevel( Int x, Int y, CellShroudStatus setting )
}
}
-//=============================================================================
-///Utility function to dump data into a .BMP file
-static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height)
-{
- HANDLE hf; // file handle
- BITMAPFILEHEADER hdr; // bitmap file-header
- PBITMAPINFOHEADER pbih; // bitmap info-header
- LPBYTE lpBits; // memory pointer
- DWORD dwTotal; // total count of bytes
- DWORD cb; // incremental count of bytes
- BYTE *hp; // byte pointer
- DWORD dwTmp;
-
- PBITMAPINFO pbmi;
-
- pbmi = (PBITMAPINFO) LocalAlloc(LPTR,sizeof(BITMAPINFOHEADER));
- if (pbmi == nullptr)
- return;
-
- pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
- pbmi->bmiHeader.biWidth = width;
- pbmi->bmiHeader.biHeight = height;
- pbmi->bmiHeader.biPlanes = 1;
- pbmi->bmiHeader.biBitCount = 24;
- pbmi->bmiHeader.biCompression = BI_RGB;
- pbmi->bmiHeader.biSizeImage = (pbmi->bmiHeader.biWidth + 7) /8 * pbmi->bmiHeader.biHeight * 24;
- pbmi->bmiHeader.biClrImportant = 0;
-
- pbih = (PBITMAPINFOHEADER) pbmi;
- lpBits = (LPBYTE) image;
-
- // Create the .BMP file.
- hf = CreateFile(pszFile,
- GENERIC_READ | GENERIC_WRITE,
- (DWORD) 0,
- nullptr,
- CREATE_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- (HANDLE) nullptr);
-
- if (hf != INVALID_HANDLE_VALUE)
- {
- hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M"
- // Compute the size of the entire file.
- hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) +
- pbih->biSize + pbih->biClrUsed
- * sizeof(RGBQUAD) + pbih->biSizeImage);
- hdr.bfReserved1 = 0;
- hdr.bfReserved2 = 0;
-
- // Compute the offset to the array of color indices.
- hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) +
- pbih->biSize + pbih->biClrUsed
- * sizeof (RGBQUAD);
-
- // Copy the BITMAPFILEHEADER into the .BMP file.
- if (WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER),
- (LPDWORD) &dwTmp, nullptr))
- {
- // Copy the BITMAPINFOHEADER and RGBQUAD array into the file.
- if (WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER) + pbih->biClrUsed * sizeof (RGBQUAD),(LPDWORD) &dwTmp, nullptr))
- {
- // Copy the array of color indices into the .BMP file.
- dwTotal = cb = pbih->biSizeImage;
- hp = lpBits;
- WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp, nullptr);
- }
- }
-
- // Close the .BMP file.
- CloseHandle(hf);
- }
-
- // Free memory.
- LocalFree( (HLOCAL) pbmi);
-}
-
-///Save Screen Capture to a file
-void W3DDisplay::takeScreenShot()
-{
- char leafname[256];
- char pathname[1024];
-
- static int frame_number = 1;
-
- Bool done = false;
- while (!done) {
-#ifdef CAPTURE_TO_TARGA
- sprintf( leafname, "%s%.3d.tga", "sshot", frame_number++);
-#else
- sprintf( leafname, "%s%.3d.bmp", "sshot", frame_number++);
-#endif
- strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname));
- strlcat(pathname, leafname, ARRAY_SIZE(pathname));
- if (_access( pathname, 0 ) == -1)
- done = true;
- }
-
- // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface.
- // Originally this code took the front buffer and tried to lock it. This does not work when the
- // render view clips outside the desktop boundaries. It crashed the game.
- SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer();
-
- SurfaceClass::SurfaceDescription surfaceDesc;
- surface->Get_Description(surfaceDesc);
-
- SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)));
- DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr);
-
- surface->Release_Ref();
- surface = nullptr;
-
- struct Rect
- {
- int Pitch;
- void* pBits;
- } lrect;
-
- lrect.pBits = surfaceCopy->Lock(&lrect.Pitch);
- if (lrect.pBits == nullptr)
- {
- surfaceCopy->Release_Ref();
- return;
- }
-
- unsigned int x,y,index,index2,width,height;
-
- width = surfaceDesc.Width;
- height = surfaceDesc.Height;
-
- char *image=NEW char[3*width*height];
-#ifdef CAPTURE_TO_TARGA
- //bytes are mixed in targa files, not rgb order.
- for (y=0; yUnlock();
- surfaceCopy->Release_Ref();
- surfaceCopy = nullptr;
-
- Targa targ;
- memset(&targ.Header,0,sizeof(targ.Header));
- targ.Header.Width=width;
- targ.Header.Height=height;
- targ.Header.PixelDepth=24;
- targ.Header.ImageType=TGA_TRUECOLOR;
- targ.SetImage(image);
- targ.YFlip();
-
- targ.Save(pathname,TGAF_IMAGE,false);
-#else //capturing to bmp file
- //bmp is same byte order
- for (y=0; yUnlock();
- surfaceCopy->Release_Ref();
- surfaceCopy = nullptr;
-
- //Flip the image
- char *ptr,*ptr1;
- char v,v1;
-
- for (y = 0; y < (height >> 1); y++)
- {
- /* Compute address of lines to exchange. */
- ptr = (image + ((width * y) * 3));
- ptr1 = (image + ((width * (height - 1)) * 3));
- ptr1 -= ((width * y) * 3);
-
- /* Exchange all the pixels on this scan line. */
- for (x = 0; x < (width * 3); x++)
- {
- v = *ptr;
- v1 = *ptr1;
- *ptr = v1;
- *ptr1 = v;
- ptr++;
- ptr1++;
- }
- }
- CreateBMPFile(pathname, image, width, height);
-#endif
-
- delete [] image;
-
- UnicodeString ufileName;
- ufileName.translate(leafname);
- TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
-}
-
/** Start/Stop capturing an AVI movie*/
void W3DDisplay::toggleMovieCapture()
{
WW3D::Toggle_Movie_Capture("Movie",30);
}
+void W3DDisplay::takeScreenShot(ScreenshotFormat format, Int jpegQuality)
+{
+ W3D_TakeCompressedScreenshot(format, jpegQuality);
+}
+
#if defined(RTS_DEBUG)
diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
index 9e432ecb827..ddd7b84bafb 100644
--- a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
+++ b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
@@ -101,7 +101,7 @@ class GUIEditDisplay : public Display
virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) override { }
virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY,
Int endX, Int endY ) override { }
- virtual void takeScreenShot() override { }
+ virtual void takeScreenShot(ScreenshotFormat format, Int jpegQuality) override { }
virtual void toggleMovieCapture() override {}
// methods that we need to stub
diff --git a/cmake/stb.cmake b/cmake/stb.cmake
new file mode 100644
index 00000000000..8db04af3e70
--- /dev/null
+++ b/cmake/stb.cmake
@@ -0,0 +1,21 @@
+# Fetch the stb library for writing JPEG and PNG screenshots.
+
+# vcpkg provides stb through a find module (FindStb.cmake), not a config package,
+# so the search must not be restricted to CONFIG mode.
+find_package(Stb QUIET)
+
+if(NOT Stb_FOUND)
+ include(FetchContent)
+ FetchContent_Declare(
+ stb
+ GIT_REPOSITORY https://github.com/TheSuperHackers/stb.git
+ GIT_TAG 5c205738c191bcb0abc65c4febfa9bd25ff35234
+ )
+
+ FetchContent_MakeAvailable(stb)
+
+ set(Stb_INCLUDE_DIR ${stb_SOURCE_DIR})
+endif()
+
+add_library(stb INTERFACE)
+target_include_directories(stb INTERFACE ${Stb_INCLUDE_DIR})
diff --git a/vcpkg.json b/vcpkg.json
index 011b913c8aa..9ce3c6667c3 100644
--- a/vcpkg.json
+++ b/vcpkg.json
@@ -3,6 +3,7 @@
"builtin-baseline": "b02e341c927f16d991edbd915d8ea43eac52096c",
"dependencies": [
"zlib",
- "ffmpeg"
+ "ffmpeg",
+ "stb"
]
}
\ No newline at end of file