Skip to content

feat(screenshot): Add threaded JPEG/PNG screenshots without game stalls#1785

Open
bobtista wants to merge 28 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/compressed-screenshot-f11
Open

feat(screenshot): Add threaded JPEG/PNG screenshots without game stalls#1785
bobtista wants to merge 28 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/compressed-screenshot-f11

Conversation

@bobtista

@bobtista bobtista commented Nov 3, 2025

Copy link
Copy Markdown

Summary

Replaces the old BMP screenshot with compressed JPEG screenshots that don't stall the game, and adds PNG support.

Closes #1555
Closes #106 ... sort of

Adds a new screenshot function using the stb_image_write library with background threading:

  • F12 - Compressed JPEG screenshot (no stalling, ~600KB files)
  • Ctrl+F12 - Lossless PNG screenshot

Notes

  • stb_image-write is public domain licensed, single-header, zero compilation needed
  • Captures frame buffer on main thread (~1-2ms)
  • Spawns detached thread for JPEG/PNG compression and disk I/O
  • JPEG quality configurable via Options.ini (default 80)
  • Works for both Generals and Zero Hour

Testing

  • Screenshots save correctly with F12 (jpg) and Ctrl+F12 (png)
  • No game stalling during screenshot
  • Files appear in correct directory with sequential numbering
    sshot003

@bobtista bobtista force-pushed the bobtista/compressed-screenshot-f11 branch 4 times, most recently from 20a3df1 to 37bd840 Compare November 3, 2025 04:32
@Stubbjax

Stubbjax commented Nov 3, 2025

Copy link
Copy Markdown

Some initial thoughts:

  • Keep variants of the same function to a single hotkey; instead toggle image format via a new setting in Options.ini
  • Apply image compression / quality via a new setting in Options.ini
  • Can we consolidate this logic in core instead of repeating the implementation twice?

@xezon

xezon commented Nov 3, 2025

Copy link
Copy Markdown

Agree with Stubbjax.

JPG 90 is big file. Better make it default 80.

Replace BMP screenshot with PNG screenshot. PNG is lossless compressed and always better than BMP.

Make F12 take JPG 80 screenshot. Make CTRL+F12 take PNG screenshot. Make JPG Quality adjustable.

Remove the old BMP code(s) and only use the new code for screenshot.

@xezon

xezon commented Nov 3, 2025

Copy link
Copy Markdown

Regarding Github formatting:

When you write

Addresses #1555

Then it will not close this report when this is merged.

Please read up on it here:
https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#closing-multiple-issues

@xezon xezon added Enhancement Is new feature or request Major Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour labels Nov 3, 2025
@L3-M L3-M added this to the GenTool features replication milestone Nov 3, 2025
@L3-M L3-M added the Input label Nov 3, 2025
@bobtista

bobtista commented Nov 3, 2025

Copy link
Copy Markdown
Author

Some initial thoughts:

  • Keep variants of the same function to a single hotkey; instead toggle image format via a new setting in Options.ini
  • Apply image compression / quality via a new setting in Options.ini
  • Can we consolidate this logic in core instead of repeating the implementation twice?

RE moving logic to core, I moved what I could to core - but there are a lot more files that need to be moved to core before this can be moved there eg WWVegas/WW3D2/*

@bobtista bobtista force-pushed the bobtista/compressed-screenshot-f11 branch 3 times, most recently from 3535e1e to efc773f Compare November 3, 2025 17:45
Comment thread Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp Outdated
@bobtista bobtista force-pushed the bobtista/compressed-screenshot-f11 branch 2 times, most recently from 977a6dc to f8162f3 Compare November 3, 2025 23:07
Comment thread cmake/stb.cmake Outdated
@bobtista bobtista force-pushed the bobtista/compressed-screenshot-f11 branch from d7e8a8d to d197bdd Compare November 5, 2025 18:04
@bobtista bobtista force-pushed the bobtista/compressed-screenshot-f11 branch from d197bdd to 9669966 Compare November 11, 2025 21:27
@xezon

xezon commented Nov 22, 2025

Copy link
Copy Markdown

Needs rebase.

@bobtista bobtista force-pushed the bobtista/compressed-screenshot-f11 branch from 9669966 to 4897b0b Compare November 22, 2025 18:10
@bobtista

Copy link
Copy Markdown
Author

Needs rebase.

Done

Comment thread Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h Outdated
Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
Comment thread Generals/Code/GameEngine/Include/Common/MessageStream.h Outdated
Comment thread Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h Outdated
Comment thread Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp Outdated
Skyaero42
Skyaero42 previously approved these changes Nov 22, 2025

@Skyaero42 Skyaero42 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

@bobtista bobtista force-pushed the bobtista/compressed-screenshot-f11 branch from 9c99306 to de35e57 Compare December 3, 2025 16:48
Comment thread Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h Outdated
@bobtista bobtista force-pushed the bobtista/compressed-screenshot-f11 branch from ec48835 to 7b46fad Compare July 8, 2026 20:52
Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp Outdated
Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp Outdated
strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname));

DWORD threadId;
HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spawning new thread for every image is quite an expensive approach. It would be better to have a screenshot processing thread sleeping and wake up when new work is ready. Unfortunately with c++98 we do not have good sync primitives for that so maybe the thread spawn is ok for now.

Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
Comment thread GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h Outdated
Comment thread GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h Outdated
Comment thread scripts/cpp/unify_move_files.py Outdated
Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp Outdated
index = 3 * (x + y * width);
image[index] = srcLine[4 * x + 2];
image[index + 1] = srcLine[4 * x + 1];
image[index + 2] = srcLine[4 * x + 0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused about the byte assignments. Here it assigns BGR and in 16 bit it assigns RGB. In the GenTool code its the exact inverse for both.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both of my branches produce RGB output, and both of GenTool’s produce BGR output, each is internally consistent, they just target different destinations.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so the 32 bit code path is little-endian specific. It will not work correctly on big-endian. That is why it is so confusing.

Try:

const unsigned int* srcLine = reinterpret_cast<const unsigned int*>(data->pixelData + y * width * 4);

for (x = 0; x < width; ++x)
{
    const unsigned int argb = srcLine[x];
    image[index + 0] = (unsigned char)((argb >> 16) & 0xFF); // r
    image[index + 1] = (unsigned char)((argb >> 08) & 0xFF); // g
    image[index + 2] = (unsigned char)((argb >> 00) & 0xFF); // b
}

This is then also in line with how the 16 bit version does it.

Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp Outdated
Comment thread Generals/Code/GameEngine/Include/Common/MessageStream.h Outdated
Comment thread Core/GameEngine/Source/Common/OptionPreferences.cpp Outdated
Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp Outdated
return 80;

// TheSuperHackers @tweak bobtista 08/07/2026 Cap the quality at 95, because JPEG quality
// TheSuperHackers @feature bobtista 08/07/2026 Cap the quality at 95, because JPEG quality

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think @feature fits either. This talks about about why a quality setting is capped at 95, not about adding a new feature.

for (y = 0; y < height; y++)
{
const unsigned short* srcLine = (const unsigned short*)(data->pixelData + y * width * 2);
for (x = 0; x < width; x++)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: Use ++x, ++y, because that is what it is supposed to do

// Convert R5G6B5 to R8G8B8
for (y = 0; y < height; y++)
{
const unsigned short* srcLine = (const unsigned short*)(data->pixelData + y * width * 2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer reinterpret_cast for pointer casts.

// TheSuperHackers @feature bobtista 08/07/2026 Cap the quality at 95, because JPEG quality
// above that increases the file size significantly with no visible benefit.
Int quality = atoi(it->second.str());
return clamp(1, quality, 95);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still offers quality 1. What is the use case for this low quality level?


for (y = 0; y < height; y++)
{
memcpy(pixels + y * width * bytesPerPixel, (const unsigned char*)lrect.pBits + y * lrect.Pitch, width * bytesPerPixel);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appeared to work and is a bit simpler:

diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
index 7d3e84791..d4dbe7c07 100644
--- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
+++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
@@ -171,7 +171,6 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality)
 		return;
 	}
 
-	unsigned int y;
 	unsigned int width = surfaceDesc.Width;
 	unsigned int height = surfaceDesc.Height;
 
@@ -179,11 +178,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality)
 	// operations are done on the screenshot thread to keep the main thread cheap.
 	const unsigned int bytesPerPixel = is32Bit ? 4 : 2;
 	unsigned char* pixels = new unsigned char[bytesPerPixel * width * height];
-
-	for (y = 0; y < height; y++)
-	{
-		memcpy(pixels + y * width * bytesPerPixel, (const unsigned char*)lrect.pBits + y * lrect.Pitch, width * bytesPerPixel);
-	}
+	memcpy(pixels, lrect.pBits, bytesPerPixel * width * height);
 
 	surfaceCopy->Unlock();
 	surfaceCopy->Release_Ref();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this work correctly in regards to lrect.Pitch ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so because lrect.Pitch * height equals bytesPerPixel * width * height, we're copying all the channels


UnicodeString ufileName;
ufileName.translate(leafname);
TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Success message races write This message is shown as soon as the worker thread is created, before stbi_write_jpg or stbi_write_png has written the file. If the Screenshots directory cannot be created, the path is invalid, or the file write fails, the worker only logs the failure while the player has already been told the screenshot was captured. The success message should be tied to the worker's write result, or changed to say the capture was queued.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
Line: 211

Comment:
**Success message races write** This message is shown as soon as the worker thread is created, before `stbi_write_jpg` or `stbi_write_png` has written the file. If the `Screenshots` directory cannot be created, the path is invalid, or the file write fails, the worker only logs the failure while the player has already been told the screenshot was captured. The success message should be tied to the worker's write result, or changed to say the capture was queued.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Is new feature or request Gen Relates to Generals Input Major Severity: Minor < Major < Critical < Blocker ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add JPG/PNG Screenshot with no stalls BMP Screenshot taken with F12 can stall game significantly

9 participants