feat(screenshot): Add threaded JPEG/PNG screenshots without game stalls#1785
feat(screenshot): Add threaded JPEG/PNG screenshots without game stalls#1785bobtista wants to merge 28 commits into
Conversation
20a3df1 to
37bd840
Compare
|
Some initial thoughts:
|
|
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. |
|
Regarding Github formatting: When you write
Then it will not close this report when this is merged. Please read up on it here: |
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/* |
3535e1e to
efc773f
Compare
977a6dc to
f8162f3
Compare
d7e8a8d to
d197bdd
Compare
d197bdd to
9669966
Compare
|
Needs rebase. |
9669966 to
4897b0b
Compare
Done |
9c99306 to
de35e57
Compare
…and return write result
…te stb.cmake comment
ec48835 to
7b46fad
Compare
| strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); | ||
|
|
||
| DWORD threadId; | ||
| HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); |
There was a problem hiding this comment.
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.
| 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]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
both of my branches produce RGB output, and both of GenTool’s produce BGR output, each is internally consistent, they just target different destinations.
There was a problem hiding this comment.
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.
…e screenshot thread
… MessageStream members
…a and MessageStream members
| 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 |
There was a problem hiding this comment.
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++) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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();There was a problem hiding this comment.
Will this work correctly in regards to lrect.Pitch ?
There was a problem hiding this comment.
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()); |
There was a problem hiding this 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.
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.
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:
Notes
Testing