port: grp.SaveScreenShot via next-frame swapchain readback + JPEG

CPythonGraphic::SaveScreenShot queues a readback of the next presented
frame (ScreenShotHost -> VulkanWindow::request_frame); WriteScreenShot runs
the rest of 40250's function verbatim (X8R8G8B8 rows, SaveJPEG, EXIF tag).
jpeg_save is encoded with stb_image_write. SHGetSpecialFolderPath returns
$HOME/Documents (else cwd); CreateDirectory/_access/jpeg_save accept '\\'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-30 12:43:03 +09:00
co-authored by Claude Opus 5.5
parent c21e012965
commit bda85eea50
17 changed files with 2211 additions and 12 deletions
+37
View File
@@ -0,0 +1,37 @@
// Platform implementation of EterLib/JpegFile.h. 40250 writes it with libjpeg-9a (JpegFile.cpp: a stdio
// destination manager, JCS_RGB, jpeg_set_defaults + jpeg_set_quality); here stb_image_write encodes the
// same input (top-down RGB rows, 3 bytes per pixel) at the same quality into a baseline JFIF file.
//
// Only jpeg_save has a caller in 40250 (CPythonGraphic::SaveJPEG, the screenshot key); the other
// declarations of JpegFile.h stay undefined.
#include "EterLib/StdAfx.h"
#include "EterLib/JpegFile.h"
#include <algorithm>
#include <string>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STB_IMAGE_WRITE_STATIC
#define STBI_WRITE_NO_STDIO
#include "../third_party/stb_image_write.h"
namespace
{
void write_to_file(void* context, void* data, int size)
{
fwrite(data, 1, size_t(size), static_cast<FILE*>(context));
}
}
int jpeg_save(unsigned char*data_d, int width, int height, int quality, const char*filename)
{
// The Win32 CRT takes '\\' as a separator (grp.SaveScreenShot builds "<Documents>\\METIN2\\<time>.jpg").
std::string path(filename);
std::replace(path.begin(), path.end(), '\\', '/');
FILE* fi = fopen(path.c_str(), "wb");
if (fi == NULL)
return 0;
const int ok = stbi_write_jpg_to_func(write_to_file, fi, width, height, 3, data_d, quality);
const bool closed = fclose(fi) == 0;
return ok && closed ? 1 : 0;
}
+159 -1
View File
@@ -9,6 +9,9 @@
#include "EterPythonLib/StdAfx.h"
#include "EterPythonLib/PythonGraphic.h"
#include "EterLib/StateManager.h"
#include "EterLib/JpegFile.h"
#include "EterBase/CRC32.h"
#include "ScreenShotHost.h"
#include "../PlatformStub.h"
#include "../EterLib/UIRenderCommands.h"
@@ -163,7 +166,162 @@ void CPythonGraphic::RenderUpButton(float sx, float sy, float ex, float ey)
RenderLine2d(sx, ey, ex, ey);
RenderLine2d(ex, sy, ex, ey);
}
bool CPythonGraphic::SaveScreenShot(const char*) { MT_PLATFORM_STUB(); return false; }
// 40250 PythonGraphic.cpp:6
bool g_isScreenShotKey = false;
// 40250 PythonGraphic.cpp:157
void GenScreenShotTag(const char* src, DWORD crc32, char* leaf, size_t leafLen)
{
const char* p = src;
const char* n = p;
while (n = strchr(p, '\\'))
p = n + 1;
_snprintf(leaf, leafLen, "YMIR_METIN2:%s:0x%.8x", p, crc32);
}
// 40250 PythonGraphic.cpp:167
bool CPythonGraphic::SaveJPEG(const char * pszFileName, LPBYTE pbyBuffer, UINT uWidth, UINT uHeight)
{
return jpeg_save(pbyBuffer, uWidth, uHeight, 100, pszFileName) != 0;
}
namespace
{
ScreenShotHost::Request g_screenShotRequest;
}
void ScreenShotHost::SetRequest(Request request)
{
g_screenShotRequest = std::move(request);
}
// 40250 PythonGraphic.cpp:172. PORT: the back-buffer lock becomes a request for the next presented frame
// (ScreenShotHost.h); the rest of the function runs in ScreenShotHost::WriteScreenShot when it arrives, so
// a true return means the capture was queued.
bool CPythonGraphic::SaveScreenShot(const char * c_pszFileName)
{
if (!g_screenShotRequest || !g_screenShotRequest(c_pszFileName))
{
TraceError("Failed to get back buffer (0x%08x)", E_FAIL);
return false;
}
return true;
}
// 40250 PythonGraphic.cpp:172, from the locked surface on (the swapchain reads back as D3DFMT_X8R8G8B8).
bool ScreenShotHost::WriteScreenShot(const char* c_pszFileName, const std::uint8_t* bgrx, unsigned uWidth, unsigned uHeight, unsigned pitch)
{
BYTE* pbyBuffer = new BYTE[uWidth * uHeight * 3];
const BYTE* pbySource = bgrx;
BYTE* pbyDestination = (BYTE*) pbyBuffer;
for(UINT y = 0; y < uHeight; ++y) {
const BYTE *pRow = pbySource;
for(UINT x = 0; x < uWidth; ++x) {
*pbyDestination++ = pRow[2]; // Blue
*pbyDestination++ = pRow[1]; // Green
*pbyDestination++ = pRow[0]; // Red
pRow += 4;
}
// increase by one line
pbySource += pitch;
}
bool bSaved = CPythonGraphic::Instance().SaveJPEG(c_pszFileName, pbyBuffer, uWidth, uHeight);
if(pbyBuffer) {
delete [] pbyBuffer;
pbyBuffer = NULL;
}
if(bSaved == false) {
TraceError("Failed to save JPEG file. (%s, %d, %d)", c_pszFileName, uWidth, uHeight);
return false;
}
if (g_isScreenShotKey)
{
FILE* srcFilePtr = fopen(c_pszFileName, "rb");
if (srcFilePtr)
{
fseek(srcFilePtr, 0, SEEK_END);
size_t fileSize = ftell(srcFilePtr);
fseek(srcFilePtr, 0, SEEK_SET);
char head[21];
size_t tailSize = fileSize - sizeof(head);
char* tail = (char*)malloc(tailSize);
fread(head, sizeof(head), 1, srcFilePtr);
fread(tail, tailSize, 1, srcFilePtr);
fclose(srcFilePtr);
char imgDesc[64];
GenScreenShotTag(c_pszFileName, GetCRC32(tail, tailSize), imgDesc, sizeof(imgDesc));
int imgDescLen = strlen(imgDesc) + 1;
unsigned char exifHeader[] = {
0xe1,
0, // blockLen[1],
0, // blockLen[0],
0x45,
0x78,
0x69,
0x66,
0x0,
0x0,
0x49,
0x49,
0x2a,
0x0,
0x8,
0x0,
0x0,
0x0,
0x1,
0x0,
0xe,
0x1,
0x2,
0x0,
static_cast<unsigned char>(imgDescLen), // textLen[0],
0, // textLen[1],
0, // textLen[2],
0, // textLen[3],
0x1a,
0x0,
0x0,
0x0,
0x0,
0x0,
0x0,
0x0,
};
exifHeader[2] = sizeof(exifHeader) + imgDescLen;
FILE* dstFilePtr = fopen(c_pszFileName, "wb");
//FILE* dstFilePtr = fopen("temp.jpg", "wb");
if (dstFilePtr)
{
fwrite(head, sizeof(head), 1, dstFilePtr);
fwrite(exifHeader, sizeof(exifHeader), 1, dstFilePtr);
fwrite(imgDesc, imgDescLen, 1, dstFilePtr);
fputc(0x00, dstFilePtr);
fputc(0xff, dstFilePtr);
fwrite(tail, tailSize, 1, dstFilePtr);
fclose(dstFilePtr);
}
free(tail);
}
}
return true;
}
// 40250 PythonGraphic.cpp:52.
void CPythonGraphic::SetCursorPosition(int x, int y)
{
@@ -0,0 +1,25 @@
#pragma once
// The frame readback behind CPythonGraphic::SaveScreenShot (grp.SaveScreenShot / SaveScreenShotToPath).
//
// 40250 locks the D3D8 back buffer inside the call and writes the JPEG before returning. Here the script
// thread and the host take turns, so the call cannot wait for the host to read the frame back: it asks the
// host for the next presented frame and returns true; the host hands the pixels to WriteScreenShot, which
// runs the rest of 40250's SaveScreenShot (X8R8G8B8 -> RGB rows, SaveJPEG, the g_isScreenShotKey EXIF tag)
// one frame later.
//
// Plain std types only: the SDL host includes this without the port headers.
#include <cstdint>
#include <functional>
#include <string>
namespace ScreenShotHost
{
// Asks the host to call WriteScreenShot(path, ...) with the next presented frame. False when the host
// cannot read the swapchain back.
using Request = std::function<bool(const std::string& path)>;
void SetRequest(Request request);
// `bgrx`: top-down rows, 4 bytes per pixel in D3DFMT_X8R8G8B8 memory order, `pitch` bytes apart.
bool WriteScreenShot(const char* path, const std::uint8_t* bgrx, unsigned width, unsigned height, unsigned pitch);
}
File diff suppressed because it is too large Load Diff