Skip to content

Commit f49799d

Browse files
committed
Implement threaded JPEG screenshot for GeneralsMD
1 parent 7dbf365 commit f49799d

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class W3DDisplay : public Display
121121
virtual VideoBuffer* createVideoBuffer( void ) ; ///< Create a video buffer that can be used for this display
122122

123123
virtual void takeScreenShot(void); //save screenshot to file
124+
virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling
124125
virtual void toggleMovieCapture(void); //enable AVI or frame capture mode.
125126

126127
virtual void toggleLetterBox(void); ///<enabled letter-boxed display

GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ static void drawFramerateBar(void);
3939
#include <windows.h>
4040
#include <io.h>
4141
#include <time.h>
42+
#include <thread>
43+
#include <memory>
44+
45+
// TheSuperHackers @bobtista 02/11/2025 STB for image encoding
46+
#define STB_IMAGE_WRITE_IMPLEMENTATION
47+
#include <stb_image_write.h>
4248

4349
// USER INCLUDES //////////////////////////////////////////////////////////////
4450
#include "Common/FramePacer.h"
@@ -3141,6 +3147,103 @@ void W3DDisplay::takeScreenShot(void)
31413147
TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
31423148
}
31433149

3150+
/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game
3151+
/// This implementation captures the frame buffer on the main thread, then spawns a background thread
3152+
/// to compress and save the image, allowing the game to continue running smoothly.
3153+
void W3DDisplay::takeScreenShotCompressed(void)
3154+
{
3155+
// TheSuperHackers @bobtista 02/11/2025 Find next available filename
3156+
char leafname[256];
3157+
char pathname[1024];
3158+
static int frame_number = 1;
3159+
3160+
Bool done = false;
3161+
while (!done) {
3162+
sprintf(leafname, "sshot%.3d.jpg", frame_number++);
3163+
strcpy(pathname, TheGlobalData->getPath_UserData().str());
3164+
strlcat(pathname, leafname, ARRAY_SIZE(pathname));
3165+
if (_access(pathname, 0) == -1)
3166+
done = true;
3167+
}
3168+
3169+
// TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy
3170+
SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer();
3171+
SurfaceClass::SurfaceDescription surfaceDesc;
3172+
surface->Get_Description(surfaceDesc);
3173+
3174+
SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)));
3175+
DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL);
3176+
3177+
surface->Release_Ref();
3178+
surface = NULL;
3179+
3180+
struct Rect
3181+
{
3182+
int Pitch;
3183+
void* pBits;
3184+
} lrect;
3185+
3186+
lrect.pBits = surfaceCopy->Lock(&lrect.Pitch);
3187+
if (lrect.pBits == NULL)
3188+
{
3189+
surfaceCopy->Release_Ref();
3190+
return;
3191+
}
3192+
3193+
unsigned int x, y, index, index2;
3194+
unsigned int width = surfaceDesc.Width;
3195+
unsigned int height = surfaceDesc.Height;
3196+
3197+
// TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data
3198+
// Using shared_ptr for automatic cleanup in the background thread
3199+
std::shared_ptr<unsigned char> imageData(new unsigned char[3 * width * height],
3200+
std::default_delete<unsigned char[]>());
3201+
unsigned char* image = imageData.get();
3202+
3203+
// TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB
3204+
for (y = 0; y < height; y++)
3205+
{
3206+
for (x = 0; x < width; x++)
3207+
{
3208+
index = 3 * (x + y * width);
3209+
index2 = y * lrect.Pitch + 4 * x;
3210+
3211+
image[index] = *((unsigned char*)lrect.pBits + index2 + 2); // R
3212+
image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G
3213+
image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B
3214+
}
3215+
}
3216+
3217+
surfaceCopy->Unlock();
3218+
surfaceCopy->Release_Ref();
3219+
surfaceCopy = NULL;
3220+
3221+
// TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread
3222+
std::string pathnameCopy(pathname);
3223+
std::string leafnameCopy(leafname);
3224+
3225+
// TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image
3226+
// This allows the game to continue running without freezing
3227+
std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() {
3228+
// TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100)
3229+
// stbi_write_jpg expects image data with Y-axis going down, which matches our data
3230+
int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90);
3231+
3232+
if (!result) {
3233+
// TheSuperHackers @bobtista 02/11/2025 Log error if write failed
3234+
// Note: Can't show UI message from background thread
3235+
OutputDebugStringA("Failed to write screenshot JPEG\n");
3236+
}
3237+
3238+
// TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope
3239+
}).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently
3240+
3241+
// TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background)
3242+
UnicodeString ufileName;
3243+
ufileName.translate(leafnameCopy.c_str());
3244+
TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
3245+
}
3246+
31443247
/** Start/Stop capturing an AVI movie*/
31453248
void W3DDisplay::toggleMovieCapture(void)
31463249
{

0 commit comments

Comments
 (0)