Skip to content

Commit e42eb10

Browse files
committed
Implement threaded JPEG screenshot for Generals
1 parent d4f6ebc commit e42eb10

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

Generals/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

Generals/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"
@@ -3022,6 +3028,103 @@ void W3DDisplay::takeScreenShot(void)
30223028
TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
30233029
}
30243030

3031+
/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game
3032+
/// This implementation captures the frame buffer on the main thread, then spawns a background thread
3033+
/// to compress and save the image, allowing the game to continue running smoothly.
3034+
void W3DDisplay::takeScreenShotCompressed(void)
3035+
{
3036+
// TheSuperHackers @bobtista 02/11/2025 Find next available filename
3037+
char leafname[256];
3038+
char pathname[1024];
3039+
static int frame_number = 1;
3040+
3041+
Bool done = false;
3042+
while (!done) {
3043+
sprintf(leafname, "sshot%.3d.jpg", frame_number++);
3044+
strcpy(pathname, TheGlobalData->getPath_UserData().str());
3045+
strlcat(pathname, leafname, ARRAY_SIZE(pathname));
3046+
if (_access(pathname, 0) == -1)
3047+
done = true;
3048+
}
3049+
3050+
// TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy
3051+
SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer();
3052+
SurfaceClass::SurfaceDescription surfaceDesc;
3053+
surface->Get_Description(surfaceDesc);
3054+
3055+
SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)));
3056+
DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL);
3057+
3058+
surface->Release_Ref();
3059+
surface = NULL;
3060+
3061+
struct Rect
3062+
{
3063+
int Pitch;
3064+
void* pBits;
3065+
} lrect;
3066+
3067+
lrect.pBits = surfaceCopy->Lock(&lrect.Pitch);
3068+
if (lrect.pBits == NULL)
3069+
{
3070+
surfaceCopy->Release_Ref();
3071+
return;
3072+
}
3073+
3074+
unsigned int x, y, index, index2;
3075+
unsigned int width = surfaceDesc.Width;
3076+
unsigned int height = surfaceDesc.Height;
3077+
3078+
// TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data
3079+
// Using shared_ptr for automatic cleanup in the background thread
3080+
std::shared_ptr<unsigned char> imageData(new unsigned char[3 * width * height],
3081+
std::default_delete<unsigned char[]>());
3082+
unsigned char* image = imageData.get();
3083+
3084+
// TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB
3085+
for (y = 0; y < height; y++)
3086+
{
3087+
for (x = 0; x < width; x++)
3088+
{
3089+
index = 3 * (x + y * width);
3090+
index2 = y * lrect.Pitch + 4 * x;
3091+
3092+
image[index] = *((unsigned char*)lrect.pBits + index2 + 2); // R
3093+
image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G
3094+
image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B
3095+
}
3096+
}
3097+
3098+
surfaceCopy->Unlock();
3099+
surfaceCopy->Release_Ref();
3100+
surfaceCopy = NULL;
3101+
3102+
// TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread
3103+
std::string pathnameCopy(pathname);
3104+
std::string leafnameCopy(leafname);
3105+
3106+
// TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image
3107+
// This allows the game to continue running without freezing
3108+
std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() {
3109+
// TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100)
3110+
// stbi_write_jpg expects image data with Y-axis going down, which matches our data
3111+
int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90);
3112+
3113+
if (!result) {
3114+
// TheSuperHackers @bobtista 02/11/2025 Log error if write failed
3115+
// Note: Can't show UI message from background thread
3116+
OutputDebugStringA("Failed to write screenshot JPEG\n");
3117+
}
3118+
3119+
// TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope
3120+
}).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently
3121+
3122+
// TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background)
3123+
UnicodeString ufileName;
3124+
ufileName.translate(leafnameCopy.c_str());
3125+
TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
3126+
}
3127+
30253128
/** Start/Stop capturing an AVI movie*/
30263129
void W3DDisplay::toggleMovieCapture(void)
30273130
{

0 commit comments

Comments
 (0)